Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/deep_code_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,17 +275,18 @@ 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,
"codebase_dir": codebase_dir,
}

app = DeepCodeAgentApp(
agent=agent,
agent_factory=agent_factory,
config={"configurable": {"thread_id": args.thread_id}},
session_info=session_info,
)
Expand Down
78 changes: 73 additions & 5 deletions src/deep_code_agent/tui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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."""
Expand All @@ -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
6 changes: 5 additions & 1 deletion src/deep_code_agent/tui/screens/main_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
20 changes: 19 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Loading