From ed796332fe4b52c6c1e2a4149b649e2b5ea79c56 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:00:22 +0800 Subject: [PATCH] refactor(tui): retire the direct session default --- nanobot/cli/agent.py | 8 +++++--- nanobot/cli/tui_launcher.py | 14 +++++++++----- tests/cli/test_tui_launcher.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/nanobot/cli/agent.py b/nanobot/cli/agent.py index d01201194..2d8272655 100644 --- a/nanobot/cli/agent.py +++ b/nanobot/cli/agent.py @@ -73,7 +73,7 @@ def agent( help="Terminal UI appearance: auto, dark, or light", ), ): - """Interact with the agent directly.""" + """Chat in the terminal or send one message non-interactively.""" from nanobot.bus.queue import MessageBus from nanobot.cron.service import CronService from nanobot.providers.factory import make_provider @@ -85,7 +85,7 @@ def agent( raise typer.BadParameter("must be auto, dark, or light", param_hint="--theme") native_tui = message is None and not classic if native_tui: - from nanobot.cli.tui_launcher import TuiUnavailableError, launch_tui + from nanobot.cli.tui_launcher import TuiSessionError, TuiUnavailableError, launch_tui from nanobot.config.loader import get_config_path if not sys.stdin.isatty() or not sys.stdout.isatty(): @@ -106,6 +106,8 @@ def agent( session_id=session_id, theme=theme, ) + except TuiSessionError as exc: + raise typer.BadParameter(str(exc), param_hint="--session") from exc except TuiUnavailableError as exc: console.print(f"[red]Native TUI unavailable: {exc}[/red]") console.print("[dim]Use `nanobot agent --classic` only if you want the old prompt.[/dim]") @@ -205,7 +207,7 @@ def agent( return _cli_progress - if message: + if message is not None: # Single message mode — direct call, no bus needed async def run_once() -> None: try: diff --git a/nanobot/cli/tui_launcher.py b/nanobot/cli/tui_launcher.py index 31ca752d0..a70226485 100644 --- a/nanobot/cli/tui_launcher.py +++ b/nanobot/cli/tui_launcher.py @@ -32,6 +32,10 @@ class TuiUnavailableError(RuntimeError): """Raised when the native TypeScript TUI cannot run on this installation.""" +class TuiSessionError(ValueError): + """Raised when a session selector cannot be opened by the native TUI.""" + + @dataclass(frozen=True) class _GatewayHandle: base_url: str @@ -46,6 +50,8 @@ def launch_tui( theme: str, ) -> int: """Run the native TUI against the shared local gateway.""" + state_path = config_path.parent / "tui" / "state.json" + chat_id = _initial_tui_chat_id(session_id, state_path) command = _resolve_tui_command() gateway = _ensure_gateway( config, @@ -72,9 +78,7 @@ def launch_tui( "NANOBOT_TUI_THEME": theme, } ) - state_path = config_path.parent / "tui" / "state.json" env["NANOBOT_TUI_STATE_PATH"] = str(state_path) - chat_id = _initial_tui_chat_id(session_id, state_path) if chat_id: env["NANOBOT_TUI_CHAT_ID"] = chat_id else: @@ -335,7 +339,7 @@ def _websocket_chat_id(session_id: str) -> str | None: if session_id.startswith("websocket:"): return session_id.split(":", 1)[1] or None if ":" in session_id: - raise TuiUnavailableError( + raise TuiSessionError( "the native TUI can open only WebSocket sessions; use --classic to resume " f"{session_id!r}" ) @@ -343,10 +347,10 @@ def _websocket_chat_id(session_id: str) -> str | None: def _initial_tui_chat_id(session_id: str | None, state_path: Path) -> str | None: - """Resume the default TUI, while keeping an explicit selector authoritative.""" + """Resume the last TUI chat, while keeping an explicit selector authoritative.""" if session_id is not None: return _websocket_chat_id(session_id) - return _read_tui_chat_id(state_path) or "tui-direct" + return _read_tui_chat_id(state_path) def _read_tui_chat_id(path: Path) -> str | None: diff --git a/tests/cli/test_tui_launcher.py b/tests/cli/test_tui_launcher.py index 90cb57f95..c40a79355 100644 --- a/tests/cli/test_tui_launcher.py +++ b/tests/cli/test_tui_launcher.py @@ -8,6 +8,7 @@ import typer from nanobot.cli.agent import agent from nanobot.cli.tui_launcher import ( + TuiSessionError, TuiUnavailableError, _authenticated_ws_url, _download_release_tui, @@ -42,7 +43,7 @@ def test_websocket_chat_id(session_id: str, expected: str | None) -> None: def test_native_tui_rejects_a_session_owned_by_another_channel() -> None: - with pytest.raises(TuiUnavailableError, match="only WebSocket sessions"): + with pytest.raises(TuiSessionError, match="only WebSocket sessions"): _websocket_chat_id("telegram:123") @@ -67,7 +68,7 @@ def test_default_tui_resumes_but_explicit_session_wins(tmp_path: Path) -> None: assert _initial_tui_chat_id("websocket:chosen", path) == "chosen" path.unlink() - assert _initial_tui_chat_id(None, path) == "tui-direct" + assert _initial_tui_chat_id(None, path) is None def test_launcher_passes_the_canonical_model_preset_to_the_tui( @@ -114,6 +115,7 @@ def test_launcher_passes_the_canonical_model_preset_to_the_tui( assert result == 0 assert captured["NANOBOT_TUI_MODEL"] == "openai/gpt-5.6" assert captured["NANOBOT_TUI_MODEL_PRESET"] == "Deep Research" + assert "NANOBOT_TUI_CHAT_ID" not in captured def test_explicit_tui_binary_must_exist( @@ -212,6 +214,28 @@ def test_interactive_agent_does_not_silently_fall_back( ] +def test_native_tui_rejects_a_classic_session_selector( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + monkeypatch.setattr("nanobot.cli.agent._load_runtime_config", lambda *_args: Config()) + monkeypatch.setattr("nanobot.config.loader.get_config_path", lambda: tmp_path / "config.json") + monkeypatch.setattr("nanobot.cli.agent.sys.stdin", SimpleNamespace(isatty=lambda: True)) + monkeypatch.setattr("nanobot.cli.agent.sys.stdout", SimpleNamespace(isatty=lambda: True)) + + with pytest.raises(typer.BadParameter, match="only WebSocket sessions"): + agent( + message=None, + session_id="cli:direct", + workspace=None, + config=None, + markdown=True, + logs=False, + classic=False, + theme="auto", + ) + + def test_default_agent_does_not_fall_back_outside_a_terminal( monkeypatch: pytest.MonkeyPatch, ) -> None: