fix: improve /history message display
This commit is contained in:
parent
90fa9e54ca
commit
77f47768dd
2 changed files with 287 additions and 12 deletions
65
cli.py
65
cli.py
|
|
@ -1546,24 +1546,65 @@ class HermesCLI:
|
||||||
if not self.conversation_history:
|
if not self.conversation_history:
|
||||||
print("(._.) No conversation history yet.")
|
print("(._.) No conversation history yet.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
preview_limit = 400
|
||||||
|
visible_index = 0
|
||||||
|
hidden_tool_messages = 0
|
||||||
|
|
||||||
|
def flush_tool_summary():
|
||||||
|
nonlocal hidden_tool_messages
|
||||||
|
if not hidden_tool_messages:
|
||||||
|
return
|
||||||
|
|
||||||
|
noun = "message" if hidden_tool_messages == 1 else "messages"
|
||||||
|
print("\n [Tools]")
|
||||||
|
print(f" ({hidden_tool_messages} tool {noun} hidden)")
|
||||||
|
hidden_tool_messages = 0
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("+" + "-" * 50 + "+")
|
print("+" + "-" * 50 + "+")
|
||||||
print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|")
|
print("|" + " " * 12 + "(^_^) Conversation History" + " " * 11 + "|")
|
||||||
print("+" + "-" * 50 + "+")
|
print("+" + "-" * 50 + "+")
|
||||||
|
|
||||||
for i, msg in enumerate(self.conversation_history, 1):
|
for msg in self.conversation_history:
|
||||||
role = msg.get("role", "unknown")
|
role = msg.get("role", "unknown")
|
||||||
content = msg.get("content") or ""
|
|
||||||
|
if role == "tool":
|
||||||
|
hidden_tool_messages += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if role not in {"user", "assistant"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
flush_tool_summary()
|
||||||
|
visible_index += 1
|
||||||
|
|
||||||
|
content = msg.get("content")
|
||||||
|
content_text = "" if content is None else str(content)
|
||||||
|
|
||||||
if role == "user":
|
if role == "user":
|
||||||
print(f"\n [You #{i}]")
|
print(f"\n [You #{visible_index}]")
|
||||||
print(f" {content[:200]}{'...' if len(content) > 200 else ''}")
|
print(
|
||||||
elif role == "assistant":
|
f" {content_text[:preview_limit]}{'...' if len(content_text) > preview_limit else ''}"
|
||||||
print(f"\n [Hermes #{i}]")
|
)
|
||||||
preview = content[:200] if content else "(tool calls)"
|
continue
|
||||||
print(f" {preview}{'...' if len(str(content)) > 200 else ''}")
|
|
||||||
|
print(f"\n [Hermes #{visible_index}]")
|
||||||
|
tool_calls = msg.get("tool_calls") or []
|
||||||
|
if content_text:
|
||||||
|
preview = content_text[:preview_limit]
|
||||||
|
suffix = "..." if len(content_text) > preview_limit else ""
|
||||||
|
elif tool_calls:
|
||||||
|
tool_count = len(tool_calls)
|
||||||
|
noun = "call" if tool_count == 1 else "calls"
|
||||||
|
preview = f"(requested {tool_count} tool {noun})"
|
||||||
|
suffix = ""
|
||||||
|
else:
|
||||||
|
preview = "(no text response)"
|
||||||
|
suffix = ""
|
||||||
|
print(f" {preview}{suffix}")
|
||||||
|
|
||||||
|
flush_tool_summary()
|
||||||
print()
|
print()
|
||||||
|
|
||||||
def reset_conversation(self):
|
def reset_conversation(self):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ that only manifest at runtime (not in mocked unit tests)."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
|
from contextlib import nullcontext
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -10,8 +12,208 @@ import pytest
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
|
||||||
|
def _install_prompt_toolkit_stubs():
|
||||||
|
"""Provide minimal prompt_toolkit shims for non-TUI unit tests."""
|
||||||
|
if "prompt_toolkit" in sys.modules:
|
||||||
|
return
|
||||||
|
|
||||||
|
class _StubBase:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __getattr__(self, _name):
|
||||||
|
return lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
class _StubStyle:
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, *_args, **_kwargs):
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
prompt_toolkit = types.ModuleType("prompt_toolkit")
|
||||||
|
prompt_toolkit.print_formatted_text = lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
history = types.ModuleType("prompt_toolkit.history")
|
||||||
|
history.FileHistory = _StubBase
|
||||||
|
|
||||||
|
styles = types.ModuleType("prompt_toolkit.styles")
|
||||||
|
styles.Style = _StubStyle
|
||||||
|
|
||||||
|
patch_stdout = types.ModuleType("prompt_toolkit.patch_stdout")
|
||||||
|
patch_stdout.patch_stdout = nullcontext
|
||||||
|
|
||||||
|
application = types.ModuleType("prompt_toolkit.application")
|
||||||
|
application.Application = _StubBase
|
||||||
|
|
||||||
|
layout = types.ModuleType("prompt_toolkit.layout")
|
||||||
|
layout.Layout = _StubBase
|
||||||
|
layout.HSplit = _StubBase
|
||||||
|
layout.Window = _StubBase
|
||||||
|
layout.FormattedTextControl = _StubBase
|
||||||
|
layout.ConditionalContainer = _StubBase
|
||||||
|
|
||||||
|
processors = types.ModuleType("prompt_toolkit.layout.processors")
|
||||||
|
processors.Processor = _StubBase
|
||||||
|
processors.Transformation = _StubBase
|
||||||
|
processors.PasswordProcessor = _StubBase
|
||||||
|
processors.ConditionalProcessor = _StubBase
|
||||||
|
|
||||||
|
filters = types.ModuleType("prompt_toolkit.filters")
|
||||||
|
filters.Condition = lambda fn: fn
|
||||||
|
|
||||||
|
dimension = types.ModuleType("prompt_toolkit.layout.dimension")
|
||||||
|
dimension.Dimension = _StubBase
|
||||||
|
|
||||||
|
menus = types.ModuleType("prompt_toolkit.layout.menus")
|
||||||
|
menus.CompletionsMenu = _StubBase
|
||||||
|
|
||||||
|
widgets = types.ModuleType("prompt_toolkit.widgets")
|
||||||
|
widgets.TextArea = _StubBase
|
||||||
|
|
||||||
|
key_binding = types.ModuleType("prompt_toolkit.key_binding")
|
||||||
|
key_binding.KeyBindings = _StubBase
|
||||||
|
|
||||||
|
completion = types.ModuleType("prompt_toolkit.completion")
|
||||||
|
completion.Completer = object
|
||||||
|
completion.Completion = _StubBase
|
||||||
|
|
||||||
|
formatted_text = types.ModuleType("prompt_toolkit.formatted_text")
|
||||||
|
formatted_text.ANSI = str
|
||||||
|
|
||||||
|
sys.modules.update(
|
||||||
|
{
|
||||||
|
"prompt_toolkit": prompt_toolkit,
|
||||||
|
"prompt_toolkit.history": history,
|
||||||
|
"prompt_toolkit.styles": styles,
|
||||||
|
"prompt_toolkit.patch_stdout": patch_stdout,
|
||||||
|
"prompt_toolkit.application": application,
|
||||||
|
"prompt_toolkit.layout": layout,
|
||||||
|
"prompt_toolkit.layout.processors": processors,
|
||||||
|
"prompt_toolkit.filters": filters,
|
||||||
|
"prompt_toolkit.layout.dimension": dimension,
|
||||||
|
"prompt_toolkit.layout.menus": menus,
|
||||||
|
"prompt_toolkit.widgets": widgets,
|
||||||
|
"prompt_toolkit.key_binding": key_binding,
|
||||||
|
"prompt_toolkit.completion": completion,
|
||||||
|
"prompt_toolkit.formatted_text": formatted_text,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_rich_stubs():
|
||||||
|
"""Provide minimal rich shims for CLI unit tests."""
|
||||||
|
if "rich" in sys.modules:
|
||||||
|
return
|
||||||
|
|
||||||
|
rich = types.ModuleType("rich")
|
||||||
|
console = types.ModuleType("rich.console")
|
||||||
|
panel = types.ModuleType("rich.panel")
|
||||||
|
table = types.ModuleType("rich.table")
|
||||||
|
|
||||||
|
class _RichStub:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __getattr__(self, _name):
|
||||||
|
return lambda *args, **kwargs: None
|
||||||
|
|
||||||
|
console.Console = _RichStub
|
||||||
|
panel.Panel = _RichStub
|
||||||
|
table.Table = _RichStub
|
||||||
|
|
||||||
|
sys.modules.update(
|
||||||
|
{
|
||||||
|
"rich": rich,
|
||||||
|
"rich.console": console,
|
||||||
|
"rich.panel": panel,
|
||||||
|
"rich.table": table,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _install_cli_dependency_stubs():
|
||||||
|
"""Stub heavy runtime-only dependencies so CLI unit tests stay lightweight."""
|
||||||
|
if "fire" not in sys.modules:
|
||||||
|
sys.modules["fire"] = types.ModuleType("fire")
|
||||||
|
|
||||||
|
if "run_agent" not in sys.modules:
|
||||||
|
run_agent = types.ModuleType("run_agent")
|
||||||
|
run_agent.AIAgent = object
|
||||||
|
sys.modules["run_agent"] = run_agent
|
||||||
|
|
||||||
|
if "model_tools" not in sys.modules:
|
||||||
|
model_tools = types.ModuleType("model_tools")
|
||||||
|
model_tools.get_tool_definitions = lambda *args, **kwargs: []
|
||||||
|
model_tools.get_toolset_for_tool = lambda *args, **kwargs: None
|
||||||
|
sys.modules["model_tools"] = model_tools
|
||||||
|
|
||||||
|
if "hermes_cli.banner" not in sys.modules:
|
||||||
|
banner = types.ModuleType("hermes_cli.banner")
|
||||||
|
banner.cprint = lambda *args, **kwargs: None
|
||||||
|
banner._GOLD = banner._BOLD = banner._DIM = banner._RST = ""
|
||||||
|
banner.VERSION = "test"
|
||||||
|
banner.HERMES_AGENT_LOGO = ""
|
||||||
|
banner.HERMES_CADUCEUS = ""
|
||||||
|
banner.COMPACT_BANNER = ""
|
||||||
|
banner.get_available_skills = lambda *args, **kwargs: []
|
||||||
|
banner.build_welcome_banner = lambda *args, **kwargs: ""
|
||||||
|
sys.modules.setdefault("hermes_cli", types.ModuleType("hermes_cli"))
|
||||||
|
sys.modules["hermes_cli.banner"] = banner
|
||||||
|
|
||||||
|
if "hermes_cli.commands" not in sys.modules:
|
||||||
|
commands = types.ModuleType("hermes_cli.commands")
|
||||||
|
commands.COMMANDS = {}
|
||||||
|
commands.SlashCommandCompleter = object
|
||||||
|
sys.modules["hermes_cli.commands"] = commands
|
||||||
|
|
||||||
|
if "hermes_cli.callbacks" not in sys.modules:
|
||||||
|
callbacks = types.ModuleType("hermes_cli.callbacks")
|
||||||
|
callbacks.register_approval_callback = lambda *args, **kwargs: None
|
||||||
|
callbacks.register_sudo_password_callback = lambda *args, **kwargs: None
|
||||||
|
sys.modules["hermes_cli.callbacks"] = callbacks
|
||||||
|
sys.modules.setdefault("hermes_cli", types.ModuleType("hermes_cli")).callbacks = callbacks
|
||||||
|
|
||||||
|
if "toolsets" not in sys.modules:
|
||||||
|
toolsets = types.ModuleType("toolsets")
|
||||||
|
toolsets.get_all_toolsets = lambda *args, **kwargs: []
|
||||||
|
toolsets.get_toolset_info = lambda *args, **kwargs: {}
|
||||||
|
toolsets.resolve_toolset = lambda *args, **kwargs: []
|
||||||
|
toolsets.validate_toolset = lambda *_args, **_kwargs: True
|
||||||
|
sys.modules["toolsets"] = toolsets
|
||||||
|
|
||||||
|
if "cron" not in sys.modules:
|
||||||
|
cron = types.ModuleType("cron")
|
||||||
|
cron.create_job = lambda *args, **kwargs: None
|
||||||
|
cron.list_jobs = lambda *args, **kwargs: []
|
||||||
|
cron.remove_job = lambda *args, **kwargs: None
|
||||||
|
cron.get_job = lambda *args, **kwargs: None
|
||||||
|
sys.modules["cron"] = cron
|
||||||
|
|
||||||
|
sys.modules.setdefault("tools", types.ModuleType("tools"))
|
||||||
|
|
||||||
|
if "tools.terminal_tool" not in sys.modules:
|
||||||
|
terminal_tool = types.ModuleType("tools.terminal_tool")
|
||||||
|
terminal_tool.cleanup_all_environments = lambda *args, **kwargs: None
|
||||||
|
terminal_tool.set_sudo_password_callback = lambda *args, **kwargs: None
|
||||||
|
terminal_tool.set_approval_callback = lambda *args, **kwargs: None
|
||||||
|
sys.modules["tools.terminal_tool"] = terminal_tool
|
||||||
|
|
||||||
|
if "tools.browser_tool" not in sys.modules:
|
||||||
|
browser_tool = types.ModuleType("tools.browser_tool")
|
||||||
|
browser_tool._emergency_cleanup_all_sessions = lambda *args, **kwargs: None
|
||||||
|
sys.modules["tools.browser_tool"] = browser_tool
|
||||||
|
|
||||||
|
|
||||||
def _make_cli(env_overrides=None, **kwargs):
|
def _make_cli(env_overrides=None, **kwargs):
|
||||||
"""Create a HermesCLI instance with minimal mocking."""
|
"""Create a HermesCLI instance with minimal mocking."""
|
||||||
|
_install_prompt_toolkit_stubs()
|
||||||
|
_install_rich_stubs()
|
||||||
|
_install_cli_dependency_stubs()
|
||||||
import cli as _cli_mod
|
import cli as _cli_mod
|
||||||
from cli import HermesCLI
|
from cli import HermesCLI
|
||||||
_clean_config = {
|
_clean_config = {
|
||||||
|
|
@ -72,6 +274,38 @@ class TestVerboseAndToolProgress:
|
||||||
assert cli.tool_progress_mode in ("off", "new", "all", "verbose")
|
assert cli.tool_progress_mode in ("off", "new", "all", "verbose")
|
||||||
|
|
||||||
|
|
||||||
|
class TestHistoryDisplay:
|
||||||
|
def test_history_numbers_only_visible_messages_and_summarizes_tools(self, capsys):
|
||||||
|
cli = _make_cli()
|
||||||
|
cli.conversation_history = [
|
||||||
|
{"role": "system", "content": "system prompt"},
|
||||||
|
{"role": "user", "content": "Hello"},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": None,
|
||||||
|
"tool_calls": [{"id": "call_1"}, {"id": "call_2"}],
|
||||||
|
},
|
||||||
|
{"role": "tool", "content": "tool output 1"},
|
||||||
|
{"role": "tool", "content": "tool output 2"},
|
||||||
|
{"role": "assistant", "content": "All set."},
|
||||||
|
{"role": "user", "content": "A" * 250},
|
||||||
|
]
|
||||||
|
|
||||||
|
cli.show_history()
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
|
||||||
|
assert "[You #1]" in output
|
||||||
|
assert "[Hermes #2]" in output
|
||||||
|
assert "(requested 2 tool calls)" in output
|
||||||
|
assert "[Tools]" in output
|
||||||
|
assert "(2 tool messages hidden)" in output
|
||||||
|
assert "[Hermes #3]" in output
|
||||||
|
assert "[You #4]" in output
|
||||||
|
assert "[You #5]" not in output
|
||||||
|
assert "A" * 250 in output
|
||||||
|
assert "A" * 250 + "..." not in output
|
||||||
|
|
||||||
|
|
||||||
class TestProviderResolution:
|
class TestProviderResolution:
|
||||||
def test_api_key_is_string_or_none(self):
|
def test_api_key_is_string_or_none(self):
|
||||||
cli = _make_cli()
|
cli = _make_cli()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue