From 17884d4011a0acce158a35364e061e4856789900 Mon Sep 17 00:00:00 2001 From: hzhaoy Date: Wed, 6 May 2026 15:23:52 +0800 Subject: [PATCH] Render TUI before slow agent setup The TUI path was constructing the DeepAgents graph before Textual could mount the first screen, so cold imports and model graph setup made startup feel frozen. This change lets the interface mount immediately and initializes the agent bridge in a background thread, keeping input disabled until the bridge is ready. Constraint: deepagents/langchain cold initialization remains expensive and is required before processing user requests Rejected: Remove or replace deepagents imports | outside the scope of a small startup responsiveness fix Confidence: high Scope-risk: moderate Directive: Keep the input disabled until AgentBridge exists; otherwise user requests can race against lazy initialization Tested: uv run pytest Not-tested: Manual interactive TUI session after push --- src/deep_code_agent/cli.py | 7 +- src/deep_code_agent/tui/app.py | 78 +++++++++++++++++-- .../tui/screens/main_screen.py | 6 +- tests/test_cli.py | 20 ++++- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/src/deep_code_agent/cli.py b/src/deep_code_agent/cli.py index 305e075..79742e2 100644 --- a/src/deep_code_agent/cli.py +++ b/src/deep_code_agent/cli.py @@ -275,9 +275,10 @@ def _run_tui_mode(args) -> None: print(f"🚀 Starting Deep Code Agent TUI...") print(f"📁 Codebase: {codebase_dir}") - try: - agent = _initialize_agent(args, codebase_dir) + def agent_factory(): + return _initialize_agent(args, codebase_dir) + try: session_info = { "model": args.model_name or "default", "session_id": args.thread_id, @@ -285,7 +286,7 @@ def _run_tui_mode(args) -> None: } app = DeepCodeAgentApp( - agent=agent, + agent_factory=agent_factory, config={"configurable": {"thread_id": args.thread_id}}, session_info=session_info, ) diff --git a/src/deep_code_agent/tui/app.py b/src/deep_code_agent/tui/app.py index c5e4e73..e625e3e 100644 --- a/src/deep_code_agent/tui/app.py +++ b/src/deep_code_agent/tui/app.py @@ -3,6 +3,8 @@ from __future__ import annotations import os +import threading +from collections.abc import Callable from typing import TYPE_CHECKING, Any from textual.app import App, ComposeResult @@ -42,25 +44,37 @@ class DeepCodeAgentApp(App): session_info = reactive({}) auto_approve_tools = reactive([]) - def __init__(self, agent: Any, config: dict | None = None, session_info: dict | None = None, **kwargs): + def __init__( + self, + agent: Any | None = None, + config: dict | None = None, + session_info: dict | None = None, + agent_factory: Callable[[], Any] | None = None, + **kwargs, + ): """Initialize the TUI app. Args: - agent: LangGraph agent + agent: LangGraph agent, if already initialized config: Optional RunnableConfig dict session_info: Optional session metadata + agent_factory: Optional callable used to initialize the agent after the TUI mounts **kwargs: Additional arguments for App """ super().__init__(**kwargs) self.agent = agent + self.agent_factory = agent_factory self.config = config or {"configurable": {"thread_id": "default"}} self.session_info = session_info or {} self.auto_approve_tools = [] self.debug_tool_calls = os.getenv("DEBUG_TOOL_CALLS", "0").strip().lower() in {"1", "true", "yes", "on"} - # Create bridge - self.bridge = AgentBridge(agent, self) - self.bridge.set_config(self.config) + # Create bridge only when the agent is available. In TUI mode the agent + # can be initialized lazily so the first screen renders immediately. + self.bridge = AgentBridge(agent, self) if agent is not None else None + if self.bridge is not None: + self.bridge.set_config(self.config) + self._agent_initialization_started = False self._main_screen: MainScreen | None = None self._chat_log: ChatLog | None = None self._status_bar: StatusBar | None = None @@ -85,6 +99,58 @@ def register_main_screen(self, screen: MainScreen) -> None: self._status_bar = screen.get_status_bar() self._input_box = screen.get_input_box() self._side_panel = screen.get_side_panel() + if self.bridge is None and self.agent_factory is not None: + self.start_agent_initialization() + + @property + def is_agent_ready(self) -> bool: + """Return whether the app has an initialized agent bridge.""" + return self.bridge is not None + + def start_agent_initialization(self) -> None: + """Initialize the agent in the background after the TUI has mounted.""" + if self._agent_initialization_started or self.agent_factory is None: + return + + self._agent_initialization_started = True + if self._status_bar is not None: + self._status_bar.set_thinking("Initializing agent...") + if self._input_box is not None: + self._input_box.set_disabled(True) + + thread = threading.Thread(target=self._initialize_agent_in_thread, name="agent-initializer", daemon=True) + thread.start() + + def _initialize_agent_in_thread(self) -> None: + try: + if self.agent_factory is None: + raise RuntimeError("Agent factory is not configured") + agent = self.agent_factory() + except Exception as exc: + self.call_from_thread(self._handle_agent_initialization_error, exc) + return + + self.call_from_thread(self._complete_agent_initialization, agent) + + def _complete_agent_initialization(self, agent: Any) -> None: + self.agent = agent + self.bridge = AgentBridge(agent, self) + self.bridge.set_config(self.config) + if self._status_bar is not None: + self._status_bar.set_ready("Ready") + if self._input_box is not None: + self._input_box.set_disabled(False) + if self._chat_log is not None: + self._chat_log.add_system_message("Agent initialized. Type your request below.") + + def _handle_agent_initialization_error(self, exc: Exception) -> None: + message = str(exc) + if self._status_bar is not None: + self._status_bar.set_error(message) + if self._input_box is not None: + self._input_box.set_disabled(True) + if self._chat_log is not None: + self._chat_log.add_system_message(f"Error initializing agent: {message}") def action_toggle_dark(self) -> None: """Toggle dark mode.""" @@ -108,4 +174,6 @@ def update_session_info(self, session_info: dict) -> None: def get_bridge(self) -> AgentBridge: """Get the agent bridge.""" + if self.bridge is None: + raise RuntimeError("Agent is still initializing") return self.bridge diff --git a/src/deep_code_agent/tui/screens/main_screen.py b/src/deep_code_agent/tui/screens/main_screen.py index a193911..b7e4b90 100644 --- a/src/deep_code_agent/tui/screens/main_screen.py +++ b/src/deep_code_agent/tui/screens/main_screen.py @@ -64,7 +64,11 @@ def on_mount(self) -> None: except Exception: pass chat_log = self.query_one("#chat_log", ChatLog) - chat_log.add_system_message("🧠 Deep Code Agent ready! Type your request below.") + is_agent_ready = bool(getattr(self.app, "is_agent_ready", True)) + if is_agent_ready: + chat_log.add_system_message("Deep Code Agent ready! Type your request below.") + else: + chat_log.add_system_message("Initializing agent...") def action_toggle_dark(self) -> None: """Toggle dark mode.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 6c5feed..ee6cc4f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,7 @@ import pytest from deep_code_agent import __version__ -from deep_code_agent.cli import _initialize_agent, main +from deep_code_agent.cli import _initialize_agent, _run_tui_mode, main def test_main_prints_version_and_exits(capsys): @@ -79,3 +79,21 @@ def test_initialize_agent_builds_model_for_explicit_provider( assert mock_create_code_agent.call_args.kwargs["model"] is mock_create_chat_model.return_value assert mock_create_code_agent.call_args.kwargs["backend_type"] == "filesystem" assert mock_create_code_agent.call_args.kwargs["checkpointer"] is mock_checkpointer.return_value + + +@patch("deep_code_agent.tui.DeepCodeAgentApp") +@patch("deep_code_agent.cli._initialize_agent") +def test_tui_mode_defers_agent_initialization_until_after_app_starts(mock_initialize_agent, mock_app_class): + """TUI mode should render before doing slow agent initialization.""" + args = SimpleNamespace( + model_name=None, + thread_id="test-thread", + ) + app_instance = mock_app_class.return_value + + _run_tui_mode(args) + + mock_initialize_agent.assert_not_called() + mock_app_class.assert_called_once() + assert "agent_factory" in mock_app_class.call_args.kwargs + app_instance.run.assert_called_once_with()