mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
refactor(tui): retire the direct session default
This commit is contained in:
@@ -73,7 +73,7 @@ def agent(
|
|||||||
help="Terminal UI appearance: auto, dark, or light",
|
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.bus.queue import MessageBus
|
||||||
from nanobot.cron.service import CronService
|
from nanobot.cron.service import CronService
|
||||||
from nanobot.providers.factory import make_provider
|
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")
|
raise typer.BadParameter("must be auto, dark, or light", param_hint="--theme")
|
||||||
native_tui = message is None and not classic
|
native_tui = message is None and not classic
|
||||||
if native_tui:
|
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
|
from nanobot.config.loader import get_config_path
|
||||||
|
|
||||||
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
||||||
@@ -106,6 +106,8 @@ def agent(
|
|||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
theme=theme,
|
theme=theme,
|
||||||
)
|
)
|
||||||
|
except TuiSessionError as exc:
|
||||||
|
raise typer.BadParameter(str(exc), param_hint="--session") from exc
|
||||||
except TuiUnavailableError as exc:
|
except TuiUnavailableError as exc:
|
||||||
console.print(f"[red]Native TUI unavailable: {exc}[/red]")
|
console.print(f"[red]Native TUI unavailable: {exc}[/red]")
|
||||||
console.print("[dim]Use `nanobot agent --classic` only if you want the old prompt.[/dim]")
|
console.print("[dim]Use `nanobot agent --classic` only if you want the old prompt.[/dim]")
|
||||||
@@ -205,7 +207,7 @@ def agent(
|
|||||||
|
|
||||||
return _cli_progress
|
return _cli_progress
|
||||||
|
|
||||||
if message:
|
if message is not None:
|
||||||
# Single message mode — direct call, no bus needed
|
# Single message mode — direct call, no bus needed
|
||||||
async def run_once() -> None:
|
async def run_once() -> None:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ class TuiUnavailableError(RuntimeError):
|
|||||||
"""Raised when the native TypeScript TUI cannot run on this installation."""
|
"""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)
|
@dataclass(frozen=True)
|
||||||
class _GatewayHandle:
|
class _GatewayHandle:
|
||||||
base_url: str
|
base_url: str
|
||||||
@@ -46,6 +50,8 @@ def launch_tui(
|
|||||||
theme: str,
|
theme: str,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Run the native TUI against the shared local gateway."""
|
"""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()
|
command = _resolve_tui_command()
|
||||||
gateway = _ensure_gateway(
|
gateway = _ensure_gateway(
|
||||||
config,
|
config,
|
||||||
@@ -72,9 +78,7 @@ def launch_tui(
|
|||||||
"NANOBOT_TUI_THEME": theme,
|
"NANOBOT_TUI_THEME": theme,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
state_path = config_path.parent / "tui" / "state.json"
|
|
||||||
env["NANOBOT_TUI_STATE_PATH"] = str(state_path)
|
env["NANOBOT_TUI_STATE_PATH"] = str(state_path)
|
||||||
chat_id = _initial_tui_chat_id(session_id, state_path)
|
|
||||||
if chat_id:
|
if chat_id:
|
||||||
env["NANOBOT_TUI_CHAT_ID"] = chat_id
|
env["NANOBOT_TUI_CHAT_ID"] = chat_id
|
||||||
else:
|
else:
|
||||||
@@ -335,7 +339,7 @@ def _websocket_chat_id(session_id: str) -> str | None:
|
|||||||
if session_id.startswith("websocket:"):
|
if session_id.startswith("websocket:"):
|
||||||
return session_id.split(":", 1)[1] or None
|
return session_id.split(":", 1)[1] or None
|
||||||
if ":" in session_id:
|
if ":" in session_id:
|
||||||
raise TuiUnavailableError(
|
raise TuiSessionError(
|
||||||
"the native TUI can open only WebSocket sessions; use --classic to resume "
|
"the native TUI can open only WebSocket sessions; use --classic to resume "
|
||||||
f"{session_id!r}"
|
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:
|
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:
|
if session_id is not None:
|
||||||
return _websocket_chat_id(session_id)
|
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:
|
def _read_tui_chat_id(path: Path) -> str | None:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import typer
|
|||||||
|
|
||||||
from nanobot.cli.agent import agent
|
from nanobot.cli.agent import agent
|
||||||
from nanobot.cli.tui_launcher import (
|
from nanobot.cli.tui_launcher import (
|
||||||
|
TuiSessionError,
|
||||||
TuiUnavailableError,
|
TuiUnavailableError,
|
||||||
_authenticated_ws_url,
|
_authenticated_ws_url,
|
||||||
_download_release_tui,
|
_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:
|
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")
|
_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"
|
assert _initial_tui_chat_id("websocket:chosen", path) == "chosen"
|
||||||
|
|
||||||
path.unlink()
|
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(
|
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 result == 0
|
||||||
assert captured["NANOBOT_TUI_MODEL"] == "openai/gpt-5.6"
|
assert captured["NANOBOT_TUI_MODEL"] == "openai/gpt-5.6"
|
||||||
assert captured["NANOBOT_TUI_MODEL_PRESET"] == "Deep Research"
|
assert captured["NANOBOT_TUI_MODEL_PRESET"] == "Deep Research"
|
||||||
|
assert "NANOBOT_TUI_CHAT_ID" not in captured
|
||||||
|
|
||||||
|
|
||||||
def test_explicit_tui_binary_must_exist(
|
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(
|
def test_default_agent_does_not_fall_back_outside_a_terminal(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user