mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(cli): route /sessions and /history through prompt_toolkit-safe printing
Bare print() output is swallowed by patch_stdout while an interactive prompt_toolkit Application owns the terminal, so /sessions and /history rendered nothing. Route those emissions through _cprint (prompt_toolkit's native renderer) when an app is running, and fall back to print otherwise. Fixes #36815
This commit is contained in:
parent
081c91c147
commit
b94397fe76
2 changed files with 77 additions and 23 deletions
66
cli.py
66
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 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
|
||||
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 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
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 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
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 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
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).
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from cli import HermesCLI
|
||||
|
|
@ -38,6 +39,39 @@ class TestCliResumeCommand:
|
|||
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=[
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue