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()