diff --git a/clients/terminal/tui/commands/builtin.py b/clients/terminal/tui/commands/builtin.py index 684a0e6..ae2e22b 100644 --- a/clients/terminal/tui/commands/builtin.py +++ b/clients/terminal/tui/commands/builtin.py @@ -11,7 +11,6 @@ from clients.terminal.config import settings from clients.terminal.tui.commands.base import BaseCommand, CommandMeta from clients.terminal.tui.context import TuiContext -from clients.terminal.tui.events import SessionInfo, SessionListUpdated from clients.terminal.tui.settings import get_tui_settings @@ -62,7 +61,6 @@ app = ctx.app() await app.attach_session(session["session_id"]) ctx.chat_panel.clear() - await _broadcast_session_list(ctx) class SessionsCommand(BaseCommand): @@ -88,7 +86,6 @@ title = s.get("name", "") or s.get("preview", "") lines.append(f"{marker}{sid[:8]} {s.get('profile_id', 'unknown')} {title}") ctx.chat_panel.handle_ws_event({"type": "status", "content": "\n".join(lines)}) - await _broadcast_session_list(ctx) class SwitchCommand(BaseCommand): @@ -132,7 +129,6 @@ app = ctx.app() await app.attach_session(session["session_id"]) ctx.chat_panel.clear() - await _broadcast_session_list(ctx) class ProfileCommand(BaseCommand): @@ -352,23 +348,3 @@ subprocess.Popen([editor, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except OSError as exc: raise OSError(f"cannot start editor '{editor}': {exc}") from exc - - -async def _broadcast_session_list(ctx: TuiContext) -> None: - """Refresh the sessions panel with the latest server state.""" - if not ctx.app: - return - try: - sessions = api.list_sessions() - except Exception: - return - info_list = [ - SessionInfo( - id=s.get("session_id", ""), - profile_id=s.get("profile_id", "unknown"), - title=s.get("name", "") or s.get("preview", ""), - created_at=s.get("created_at", ""), - ) - for s in sessions - ] - ctx.app().post_message(SessionListUpdated(info_list, ctx.session_id)) diff --git a/clients/terminal/tui/context.py b/clients/terminal/tui/context.py index b2cd02d..f54f503 100644 --- a/clients/terminal/tui/context.py +++ b/clients/terminal/tui/context.py @@ -11,7 +11,6 @@ from clients.terminal.tui.chat_model import ChatModel from clients.terminal.tui.settings import TuiSettings from clients.terminal.tui.widgets.chat_panel import ChatPanel - from clients.terminal.tui.widgets.sessions_panel import SessionsPanel from clients.terminal.tui.widgets.status_panel import StatusPanel from clients.terminal.ws_client import NaviWebSocketClient @@ -27,7 +26,6 @@ settings: "TuiSettings | None" = None chat_panel: "ChatPanel | None" = None status_panel: "StatusPanel | None" = None - sessions_panel: "SessionsPanel | None" = None chat_model: "ChatModel | None" = None cwd: Path = field(default_factory=lambda: Path.cwd().resolve()) diff --git a/clients/terminal/tui/events.py b/clients/terminal/tui/events.py index 012aaba..e21e4f8 100644 --- a/clients/terminal/tui/events.py +++ b/clients/terminal/tui/events.py @@ -2,8 +2,6 @@ from __future__ import annotations -from dataclasses import dataclass - from textual.message import Message @@ -49,28 +47,3 @@ self.args = args self.message = message super().__init__() - - -@dataclass -class SessionInfo: - id: str - profile_id: str - title: str = "" - created_at: str = "" - - -class SessionListUpdated(Message): - """Sessions list changed (loaded, switched, created).""" - - def __init__(self, sessions: list[SessionInfo], current_id: str | None) -> None: - self.sessions = sessions - self.current_id = current_id - super().__init__() - - -class SessionSelected(Message): - """User selected a session from the sessions panel.""" - - def __init__(self, session_id: str) -> None: - self.session_id = session_id - super().__init__() diff --git a/clients/terminal/tui/tui_app.py b/clients/terminal/tui/tui_app.py index f175d09..773b360 100644 --- a/clients/terminal/tui/tui_app.py +++ b/clients/terminal/tui/tui_app.py @@ -16,9 +16,6 @@ from clients.terminal.tui.events import ( ConnectionStatusChanged, PermissionRequest, - SessionInfo, - SessionListUpdated, - SessionSelected, UserSubmitted, WsEvent, ) @@ -30,7 +27,7 @@ from clients.terminal.tui.commands.registry import get_registry from clients.terminal.tui.screens.command_palette import CommandPaletteScreen from clients.terminal.tui.screens.permission_dialog import PermissionDialogScreen -from clients.terminal.tui.widgets import ChatPanel, InputBox, SessionsPanel, StatusBar, StatusPanel +from clients.terminal.tui.widgets import ChatPanel, InputBox, StatusBar, StatusPanel from clients.terminal.tui.ws_bridge import WsBridge @@ -64,7 +61,6 @@ set_active_theme(self._theme_name) self._chat_panel = ChatPanel() self._status_panel = StatusPanel() - self._sessions_panel = SessionsPanel() self._input_box = InputBox() self._status_bar = StatusBar() self._state = StateManager() @@ -72,7 +68,6 @@ state=self._state, chat_panel=self._chat_panel, status_panel=self._status_panel, - sessions_panel=self._sessions_panel, settings=self._tui_settings, cwd=cwd or Path.cwd().resolve(), ) @@ -90,7 +85,6 @@ yield self._chat_panel with Vertical(): yield self._status_panel - yield self._sessions_panel yield self._input_box yield self._status_bar @@ -214,40 +208,6 @@ self._chat_panel.handle_ws_event( {"type": "status", "content": f"Connected to {session_id[:8]}"} ) - self.run_worker(self._refresh_sessions(session_id)) - - async def _refresh_sessions(self, current_session_id: str | None = None) -> None: - current = current_session_id or self._ctx.session_id - try: - raw_sessions = api.list_sessions() - except Exception: - return - sessions = [self._session_info_from_api(item) for item in raw_sessions] - self.post_message(SessionListUpdated(sessions, current)) - - @staticmethod - def _session_info_from_api(item: dict) -> SessionInfo: - sid = item.get("session_id", "") - return SessionInfo( - id=sid, - profile_id=item.get("profile_id", "unknown"), - title=item.get("name", "") or item.get("preview", ""), - created_at=item.get("created_at", ""), - ) - - def on_session_selected(self, event: SessionSelected) -> None: - self.run_worker(self._switch_session(event.session_id)) - - async def _switch_session(self, session_id: str) -> None: - self._state.set_session_id(session_id) - await self.attach_session(session_id) - self._chat_panel.clear() - self._chat_panel.handle_ws_event( - {"type": "status", "content": f"Switched to {session_id[:8]}"} - ) - - def on_session_list_updated(self, event: SessionListUpdated) -> None: - self._sessions_panel.on_session_list_updated(event) def on_user_submitted(self, event: UserSubmitted) -> None: text = event.text diff --git a/clients/terminal/tui/widgets/__init__.py b/clients/terminal/tui/widgets/__init__.py index 42d3bfc..ee0a295 100644 --- a/clients/terminal/tui/widgets/__init__.py +++ b/clients/terminal/tui/widgets/__init__.py @@ -4,8 +4,7 @@ from .chat_panel import ChatPanel from .input_box import InputBox -from .sessions_panel import SessionsPanel from .status_bar import StatusBar from .status_panel import StatusPanel -__all__ = ["ChatPanel", "InputBox", "SessionsPanel", "StatusBar", "StatusPanel"] +__all__ = ["ChatPanel", "InputBox", "StatusBar", "StatusPanel"] diff --git a/clients/terminal/tui/widgets/sessions_panel.py b/clients/terminal/tui/widgets/sessions_panel.py deleted file mode 100644 index 9184efc..0000000 --- a/clients/terminal/tui/widgets/sessions_panel.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Sessions panel widget for the TUI.""" - -from __future__ import annotations - -from textual.app import ComposeResult -from textual.containers import Vertical -from textual.widgets import DataTable, Static -from textual.widgets.data_table import RowKey - -from clients.terminal.tui.events import SessionListUpdated, SessionSelected, SessionInfo - - -class SessionsPanel(Vertical): - """Right-side panel showing server sessions and allowing quick switching.""" - - DEFAULT_CSS = """ - SessionsPanel { - border: solid $tui-panel; - background: $tui-panel; - color: $tui-text-muted; - padding: 0; - height: 1fr; - width: 1fr; - } - SessionsPanel .title { - text-style: bold; - color: $tui-primary; - padding: 1 1 0 1; - height: auto; - } - SessionsPanel DataTable { - height: 1fr; - border: none; - background: $tui-panel; - color: $tui-text-muted; - } - SessionsPanel DataTable > .datatable--header { - color: $tui-text-dim; - text-style: bold; - background: $tui-surface; - } - SessionsPanel DataTable > .datatable--row { - background: $tui-panel; - } - SessionsPanel DataTable > .datatable--row-sessions-panel-cursor { - background: $tui-selection; - color: $tui-background; - } - SessionsPanel DataTable > .datatable--row-sessions-panel-cursor .datatable--cursor { - color: $tui-background; - } - SessionsPanel .empty { - color: $tui-text-dim; - text-align: center; - padding: 1; - } - """ - - def __init__(self) -> None: - super().__init__() - self._title = Static("Sessions", classes="title") - self._table: DataTable | None = None - self._sessions: list[SessionInfo] = [] - self._current_id: str | None = None - - def compose(self) -> ComposeResult: - yield self._title - yield DataTable(id="sessions-table") - - def on_mount(self) -> None: - self._table = self.query_one("#sessions-table", DataTable) - self._table.cursor_type = "row" - self._table.show_header = True - self._table.zebra_stripes = True - self._table.add_columns("", "ID", "Profile", "Preview") - self._populate_table() - - def on_session_list_updated(self, event: SessionListUpdated) -> None: - self._sessions = event.sessions - self._current_id = event.current_id - self._populate_table() - - def _populate_table(self) -> None: - if self._table is None: - return - self._table.clear() - if not self._sessions: - self._table.add_row("", "—", "", "No sessions") - return - - for session in self._sessions: - marker = "●" if session.id == self._current_id else "" - short_id = "-".join(session.id.split("-")[:2]) - profile = self._truncate(session.profile_id, 12) - preview = self._truncate(session.title, 24) - self._table.add_row(marker, short_id, profile, preview, key=session.id) - - if self._current_id: - try: - self._table.move_cursor(row=RowKey(self._current_id)) - except Exception: - pass - - def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - session_id = str(event.row_key.value) if event.row_key else "" - if session_id: - self.post_message(SessionSelected(session_id)) - - @staticmethod - def _truncate(text: str, max_len: int) -> str: - if len(text) <= max_len: - return text - return text[: max_len - 1] + "…" diff --git a/tests/clients/test_tui_app.py b/tests/clients/test_tui_app.py index f05f459..2902774 100644 --- a/tests/clients/test_tui_app.py +++ b/tests/clients/test_tui_app.py @@ -30,10 +30,9 @@ """Mock the backend REST api so TUI tests never hit a live server. Every test mounts NaviCodeTui, whose _startup worker calls api.create_session / - api.get_session and whose _refresh_sessions worker calls api.list_sessions. - Without a server these raise and _ctx.session_id stays None, which breaks any - test that asserts on the session. Tests that assert on stop_session still - monkeypatch it per-test with their own call-tracking list. + api.get_session. Without a server these raise and _ctx.session_id stays None, + which breaks any test that asserts on the session. Tests that assert on + stop_session still monkeypatch it per-test with their own call-tracking list. """ import clients.terminal.api as api_module @@ -62,7 +61,6 @@ await pilot.pause() assert pilot.app.query_one("ChatPanel") is not None assert pilot.app.query_one("StatusPanel") is not None - assert pilot.app.query_one("SessionsPanel") is not None assert pilot.app.query_one("InputBox") is not None diff --git a/tests/clients/test_tui_sessions_panel.py b/tests/clients/test_tui_sessions_panel.py deleted file mode 100644 index 110055d..0000000 --- a/tests/clients/test_tui_sessions_panel.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Tests for the SessionsPanel widget and session switching.""" - -from __future__ import annotations - -from pathlib import Path - -import pytest - -from clients.terminal.tui.events import SessionInfo, SessionListUpdated -from clients.terminal.tui.tui_app import NaviCodeTui - - -@pytest.fixture(autouse=True) -def tmp_state_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Override the state dir so tests never touch ~/.navi_code.""" - from clients.terminal import config - - original = config.settings.state_dir - config.settings.state_dir = tmp_path - import clients.terminal.tui.settings as settings_module - - settings_module._tui_settings = None - yield tmp_path - config.settings.state_dir = original - settings_module._tui_settings = None - - -@pytest.mark.anyio -async def test_sessions_panel_mounts() -> None: - """The TUI layout includes a SessionsPanel on the right.""" - async with NaviCodeTui(new_session=True).run_test() as pilot: - await pilot.pause() - assert pilot.app.query_one("SessionsPanel") is not None - - -@pytest.mark.anyio -async def test_session_list_updated_populates_table() -> None: - """Posting SessionListUpdated fills the sessions table.""" - async with NaviCodeTui(new_session=True).run_test() as pilot: - await pilot.pause() - panel = pilot.app.query_one("SessionsPanel") - sessions = [ - SessionInfo(id="sess-aaaa-1111", profile_id="navi_code", title="First", created_at=""), - SessionInfo(id="sess-bbbb-2222", profile_id="dev", title="Second", created_at=""), - ] - pilot.app.post_message(SessionListUpdated(sessions, "sess-aaaa-1111")) - await pilot.pause() - table = panel._table - assert table is not None - assert table.row_count == 2 - row = table.get_row_at(0) - assert "sess-aaaa" in str(row) - assert "First" in str(row) - - -@pytest.mark.anyio -async def test_data_table_row_selects_session() -> None: - """Selecting a row in the sessions panel switches the active session.""" - async with NaviCodeTui(new_session=True).run_test() as pilot: - await pilot.pause() - panel = pilot.app.query_one("SessionsPanel") - sessions = [ - SessionInfo(id="sess-aaaa-1111", profile_id="navi_code", title="First", created_at=""), - SessionInfo(id="sess-bbbb-2222", profile_id="dev", title="Second", created_at=""), - ] - pilot.app.post_message(SessionListUpdated(sessions, "sess-aaaa-1111")) - await pilot.pause() - - table = panel._table - table.action_cursor_down() - await pilot.pause() - table.action_select_cursor() - await pilot.pause() - assert pilot.app._ctx.session_id == "sess-bbbb-2222" - - -@pytest.mark.anyio -async def test_enter_key_selects_session() -> None: - """Pressing Enter on a focused sessions table switches the active session.""" - async with NaviCodeTui(new_session=True).run_test() as pilot: - await pilot.pause() - panel = pilot.app.query_one("SessionsPanel") - sessions = [ - SessionInfo(id="sess-aaaa-1111", profile_id="navi_code", title="First", created_at=""), - SessionInfo(id="sess-bbbb-2222", profile_id="dev", title="Second", created_at=""), - ] - pilot.app.post_message(SessionListUpdated(sessions, "sess-aaaa-1111")) - await pilot.pause() - - table = panel._table - table.focus() - table.action_cursor_down() - await pilot.pause() - await pilot.press("enter") - await pilot.pause() - assert pilot.app._ctx.session_id == "sess-bbbb-2222"