From caeb0a5e6c3ed03f9f76acaac29c51e98b4114b2 Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Fri, 29 May 2026 01:16:16 +0300 Subject: [PATCH] fix(cli): make classic CLI /indicator command branch aware --- cli.py | 46 +++++++++++++++++++++ tests/cli/test_cli_init.py | 9 +++++ tests/cli/test_indicator_command.py | 62 +++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/cli/test_indicator_command.py diff --git a/cli.py b/cli.py index aeffd8bad8a6..4ca5ad50209e 100644 --- a/cli.py +++ b/cli.py @@ -327,6 +327,16 @@ def _parse_service_tier_config(raw: str) -> str | None: logger.warning("Unknown service_tier '%s', ignoring", raw) return None + +_INDICATOR_STYLES: tuple[str, ...] = ("ascii", "emoji", "kaomoji", "unicode") +_INDICATOR_DEFAULT = "kaomoji" + + +def _normalize_indicator_style(value: Any) -> str: + """Normalize the persisted TUI indicator style to a known value.""" + raw = str(value or "").strip().lower() + return raw if raw in _INDICATOR_STYLES else _INDICATOR_DEFAULT + def load_cli_config() -> Dict[str, Any]: """ Load CLI configuration from config files. @@ -8652,6 +8662,8 @@ def process_command(self, command: str) -> bool: self._handle_subgoal_command(cmd_original) elif canonical == "skin": self._handle_skin_command(cmd_original) + elif canonical == "indicator": + self._handle_indicator_command(cmd_original) elif canonical == "voice": self._handle_voice_command(cmd_original) elif canonical == "busy": @@ -9535,6 +9547,40 @@ def _handle_skin_command(self, cmd: str): if self._apply_tui_skin_style(): print(" Prompt + TUI colors updated.") + def _handle_indicator_command(self, cmd: str) -> None: + """Handle /indicator - show or change the TUI busy-indicator style.""" + from hermes_cli.config import load_config + + parts = cmd.strip().split(maxsplit=1) + if len(parts) < 2 or not parts[1].strip(): + cfg = load_config() or {} + current = _normalize_indicator_style( + (cfg.get("display") or {}).get("tui_status_indicator", "") + ) + _cprint(f" {_ACCENT}TUI indicator: {current}{_RST}") + _cprint( + f" {_DIM}Usage: /indicator [kaomoji|emoji|unicode|ascii]{_RST}" + ) + _cprint( + f" {_DIM}Applies to Hermes TUI and dashboard chat, not the classic CLI prompt.{_RST}" + ) + return + + raw = parts[1].strip().lower() + if raw not in _INDICATOR_STYLES: + choices = "|".join(_INDICATOR_STYLES) + _cprint(f" {_DIM}(._.) Unknown indicator: {raw!r}{_RST}") + _cprint(f" {_DIM}Usage: /indicator [{choices}]{_RST}") + return + + if save_config_value("display.tui_status_indicator", raw): + _cprint(f" {_ACCENT}✓ TUI indicator set to '{raw}' (saved to config){_RST}") + else: + _cprint(f" {_ACCENT}✓ TUI indicator set to '{raw}' (session only){_RST}") + _cprint( + f" {_DIM}The change applies to Hermes TUI and dashboard chat sessions.{_RST}" + ) + def _handle_footer_command(self, cmd_original: str) -> None: """Toggle or inspect ``display.runtime_footer.enabled`` from the CLI. diff --git a/tests/cli/test_cli_init.py b/tests/cli/test_cli_init.py index 67004384ae75..7eb83861950d 100644 --- a/tests/cli/test_cli_init.py +++ b/tests/cli/test_cli_init.py @@ -472,6 +472,15 @@ def test_sessions_command_is_dispatched(self): called_with = mock_handler.call_args.args[0] assert called_with.lower().startswith("/sessions") + def test_indicator_command_is_dispatched(self): + """/indicator must hit _handle_indicator_command, not fall through.""" + cli = _make_cli() + + with patch.object(cli, "_handle_indicator_command") as mock_handler: + cli.process_command("/indicator emoji") + + mock_handler.assert_called_once_with("/indicator emoji") + class TestRootLevelProviderOverride: """Root-level provider/base_url in config.yaml must NOT override model.provider.""" diff --git a/tests/cli/test_indicator_command.py b/tests/cli/test_indicator_command.py new file mode 100644 index 000000000000..4893d563dc71 --- /dev/null +++ b/tests/cli/test_indicator_command.py @@ -0,0 +1,62 @@ +"""Tests for the /indicator CLI command.""" + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + + +def _import_cli(): + import hermes_cli.config as config_mod + + if not hasattr(config_mod, "save_env_value_secure"): + config_mod.save_env_value_secure = lambda key, value: { + "success": True, + "stored_as": key, + "validated": False, + } + + import cli as cli_mod + + return cli_mod + + +class TestHandleIndicatorCommand(unittest.TestCase): + def test_no_args_shows_normalized_status(self): + cli_mod = _import_cli() + stub = SimpleNamespace() + with ( + patch.object(cli_mod, "_cprint") as mock_cprint, + patch("hermes_cli.config.load_config", return_value={"display": {"tui_status_indicator": " EMOJI "}}), + patch.object(cli_mod, "save_config_value") as mock_save, + ): + cli_mod.HermesCLI._handle_indicator_command(stub, "/indicator") + + mock_save.assert_not_called() + printed = " ".join(str(call) for call in mock_cprint.call_args_list) + self.assertIn("emoji", printed.lower()) + self.assertIn("dashboard", printed.lower()) + + def test_valid_argument_saves_lowercased_style(self): + cli_mod = _import_cli() + stub = SimpleNamespace() + with ( + patch.object(cli_mod, "_cprint"), + patch.object(cli_mod, "save_config_value", return_value=True) as mock_save, + ): + cli_mod.HermesCLI._handle_indicator_command(stub, "/indicator EMOJI") + + mock_save.assert_called_once_with("display.tui_status_indicator", "emoji") + + def test_invalid_argument_prints_usage_without_saving(self): + cli_mod = _import_cli() + stub = SimpleNamespace() + with ( + patch.object(cli_mod, "_cprint") as mock_cprint, + patch.object(cli_mod, "save_config_value") as mock_save, + ): + cli_mod.HermesCLI._handle_indicator_command(stub, "/indicator sparkle") + + mock_save.assert_not_called() + printed = " ".join(str(call) for call in mock_cprint.call_args_list) + self.assertIn("unknown indicator", printed.lower()) + self.assertIn("ascii|emoji|kaomoji|unicode", printed.lower())