feat(tui): pluggable busy-indicator styles (#13610)#17150
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a user-configurable busy-indicator style for the TUI status bar to reduce layout jitter, wiring it through config, gateway RPC, and slash-command UX.
Changes:
- Introduces
display.tui_status_indicator(defaultkaomoji) and threads it intoUiState.indicatorStylevia config sync. - Adds
/indicator [style]slash command (+ completion wiring) and gatewayconfig.get/config.setsupport forkey=indicator. - Updates
FaceTickerto render different indicator styles and decouple glyph tick cadence from verb tick cadence; adds targeted tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ui-tui/src/gatewayTypes.ts | Adds config field typing for tui_status_indicator; small event union formatting change. |
| ui-tui/src/components/appChrome.tsx | Implements pluggable indicator rendering in FaceTicker with per-style cadence. |
| ui-tui/src/app/useConfigSync.ts | Normalizes indicator style and applies it into UI state from config.get full. |
| ui-tui/src/app/uiStore.ts | Adds indicatorStyle default to the UI store. |
| ui-tui/src/app/slash/commands/session.ts | Adds /indicator slash command with validation + hot-swap via patchUiState. |
| ui-tui/src/app/interfaces.ts | Introduces IndicatorStyle type and adds it to UiState. |
| ui-tui/src/tests/useConfigSync.test.ts | Adds unit tests for indicator normalization and config fan-out. |
| ui-tui/src/tests/createSlashHandler.test.ts | Adds routing + behavior tests for /indicator. |
| tui_gateway/server.py | Adds config.set/get handling for key=indicator. |
| hermes_cli/config.py | Adds default config display.tui_status_indicator. |
| hermes_cli/commands.py | Adds indicator CommandDef for completion/catalog exposure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The status-bar `FaceTicker` rotated through wide-and-variable kaomoji glyphs (`(。•́︿•̀。)`, `( ͡° ͜ʖ ͡°)`, …) every 2.5s. Real display widths range from ~5 to ~16 columns, so the rest of the bar (cwd, ctx %, voice, bg counter) shifted on every cycle. Padding the verb alone (#17116) helped but didn't address the dominant jitter source — the glyph itself. Add four indicator styles, configurable + hot-swappable: * `kaomoji` (default — preserves the existing vibe; verb is now pad-stable so the only width churn left is the kaomoji itself). * `emoji` — single 2-col emoji frame (`⚕ 🌀 🤔 ✨ 🍵 🔮`). * `unicode` — `unicode-animations` braille spinner (1-col, smooth). * `ascii` — `| / - \` (1-col, max compat). Wires: * `display.tui_status_indicator` in `DEFAULT_CONFIG` (default `kaomoji`). * New JSON-RPC `config.set/get indicator` keys, narrow allow-list. * `applyDisplay` reads the field and patches `UiState.indicatorStyle`, so the existing `mtime` poll picks up `~/.hermes/config.yaml` edits within ~5s without a TUI restart. * `/indicator [style]` slash command (alias `/indicator-style`, subcommand completion `kaomoji|emoji|unicode|ascii`). Bare form shows the current style; setter fires `config.set` and optimistically `patchUiState({ indicatorStyle })` so the live TUI swaps immediately, matching the `/skin` UX. * `CommandDef("indicator", ..., subcommands=...)` so classic CLI autocomplete + TUI `complete.slash` both surface it. * `FaceTicker` decouples spinner cadence from verb cadence — the glyph runs at the spinner's authored interval (or `FACE_TICK_MS` for kaomoji), the verb stays on the original 2.5s cycle, and both re-arm cleanly when style changes. Tests: * `normalizeIndicatorStyle` rejects unknown / non-string input. * `applyDisplay → tui_status_indicator` covers fan-out + fallback. * `/indicator <style>` hot-swaps `UiState.indicatorStyle` after a successful `config.set`. * `/indicator sparkle` rejects with the usage hint and never hits the gateway. * Slash-parity matrix gets `'/indicator'` → `config.get`. Validation: cd ui-tui && npm run type-check — clean; npm test --run — 398/398. scripts/run_tests.sh tests/test_tui_gateway_server.py tests/hermes_cli/test_commands.py — 220/220.
…rror format Round 1 Copilot review on PR #17150: - Exported `INDICATOR_STYLES` const tuple from `interfaces.ts`; `IndicatorStyle` union type is derived from it. `useConfigSync` builds its validation Set from the tuple, and `session.ts` uses it for both the usage hint and the runtime allow-list — adding/removing a style now touches one line. - Backend `config.set indicator` error message: switched `sorted(allowed)` list repr to `pick one of ascii|emoji|kaomoji|unicode` (matches the TUI usage hint), and reports the normalized `raw` instead of the original `value`. Backend allowed tuple now has a comment pointing back at `INDICATOR_STYLES` so the two stay aligned. Note: kept the verb portion unpadded per design intent — fixed-width padding was the exact UX the `/indicator` command was added to remove. Stable width comes from the glyph; verbs cycling is part of the kawaii aesthetic. Reply on the verb thread will explain.
Round 2 Copilot review on PR #17150: - `tui_status_indicator?: 'ascii' | ... | string` collapses to `string` in TS — consumers got no narrowing. Documented as plain `string` with a comment about runtime validation via `normalizeIndicatorStyle`. - `FaceTicker` always started a 2.5s verb interval, even for the `unicode` style which hides the verb entirely. Now gated on `showVerb` from `renderIndicator` — `unicode` stays calm. Pre-emptive self-review (avoid round 3): - Three call sites duplicated the literal `'kaomoji'` default (uiStore, normalizeIndicatorStyle, slash command). Added `DEFAULT_INDICATOR_STYLE` to interfaces.ts and threaded it through so changing the default touches one line.
db3f220 to
112495a
Compare
…render Round 4 Copilot review on PR #17150: `config.get` for `indicator` returned the raw `display.tui_status_indicator` value without validation, so a hand-edited config.yaml with stray casing or an unknown style would leave `/indicator` printing one thing while the TUI rendered the kaomoji default (frontend's `normalizeIndicatorStyle` does this normalization on receive). Lifted the allow-list to module scope as `_INDICATOR_STYLES` / `_INDICATOR_DEFAULT`, reused by both `config.set` and `config.get`. Comment notes the alignment with `INDICATOR_STYLES` / `DEFAULT_INDICATOR_STYLE` in interfaces.ts so adding/removing a style is a one-line change on each end. Tests cover: known value verbatim, casing/whitespace normalize, unknown→default, unset→default.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…cator error Round 5 Copilot review on PR #17150: `raw = str(value or "").strip().lower()` collapsed any falsy non-string (`0`, `False`, `[]`) to empty string, so the error message read `unknown indicator: ` with nothing after — losing the original input. Switched to `("" if value is None else str(value)).strip().lower()` so only `None` (the genuine 'no value' case) becomes blank. Used `{raw!r}` in the error so the diagnostic is unambiguous (`'0'` vs `0`). Tests: - known-value happy path (`'EMOJI'` → `'emoji'`) - falsy non-string inputs (`0` / `False` / `[]`) surface meaningfully - `None` keeps the blank-repr error
Summary
The status-bar
FaceTickerrotated through wide-and-variable kaomoji glyphs ((。•́︿•̀。),( ͡° ͜ʖ ͡°), …) every 2.5s — display widths range from ~5 to ~16 columns, so the rest of the bar (cwd, ctx %, voice, bg counter) shifted on every cycle.Padding the verb alone (#17116) helped a little but didn't address the dominant jitter source — the glyph itself. Closer fix: let the user pick a stable-width indicator style.
What this adds
Four busy-indicator styles, default keeps current vibe:
kaomoji(default) — preserves the existing experience.emoji— single 2-col emoji frame:⚕ 🌀 🤔 ✨ 🍵 🔮.unicode—unicode-animationsbraille spinner (1-col, smooth).ascii—| / - \(1-col, max compatibility).Wires
display.tui_status_indicatorinDEFAULT_CONFIG(defaultkaomoji).config.set/config.gethandler forkey=indicatorwith a narrow allow-list.applyDisplayreads the field intoUiState.indicatorStyle; the existing 5s mtime poll picks up~/.hermes/config.yamledits without restart./indicator [style]slash command. Bare form shows current; setter firesconfig.setand optimisticallypatchUiStateso the live TUI swaps immediately (matching/skinUX). Subcommand completion:kaomoji|emoji|unicode|ascii.CommandDef("indicator", ..., subcommands=...)so classic CLI autocomplete + TUIcomplete.slashboth surface it.FaceTickerdecouples spinner cadence from verb cadence — glyph runs at the spinner's authored interval (orFACE_TICK_MSfor kaomoji), verb stays on the original 2.5s cycle, both re-arm cleanly when style changes.Test plan
npm run type-check— cleannpm test -- --run— 398/398 passscripts/run_tests.sh tests/test_tui_gateway_server.py tests/hermes_cli/test_commands.py— 220/220 passnormalizeIndicatorStylerejects unknown / non-stringapplyDisplay → tui_status_indicatorcovers fan-out + fallback/indicator <style>hot-swapsUiState.indicatorStyleafterconfig.set/indicator sparklerejects with usage hint, never hits gateway'/indicator'→config.getRefs
Supersedes #17116 (
bb/tui-status-ticker-width) — that one padded the verb to the longest catalogue entry, which addressed only a small share of the actual width churn. Closes #13610 by giving users a way out instead of pretending kaomoji widths are fixable.