mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(cli): wire /sessions slash command in the classic CLI
The 'sessions' command has been registered in the central command registry since #20805 (May 2025) and surfaces in /help and tab-completion, but the classic CLI's process_command() never had an elif branch for it. The canonical name fell through and printed 'Unknown command: sessions'. The TUI side was wired up correctly via the SessionPicker overlay; only the legacy CLI was missing the dispatch. Adds _handle_sessions_command() which mirrors /resume's no-arg behavior inline (the CLI has no overlay primitive equivalent to the TUI picker): - /sessions and /sessions list → print the recent-sessions table - /sessions <id_or_title> → delegates to _handle_resume_command Includes regression tests covering the dispatcher wiring (the original bug) plus the three handler branches.
This commit is contained in:
parent
09d970160b
commit
d6c488f2dc
2 changed files with 117 additions and 0 deletions
34
cli.py
34
cli.py
|
|
@ -5961,6 +5961,38 @@ class HermesCLI:
|
|||
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.
|
||||
|
||||
|
|
@ -7540,6 +7572,8 @@ class HermesCLI:
|
|||
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 == "codex-runtime":
|
||||
|
|
|
|||
|
|
@ -319,6 +319,89 @@ class TestHistoryDisplay:
|
|||
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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue