Skip to content
Closed
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
34 changes: 34 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5917,6 +5917,38 @@ def _handle_resume_command(self, cmd_original: str) -> None:
else:
_cprint(f" ↻ Resumed session {target_id}{title_part} — no messages, starting fresh.")

def _handle_sessions_command(self, cmd_original: str) -> None:
"""Handle /sessions [list|<id_or_title>] — browse or resume previous sessions.

Without arguments, prints the same recent-sessions table that /resume
shows when called without a target, and tells the user how to resume.
With an explicit subcommand or target, delegates to the resume flow so
``/sessions <id>`` and ``/resume <id>`` behave identically.

The TUI ships an interactive picker overlay for this command; the
classic CLI prints an inline list because there is no equivalent
overlay primitive here. Without this handler the canonical name
``sessions`` falls through ``process_command``'s elif chain and
prints ``Unknown command: sessions`` even though the command is
registered in the central COMMAND_REGISTRY.
"""
parts = cmd_original.split(None, 1)
arg = parts[1].strip() if len(parts) > 1 else ""
sub = arg.lower()

# Bare /sessions or /sessions list — show recent sessions inline.
if not arg or sub in {"list", "ls", "browse"}:
if not self._session_db:
from hermes_state import format_session_db_unavailable
_cprint(f" {format_session_db_unavailable()}")
return
if not self._show_recent_sessions(reason="sessions"):
_cprint(" (._.) No previous sessions yet.")
return

# /sessions <id_or_title> behaves the same as /resume <id_or_title>.
self._handle_resume_command(f"/resume {arg}")

def _handle_branch_command(self, cmd_original: str) -> None:
"""Handle /branch [name] — fork the current session into a new independent copy.

Expand Down Expand Up @@ -7452,6 +7484,8 @@ def process_command(self, command: str) -> bool:
self.new_session(title=title)
elif canonical == "resume":
self._handle_resume_command(cmd_original)
elif canonical == "sessions":
self._handle_sessions_command(cmd_original)
elif canonical == "model":
self._handle_model_switch(cmd_original)
elif canonical == "gquota":
Expand Down
83 changes: 83 additions & 0 deletions tests/cli/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,89 @@ def test_resume_without_target_lists_recent_sessions(self, capsys):
assert "Checking Running Hermes Agent" in output
assert "Use /resume <session id or title> to continue" in output

def test_sessions_command_no_args_lists_recent_sessions(self, capsys):
"""/sessions with no args prints the recent-sessions table (TUI parity).

Regression test: `sessions` was registered in the central command
registry and surfaced by /help and tab-completion, but the classic
CLI dispatcher had no elif branch for it, so the canonical name fell
through and printed `Unknown command: sessions`.
"""
cli = _make_cli()
cli.session_id = "current"
cli._session_db = MagicMock()
cli._session_db.list_sessions_rich.return_value = [
{
"id": "20260401_201329_d85961",
"title": "Checking Running Hermes Agent",
"preview": "check running gateways for hermes agent",
"last_active": 0,
},
]

# Drive it through the public dispatcher to also lock in the
# process_command wiring, not just the handler in isolation.
cli.process_command("/sessions")
output = capsys.readouterr().out

assert "Unknown command" not in output
assert "Recent sessions" in output
assert "Checking Running Hermes Agent" in output
assert "20260401_201329_d85961" in output

def test_sessions_list_subcommand_lists_recent_sessions(self, capsys):
"""/sessions list is an explicit alias for the no-arg list view."""
cli = _make_cli()
cli.session_id = "current"
cli._session_db = MagicMock()
cli._session_db.list_sessions_rich.return_value = [
{
"id": "20260401_201329_d85961",
"title": "Checking Running Hermes Agent",
"preview": "check running gateways for hermes agent",
"last_active": 0,
},
]

cli.process_command("/sessions list")
output = capsys.readouterr().out

assert "Unknown command" not in output
assert "Recent sessions" in output
assert "Checking Running Hermes Agent" in output

def test_sessions_with_target_delegates_to_resume(self):
"""/sessions <id_or_title> behaves identically to /resume <id_or_title>.

We intercept `_handle_resume_command` rather than the full resume
machinery (which would otherwise require simulating an entire session
switch). The contract under test is the dispatch wiring.
"""
cli = _make_cli()
with patch.object(cli, "_handle_resume_command") as mock_resume:
cli.process_command("/sessions Checking Running Hermes Agent")

mock_resume.assert_called_once_with(
"/resume Checking Running Hermes Agent"
)

def test_sessions_command_is_dispatched(self):
"""/sessions must hit _handle_sessions_command, not fall through.

Direct test that the process_command elif chain routes the canonical
name to the handler. Without this wiring, /sessions printed
`Unknown command: sessions` even though it was a registered command.
"""
cli = _make_cli()
cli._session_db = None # exercise the no-db path too

with patch.object(cli, "_handle_sessions_command") as mock_handler:
cli.process_command("/sessions")

mock_handler.assert_called_once()
called_with = mock_handler.call_args.args[0]
assert called_with.lower().startswith("/sessions")


class TestRootLevelProviderOverride:
"""Root-level provider/base_url in config.yaml must NOT override model.provider."""
Expand Down