fix(cli): make TUI prompt and accent output skin-aware
Salvaged from PR #932 by Wayne onto current main. Apply skin-aware prompt symbols and live prompt_toolkit color refresh, replace lingering hardcoded accent output with active-skin colors, keep ANSI-safe response rendering, preserve secret-capture and approval-prompt state handling, and add integration coverage for prompt state and style refresh behavior.
This commit is contained in:
@@ -60,6 +60,9 @@ class TestBuiltinSkins:
|
||||
assert skin.name == "ares"
|
||||
assert skin.tool_prefix == "╎"
|
||||
assert skin.get_color("banner_border") == "#9F1C1C"
|
||||
assert skin.get_color("response_border") == "#C7A96B"
|
||||
assert skin.get_color("session_label") == "#C7A96B"
|
||||
assert skin.get_color("session_border") == "#6E584B"
|
||||
assert skin.get_branding("agent_name") == "Ares Agent"
|
||||
|
||||
def test_ares_has_spinner_customization(self):
|
||||
@@ -230,3 +233,82 @@ class TestDisplayIntegration:
|
||||
from agent.display import get_cute_tool_message
|
||||
msg = get_cute_tool_message("terminal", {"command": "ls"}, 0.5)
|
||||
assert msg.startswith("┊")
|
||||
|
||||
|
||||
class TestCliBrandingHelpers:
|
||||
def test_active_prompt_symbol_default(self):
|
||||
from hermes_cli.skin_engine import get_active_prompt_symbol
|
||||
|
||||
assert get_active_prompt_symbol() == "❯ "
|
||||
|
||||
def test_active_prompt_symbol_ares(self):
|
||||
from hermes_cli.skin_engine import set_active_skin, get_active_prompt_symbol
|
||||
|
||||
set_active_skin("ares")
|
||||
assert get_active_prompt_symbol() == "⚔ ❯ "
|
||||
|
||||
def test_active_help_header_ares(self):
|
||||
from hermes_cli.skin_engine import set_active_skin, get_active_help_header
|
||||
|
||||
set_active_skin("ares")
|
||||
assert get_active_help_header() == "(⚔) Available Commands"
|
||||
|
||||
def test_active_goodbye_ares(self):
|
||||
from hermes_cli.skin_engine import set_active_skin, get_active_goodbye
|
||||
|
||||
set_active_skin("ares")
|
||||
assert get_active_goodbye() == "Farewell, warrior! ⚔"
|
||||
|
||||
def test_prompt_toolkit_style_overrides_cover_tui_classes(self):
|
||||
from hermes_cli.skin_engine import set_active_skin, get_prompt_toolkit_style_overrides
|
||||
|
||||
set_active_skin("ares")
|
||||
overrides = get_prompt_toolkit_style_overrides()
|
||||
required = {
|
||||
"input-area",
|
||||
"placeholder",
|
||||
"prompt",
|
||||
"prompt-working",
|
||||
"hint",
|
||||
"input-rule",
|
||||
"image-badge",
|
||||
"completion-menu",
|
||||
"completion-menu.completion",
|
||||
"completion-menu.completion.current",
|
||||
"completion-menu.meta.completion",
|
||||
"completion-menu.meta.completion.current",
|
||||
"clarify-border",
|
||||
"clarify-title",
|
||||
"clarify-question",
|
||||
"clarify-choice",
|
||||
"clarify-selected",
|
||||
"clarify-active-other",
|
||||
"clarify-countdown",
|
||||
"sudo-prompt",
|
||||
"sudo-border",
|
||||
"sudo-title",
|
||||
"sudo-text",
|
||||
"approval-border",
|
||||
"approval-title",
|
||||
"approval-desc",
|
||||
"approval-cmd",
|
||||
"approval-choice",
|
||||
"approval-selected",
|
||||
}
|
||||
assert required.issubset(overrides.keys())
|
||||
|
||||
def test_prompt_toolkit_style_overrides_use_skin_colors(self):
|
||||
from hermes_cli.skin_engine import (
|
||||
set_active_skin,
|
||||
get_active_skin,
|
||||
get_prompt_toolkit_style_overrides,
|
||||
)
|
||||
|
||||
set_active_skin("ares")
|
||||
skin = get_active_skin()
|
||||
overrides = get_prompt_toolkit_style_overrides()
|
||||
assert overrides["prompt"] == skin.get_color("prompt")
|
||||
assert overrides["input-rule"] == skin.get_color("input_rule")
|
||||
assert overrides["clarify-title"] == f"{skin.get_color('banner_title')} bold"
|
||||
assert overrides["sudo-prompt"] == f"{skin.get_color('ui_error')} bold"
|
||||
assert overrides["approval-title"] == f"{skin.get_color('ui_warn')} bold"
|
||||
|
||||
95
tests/test_cli_skin_integration.py
Normal file
95
tests/test_cli_skin_integration.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from cli import HermesCLI, _rich_text_from_ansi
|
||||
from hermes_cli.skin_engine import get_active_skin, set_active_skin
|
||||
|
||||
|
||||
def _make_cli_stub():
|
||||
cli = HermesCLI.__new__(HermesCLI)
|
||||
cli._sudo_state = None
|
||||
cli._secret_state = None
|
||||
cli._approval_state = None
|
||||
cli._clarify_state = None
|
||||
cli._clarify_freetext = False
|
||||
cli._command_running = False
|
||||
cli._agent_running = False
|
||||
cli._command_spinner_frame = lambda: "⟳"
|
||||
cli._tui_style_base = {
|
||||
"prompt": "#fff",
|
||||
"input-area": "#fff",
|
||||
"input-rule": "#aaa",
|
||||
"prompt-working": "#888 italic",
|
||||
}
|
||||
cli._app = SimpleNamespace(style=None)
|
||||
cli._invalidate = MagicMock()
|
||||
return cli
|
||||
|
||||
|
||||
class TestCliSkinPromptIntegration:
|
||||
def test_default_prompt_fragments_use_default_symbol(self):
|
||||
cli = _make_cli_stub()
|
||||
|
||||
set_active_skin("default")
|
||||
assert cli._get_tui_prompt_fragments() == [("class:prompt", "❯ ")]
|
||||
|
||||
def test_ares_prompt_fragments_use_skin_symbol(self):
|
||||
cli = _make_cli_stub()
|
||||
|
||||
set_active_skin("ares")
|
||||
assert cli._get_tui_prompt_fragments() == [("class:prompt", "⚔ ❯ ")]
|
||||
|
||||
def test_secret_prompt_fragments_preserve_secret_state(self):
|
||||
cli = _make_cli_stub()
|
||||
cli._secret_state = {"response_queue": object()}
|
||||
|
||||
set_active_skin("ares")
|
||||
assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "🔑 ❯ ")]
|
||||
|
||||
def test_icon_only_skin_symbol_still_visible_in_special_states(self):
|
||||
cli = _make_cli_stub()
|
||||
cli._secret_state = {"response_queue": object()}
|
||||
|
||||
with patch("hermes_cli.skin_engine.get_active_prompt_symbol", return_value="⚔ "):
|
||||
assert cli._get_tui_prompt_fragments() == [("class:sudo-prompt", "🔑 ⚔ ")]
|
||||
|
||||
def test_build_tui_style_dict_uses_skin_overrides(self):
|
||||
cli = _make_cli_stub()
|
||||
|
||||
set_active_skin("ares")
|
||||
skin = get_active_skin()
|
||||
style_dict = cli._build_tui_style_dict()
|
||||
|
||||
assert style_dict["prompt"] == skin.get_color("prompt")
|
||||
assert style_dict["input-rule"] == skin.get_color("input_rule")
|
||||
assert style_dict["prompt-working"] == f"{skin.get_color('banner_dim')} italic"
|
||||
assert style_dict["approval-title"] == f"{skin.get_color('ui_warn')} bold"
|
||||
|
||||
def test_apply_tui_skin_style_updates_running_app(self):
|
||||
cli = _make_cli_stub()
|
||||
|
||||
set_active_skin("ares")
|
||||
assert cli._apply_tui_skin_style() is True
|
||||
assert cli._app.style is not None
|
||||
cli._invalidate.assert_called_once_with(min_interval=0.0)
|
||||
|
||||
def test_handle_skin_command_refreshes_live_tui(self, capsys):
|
||||
cli = _make_cli_stub()
|
||||
|
||||
with patch("cli.save_config_value", return_value=True):
|
||||
cli._handle_skin_command("/skin ares")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Skin set to: ares (saved)" in output
|
||||
assert "Prompt + TUI colors updated." in output
|
||||
assert cli._app.style is not None
|
||||
|
||||
|
||||
class TestAnsiRichTextHelper:
|
||||
def test_preserves_literal_brackets(self):
|
||||
text = _rich_text_from_ansi("[notatag] literal")
|
||||
assert text.plain == "[notatag] literal"
|
||||
|
||||
def test_strips_ansi_but_keeps_plain_text(self):
|
||||
text = _rich_text_from_ansi("\x1b[31mred\x1b[0m")
|
||||
assert text.plain == "red"
|
||||
Reference in New Issue
Block a user