Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -9535,6 +9547,40 @@ def _handle_skin_command(self, cmd: str):
if self._apply_tui_skin_style():
print(" Prompt + TUI colors updated.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current main extracted _handle_*_command methods into hermes_cli/cli_commands_mixin.py (_handle_skin_command is now there at line 2250). Please move this handler and its local validation support into that mixin; keep only the dispatch branch in cli.py.


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.

Expand Down
9 changes: 9 additions & 0 deletions tests/cli/test_cli_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
62 changes: 62 additions & 0 deletions tests/cli/test_indicator_command.py
Original file line number Diff line number Diff line change
@@ -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())
Loading