Skip to content
Closed
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
40 changes: 40 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8028,6 +8028,8 @@ def process_command(self, command: str) -> bool:
self._handle_skin_command(cmd_original)
elif canonical == "voice":
self._handle_voice_command(cmd_original)
elif canonical == "indicator":
self._handle_indicator_command(cmd_original)
elif canonical == "busy":
self._handle_busy_command(cmd_original)
else:
Expand Down Expand Up @@ -9016,6 +9018,44 @@ def _handle_reasoning_command(self, cmd: str):
else:
_cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}")

def _handle_indicator_command(self, cmd: str):
"""Handle /indicator — pick the TUI busy-indicator style.

Usage:
/indicator Show current indicator style
/indicator status Show current indicator style
/indicator kaomoji Use animated kaomoji faces (default)
/indicator emoji Use emoji indicator
/indicator unicode Use unicode (braille) spinner
/indicator ascii Use plain ASCII indicator
"""
valid = {"kaomoji", "emoji", "unicode", "ascii"}
try:
cfg = load_cli_config()
current = ((cfg.get("display") if isinstance(cfg, dict) else None) or {}).get(
"tui_status_indicator", "kaomoji"
) or "kaomoji"
except Exception:
current = "kaomoji"

parts = cmd.strip().split(maxsplit=1)
if len(parts) < 2 or parts[1].strip().lower() == "status":
_cprint(f" {_ACCENT}Busy indicator style: {current}{_RST}")
_cprint(f" {_DIM}Usage: /indicator [kaomoji|emoji|unicode|ascii|status]{_RST}")
return

arg = parts[1].strip().lower()
if arg not in valid:
_cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}")
_cprint(f" {_DIM}Usage: /indicator [kaomoji|emoji|unicode|ascii|status]{_RST}")
return

if save_config_value("display.tui_status_indicator", arg):
_cprint(f" {_ACCENT}✓ Busy indicator style set to '{arg}' (saved to config){_RST}")
_cprint(f" {_DIM}Takes effect on next CLI/TUI start.{_RST}")
else:
_cprint(f" {_ACCENT}✓ Busy indicator style set to '{arg}' (session only){_RST}")

def _handle_busy_command(self, cmd: str):
"""Handle /busy — control what Enter does while Hermes is working.

Expand Down
79 changes: 79 additions & 0 deletions tests/hermes_cli/test_indicator_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Regression test for issue #27603: /indicator slash command had no handler.

The command is defined in COMMAND_REGISTRY but `HermesCLI.process_command()`
did not dispatch it, causing "Unknown command: /indicator <style>".
"""
from __future__ import annotations

import importlib
import sys
import types

import pytest


def test_indicator_command_registered():
from hermes_cli.commands import COMMAND_REGISTRY, resolve_command

names = [c.name for c in COMMAND_REGISTRY]
assert "indicator" in names

cdef = resolve_command("indicator")
assert cdef is not None and cdef.name == "indicator"


def test_indicator_handler_exists_and_dispatched():
"""The HermesCLI class must expose _handle_indicator_command, and
process_command() must dispatch /indicator to it."""
import cli as cli_mod

assert hasattr(cli_mod.HermesCLI, "_handle_indicator_command"), (
"HermesCLI is missing _handle_indicator_command — /indicator will fall "
"through to 'Unknown command' (issue #27603)."
)

# Source-level check that the dispatch is wired in process_command.
src = cli_mod.__file__
with open(src, "r") as f:
text = f.read()
assert 'canonical == "indicator"' in text, (
"process_command() is missing the `elif canonical == \"indicator\"` branch."
)


def test_indicator_handler_saves_config_value(monkeypatch):
"""Calling _handle_indicator_command with a valid style should call
save_config_value('display.tui_status_indicator', <style>)."""
import cli as cli_mod

calls: list[tuple] = []

def fake_save(key, value):
calls.append((key, value))
return True

monkeypatch.setattr(cli_mod, "save_config_value", fake_save)
monkeypatch.setattr(cli_mod, "load_cli_config", lambda: {"display": {}})

# Build a minimal stand-in for `self`; the handler only uses module-level
# helpers, so an empty object is sufficient.
fake_self = types.SimpleNamespace()
cli_mod.HermesCLI._handle_indicator_command(fake_self, "/indicator unicode")

assert ("display.tui_status_indicator", "unicode") in calls


def test_indicator_handler_rejects_unknown_style(monkeypatch):
import cli as cli_mod

calls: list[tuple] = []
monkeypatch.setattr(
cli_mod, "save_config_value",
lambda k, v: (calls.append((k, v)) or True),
)
monkeypatch.setattr(cli_mod, "load_cli_config", lambda: {"display": {}})

fake_self = types.SimpleNamespace()
cli_mod.HermesCLI._handle_indicator_command(fake_self, "/indicator bogus")
# Must not have saved anything for an unknown style.
assert calls == []
Loading