diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 917e8b1e02..a679817b38 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -104,7 +104,7 @@ COMMAND_REGISTRY: list[CommandDef] = [ args_hint="[level|show|hide]", subcommands=("none", "low", "minimal", "medium", "high", "xhigh", "show", "hide", "on", "off")), CommandDef("skin", "Show or change the display skin/theme", "Configuration", - cli_only=True, args_hint="[name]"), + args_hint="[name]"), CommandDef("voice", "Toggle voice mode", "Configuration", args_hint="[on|off|tts|status]", subcommands=("on", "off", "tts", "status")), @@ -861,6 +861,23 @@ class SlashCommandCompleter(Completer): ) count += 1 + @staticmethod + def _skin_completions(sub_text: str, sub_lower: str): + """Yield completions for /skin from available skins.""" + try: + from hermes_cli.skin_engine import list_skins + for s in list_skins(): + name = s["name"] + if name.startswith(sub_lower) and name != sub_lower: + yield Completion( + name, + start_position=-len(sub_text), + display=name, + display_meta=s.get("description", "") or s.get("source", ""), + ) + except Exception: + pass + def _model_completions(self, sub_text: str, sub_lower: str): """Yield completions for /model from config aliases + built-in aliases.""" seen = set() @@ -915,10 +932,14 @@ class SlashCommandCompleter(Completer): sub_text = parts[1] if len(parts) > 1 else "" sub_lower = sub_text.lower() - # Dynamic model alias completions for /model - if " " not in sub_text and base_cmd == "/model": - yield from self._model_completions(sub_text, sub_lower) - return + # Dynamic completions for commands with runtime lists + if " " not in sub_text: + if base_cmd == "/model": + yield from self._model_completions(sub_text, sub_lower) + return + if base_cmd == "/skin": + yield from self._skin_completions(sub_text, sub_lower) + return # Static subcommand completions if " " not in sub_text and base_cmd in SUBCOMMANDS: diff --git a/run_agent.py b/run_agent.py index d7234f2964..499843585c 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5722,7 +5722,7 @@ class AIAgent: # (gateway, batch, quiet) still get reasoning. # Any reasoning that wasn't shown during streaming is caught by the # CLI post-response display fallback (cli.py _reasoning_shown_this_turn). - if not self.stream_delta_callback: + if not self.stream_delta_callback and not self._stream_callback: try: self.reasoning_callback(reasoning_text) except Exception: diff --git a/tui_gateway/server.py b/tui_gateway/server.py index f0b80ad502..023da60b1a 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -121,7 +121,7 @@ def write_json(obj: dict) -> bool: def _emit(event: str, sid: str, payload: dict | None = None): params = {"type": event, "session_id": sid} - if payload: + if payload is not None: params["payload"] = payload write_json({"jsonrpc": "2.0", "method": "event", "params": params}) @@ -842,6 +842,8 @@ def _(rid, params: dict) -> dict: cfg.setdefault("display", {})[key] = value nv = value _save_cfg(cfg) + if key == "skin": + _emit("skin.changed", "", resolve_skin()) return _ok(rid, {"key": key, "value": nv}) except Exception as e: return _err(rid, 5001, str(e)) @@ -868,6 +870,8 @@ def _(rid, params: dict) -> dict: return _ok(rid, {"config": _load_cfg()}) if key == "prompt": return _ok(rid, {"prompt": _load_cfg().get("custom_prompt", "")}) + if key == "skin": + return _ok(rid, {"value": _load_cfg().get("display", {}).get("skin", "default")}) return _err(rid, 4002, f"unknown config key: {key}") diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx index 88dcf84e64..255a62d0ef 100644 --- a/ui-tui/src/app.tsx +++ b/ui-tui/src/app.tsx @@ -840,7 +840,7 @@ export function App({ gw }: { gw: GatewayClient }) { return } - if (completions.length && input && (key.upArrow || key.downArrow)) { + if (completions.length && input && historyIdx === null && (key.upArrow || key.downArrow)) { setCompIdx(i => (key.upArrow ? (i - 1 + completions.length) % completions.length : (i + 1) % completions.length)) return @@ -1004,6 +1004,13 @@ export function App({ gw }: { gw: GatewayClient }) { break + case 'skin.changed': + if (p) { + setTheme(fromSkin(p.colors ?? {}, p.branding ?? {}, p.banner_logo ?? '', p.banner_hero ?? '')) + } + + break + case 'session.info': setInfo(p as SessionInfo) @@ -1520,6 +1527,15 @@ export function App({ gw }: { gw: GatewayClient }) { return true + case 'skin': + if (arg) { + rpc('config.set', { key: 'skin', value: arg }).then((r: any) => sys(`skin → ${r.value}`)) + } else { + rpc('config.get', { key: 'skin' }).then((r: any) => sys(`skin: ${r.value || 'default'}`)) + } + + return true + case 'yolo': rpc('config.set', { key: 'yolo' }).then((r: any) => sys(`yolo ${r.value === '1' ? 'on' : 'off'}`))