'/sessions' slash command wiring and visibility issues
Cluster 21677 · gateway. Data as of
1 open item. Fix merged in #56168 on 1 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 32 related items grouped here, 12 issues already closed.
- Fix event
- A fix was merged to main in PR #56168. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #56168. That is separate from the fix event. - Still open
- 1 issue and 0 PRs.
Next step: read #56168, then check whether your report is the one still open.
Read #56168Pick up a Lab taskThis cluster covers the addition and repeated fixing of the /sessions (and related /indicator) slash commands across CLI, TUI, and gateway contexts, including missing handlers, silent failures, unknown-command errors, and no visible output.
Open issues1
- Issue#22960/sessions and /indicator slash commands silently fail in CLI mode
Open PRs0
Fixes on record
-
#56168 fix(cli,doctor): visible /sessions & /history + doctor toolset filter (salvage #36897)
files:
cli.py,hermes_cli/doctor.py,scripts/release.py,tests/cli/test_cli_resume_command.py,tests/hermes_cli/test_doctor.pyShow the diff
-
cli.py@@ -2510,6 +2510,26 @@ def _prepend_note_to_message(message, note: str): return message +def _cli_visible_print(text: str = "") -> None: + """Print normally unless prompt_toolkit owns the live terminal. + + Bare ``print()`` output is swallowed by ``patch_stdout`` while an + interactive ``Application`` is running, so ``/sessions`` and ``/history`` + would render nothing. Route through ``_cprint`` (prompt_toolkit-native) + in that case, and fall back to ``print`` otherwise. + """ + try: + from prompt_toolkit.application import get_app_or_none + app = get_app_or_none() + except Exception: + app = None + + if app is not None and getattr(app, "_is_running", False): + _cprint(text) + else: + print(text) + + # --------------------------------------------------------------------------- # File-drop / local attachment detection — extracted as pure helpers for tests. # --------------------------------------------------------------------------- @@ -6549,30 +6569,30 @@ def _show_recent_sessions(self, *, reason: str = "history", limit: int = 10) -> from hermes_cli.main import _relative_time - print() + _cli_visible_print() if reason == "history": - print("(._.) No messages in the current chat yet — here are recent sessions you can resume:") + _cli_visible_print("(._.) No messages in the current chat yet — here are recent sessions you can resume:") else: - print(" Recent sessions:") - print() - print(f" {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}") - print(f" {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}") + _cli_visible_print(" Recent sessions:") + _cli_visible_print() + _cli_visible_print(f" {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}") + _cli_visible_print(f" {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}") for idx, session in enumerate(sessions, start=1): title = session.get("title") or "—" preview = (session.get("preview") or "")[:38] last_active = _relative_time(session.get("last_active")) - print(f" {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}") - print() - print(" Use /resume <number>, /resume <session id>, or /resume <session title> to continue.") - print(" Example: /resume 2") - print() + _cli_visible_print(f" {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}") + _cli_visible_print() + _cli_visible_print(" Use /resume <number>, /resume <session id>, or /resume <session title> to continue.") + _cli_visible_print(" Example: /resume 2") + _cli_visible_print() return True def show_history(self): """Display conversation history.""" if not self.conversation_history: if not self._show_recent_sessions(reason="history"): - print("(._.) No conversation history yet.") + _cli_visible_print("(._.) No conversation history yet.") return preview_limit = 400 @@ -6601,14 +6621,14 @@ def flush_tool_summary(): return noun = "message" if hidden_tool_messages == 1 else "messages" - print("\n [Tools]") - print(f" ({hidden_tool_messages} tool {noun} hidden)") + _cli_visible_print("\n [Tools]") + _cli_visible_print(f" ({hidden_tool_messages} tool {noun} hidden)") hidden_tool_messages = 0 - print() - print("+" + "-" * 50 + "+") - print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|") - print("+" + "-" * 50 + "+") + _cli_visible_print() + _cli_visible_print("+" + "-" * 50 + "+") + _cli_visible_print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|") + _cli_visible_print("+" + "-" * 50 + "+") for msg in self.conversation_history: role = msg.get("role", "unknown") @@ -6627,13 +6647,13 @@ def flush_tool_summary(): content_text = "" if content is None else str(content) if role == "user": - print(f"\n [You #{visible_index}]{_ts_suffix(msg)}") - print( + _cli_visible_print(f"\n [You #{visible_index}]{_ts_suffix(msg)}") + _cli_visible_print( f" {content_text[:preview_limit]}{'...' if len(content_text) > preview_limit else ''}" ) continue - print(f"\n [Hermes #{visible_index}]{_ts_suffix(msg)}") + _cli_visible_print(f"\n [Hermes #{visible_index}]{_ts_suffix(msg)}") tool_calls = msg.get("tool_calls") or [] if content_text: preview = content_text[:preview_limit] @@ -6646,10 +6666,10 @@ def flush_tool_summary(): else: preview = "(no text response)" suffix = "" - print(f" {preview}{suffix}") + _cli_visible_print(f" {preview}{suffix}") flush_tool_summary() - print() + _cli_visible_print() def _notify_session_boundary(self, event_type: str) -> None: """Fire a session-boundary plugin hook (on_session_finalize or on_session_reset). -
hermes_cli/doctor.py@@ -199,6 +199,32 @@ def _fail_and_issue(text: str, detail: str, fix: str, issues: list[str]) -> None issues.append(fix) +def _enabled_cli_toolsets_for_doctor() -> set[str] | None: + """Return toolsets enabled for the CLI, or None if config resolution fails.""" + try: + from hermes_cli.config import load_config + from hermes_cli.tools_config import _get_platform_tools + + return {str(toolset) for toolset in _get_platform_tools(load_config() or {}, "cli")} + except Exception: + return None + + +def _missing_api_key_toolsets_for_summary(unavailable: list[dict]) -> list[dict]: + """Filter unavailable API-key toolsets to those enabled for the CLI.""" + api_key_unavailable = [ + item for item in unavailable + if item.get("missing_vars") or item.get("env_vars") + ] + enabled_toolsets = _enabled_cli_toolsets_for_doctor() + if enabled_toolsets is None: + return api_key_unavailable + return [ + item for item in api_key_unavailable + if str(item.get("name") or "") in enabled_toolsets + ] + + def _read_pyproject_version() -> str | None: """Read the ``version = "..."`` from ``pyproject.toml`` at the project root. @@ -2161,8 +2187,10 @@ def _probe_azure_entra() -> _ConnectivityResult: else: check_warn(item["name"], "(system dependency not met)") - # Count disabled tools with API key requirements - api_disabled = [u for u in unavailable if (u.get("missing_vars") or u.get("env_vars"))] + # Count missing API-key requirements only for toolsets enabled in the + # current CLI platform. Default-off or explicitly disabled toolsets may + # still show warnings above, but should not pollute the final summary. + api_disabled = _missing_api_key_toolsets_for_summary(unavailable) if api_disabled: issues.append("Run 'hermes setup' to configure missing API keys for full tool access") except Exception as e: -
scripts/release.py@@ -47,6 +47,7 @@ AUTHOR_MAP = { "[redacted:email]": "jnibarger01", # PR #35130 salvage (ReDoS-bound threat-pattern filler + FTS5 query cap + V4A Move-File approval/traversal targets) "[email protected]": "petrichor-op", # PR #41281 salvage (never persist ephemeral empty-response recovery scaffolding to the SQLite session store / JSON log; filter by flag not position) + "[email protected]": "redactdeveloper", # PR #36897 salvage (route /sessions & /history through prompt_toolkit-safe print; filter doctor missing-key summary to CLI-enabled toolsets) "[redacted:email]": "syahidfrd", # PR #17059 salvage (tag unverified senders in Slack thread context to mitigate indirect prompt injection) "[email protected]": "H2KFORGIVEN", # PR #22523 salvage (turn-pair preservation: never orphan the last user ask at head_end during compaction) "[email protected]": "sgabel", # PR #13139 salvage (redact secrets in user-facing approval prompts) -
tests/cli/test_cli_resume_command.py@@ -1,3 +1,4 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, patch from cli import HermesCLI @@ -38,6 +39,39 @@ def test_show_recent_sessions_includes_indexes_and_resume_hint(self, capsys): assert "/resume 2" in output assert "/resume <session title>" in output + def test_show_recent_sessions_uses_prompt_toolkit_safe_print(self): + cli_obj = _make_cli() + cli_obj._list_recent_sessions = MagicMock(return_value=[ + {"id": "sess_002", "title": "Coding", "preview": "build feature", "last_active": None}, + ]) + + running_app = SimpleNamespace(_is_running=True) + with ( + patch("prompt_toolkit.application.get_app_or_none", return_value=running_app), + patch("cli._cprint") as mock_cprint, + ): + shown = cli_obj._show_recent_sessions(reason="sessions") + + assert shown is True + printed = "\n".join(call.args[0] for call in mock_cprint.call_args_list) + assert "Recent sessions" in printed + assert "Coding" in printed + + def test_show_history_uses_prompt_toolkit_safe_print(self): + cli_obj = _make_cli() + cli_obj.conversation_history = [{"role": "user", "content": "Hello"}] + + running_app = SimpleNamespace(_is_running=True) + with ( + patch("prompt_toolkit.application.get_app_or_none", return_value=running_app), + patch("cli._cprint") as mock_cprint, + ): + cli_obj.show_history() + + printed = "\n".join(call.args[0] for call in mock_cprint.call_args_list) + assert "Conversation History" in printed + assert "Hello" in printed + def test_handle_resume_by_index_switches_to_numbered_session(self): cli_obj = _make_cli() cli_obj._list_recent_sessions = MagicMock(return_value=[ -
tests/hermes_cli/test_doctor.py@@ -51,6 +51,30 @@ def test_returns_false_when_no_provider_settings(self): assert not _has_provider_env_config(content) +class TestDoctorToolAvailabilitySummary: + def test_missing_api_key_summary_ignores_disabled_toolsets(self, monkeypatch): + unavailable = [ + {"name": "rl", "missing_vars": ["TINKER_API_KEY"]}, + {"name": "web", "missing_vars": ["EXA_API_KEY"]}, + ] + monkeypatch.setattr(doctor, "_enabled_cli_toolsets_for_doctor", lambda: {"web"}) + + filtered = doctor._missing_api_key_toolsets_for_summary(unavailable) + + assert [item["name"] for item in filtered] == ["web"] + + def test_missing_api_key_summary_falls_back_when_config_unavailable(self, monkeypatch): + unavailable = [ + {"name": "rl", "missing_vars": ["TINKER_API_KEY"]}, + {"name": "web", "missing_vars": ["EXA_API_KEY"]}, + ] + monkeypatch.setattr(doctor, "_enabled_cli_toolsets_for_doctor", lambda: None) + + filtered = doctor._missing_api_key_toolsets_for_summary(unavailable) + + assert [item["name"] for item in filtered] == ["rl", "web"] + + class TestDoctorEnvFileEncoding: """Regression for #18637 (bug 3): `hermes doctor` crashed on Windows Chinese locale (GBK) because `.env` was read with Path.read_text() which
-
18 PRs closed without merging
From the close comments: done elsewhere 8 · duplicate 4 · superseded by another PR 2 · merged via another change 1 · stale or abandoned 1 · other 2
- #21742 fix(gateway): handle sessions command · stale or abandoned
-
#21748 fix(gateway): handle sessions slash command · done elsewhere →
3e7e9b2 -
#22962 fix(cli): wire up /sessions and /indicator slash commands · other →
d6c488f,#25986,#25231,#21680,#22962,#23289,#24412,#21677,#22951 -
#23077 fix(cli): /sessions shows session list in classic REPL mode (#22951) · duplicate →
#22965 -
#23263 fix(gateway): add sessions command dispatch · done elsewhere →
3e7e9b2
Show 13 more
-
#23289 fix(cli): wire sessions and indicator slash commands · duplicate →
#22962,#22960 -
#23577 fix(cli): mark sessions command as cli_only · superseded by another PR →
d6c488f,3e7e9b2 -
#23680 fix(commands): make /sessions CLI-only and functional · other →
#23577,#23533 -
#23745 fix(cli): wire /sessions slash command to recent-session list · duplicate →
#21680,#21748,#22962,#23289,#21677,#21734,#22951,#22960 -
#26047 fix(cli): wire /whoami slash command in classic CLI · merged via another change →
#90381 -
#27627 fix(cli): wire /indicator slash command to a handler · duplicate →
#27603,#22960,#22962,#23006,#22965,#23289 -
#30379 fix(cli,gateway): handle sessions slash command · done elsewhere →
3e7e9b2,#30353 -
#35879 fix(gateway): route /sessions through the resume flow · done elsewhere →
3e7e9b2,#21734 -
#36851 fix(cli): /sessions and /history show no visible output (#36815) · done elsewhere →
b94397f,#36815 -
#36897 Fix cron, CLI, doctor, and Termux regressions · done elsewhere →
b94397f,6b21a93,#56168,#36815,#11336,#36867,#36771,#29603,#36287 -
#41248 fix(gateway): handle /sessions natively · done elsewhere →
3e7e9b2 -
#41261 fix(gateway): handle /sessions natively instead of falling through to LLM · done elsewhere →
19d4174 - #43637 feat(gateway): add /sessions slash command for messaging platforms · superseded by another PR
Closed issues · 12
- Closed#21677Dashboard/TUI slash path reports Unknown command for /sessions
- Closed#21734/sessions command on gateway platforms is recognized but has no handler — returns unknown command or falls through as plain text
- Closed#22948fix(cli, tui): /sessions unknown command in classic CLI + chmod breaks TUI build on Windows
- Closed#22951fix(cli): /sessions unknown command in classic REPL mode
- Closed#23533Bug: /sessions command is registered but has no handler — silent no-op in gateway and CLI
- Closed#23741/sessions slash command registered but no dispatcher → 'Unknown command'
- Closed#27603Bug: /indicator slash command registered but has no handler implementation
- Closed#30348fix(gateway): /sessions slash command not handled in gateway dispatch
- Closed#30353fix(cli/gateway): /sessions slash command not working on CLI or gateway
- Closed#36815/sessions and /history show no visible output in CLI
- Closed#41238Gateway /sessions command falls through to LLM instead of using a native session-list handler
- Closed#51009[Bug] /whoami and other registered slash commands return unknown command in Desktop app and TUI
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.05: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #56168
- Closed issues
- 12
- Attempts
- 18 PRs closed without merging
- Fix link checked
- 1 Jul 2026