From 3eadf100478f64ccd91c38b6e01c3cdedb044f00 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 25 Apr 2026 23:39:06 -0500 Subject: [PATCH] chore(tui): strip showroom chrome and beef up slash demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drop title bar, "real ink" tag, meta line — terminal box is the surface - single bottom controls row: ↻ · scale · speed · picker · progress - slash workflow now types each command, echoes a slash msg, then renders the panel - adds a /help scene (real Panel grouped by category) - README minus the "real ink" marketing --- ui-tui/.showroom/README.md | 71 +++++------- ui-tui/.showroom/record.tsx | 68 ++++++++---- ui-tui/.showroom/src/showroom.css | 86 +++++---------- ui-tui/.showroom/src/showroom.js | 34 ++---- .../.showroom/workflows/slash-commands.json | 102 ++++++++++++------ 5 files changed, 175 insertions(+), 186 deletions(-) diff --git a/ui-tui/.showroom/README.md b/ui-tui/.showroom/README.md index 3c23df969a..dd131bbe98 100644 --- a/ui-tui/.showroom/README.md +++ b/ui-tui/.showroom/README.md @@ -1,59 +1,43 @@ # TUI Showroom -Cinematic demos of the real `ui-tui`. Workflows are built from actual Ink-rendered ANSI captured from `MessageLine`, `Panel`, and friends — replayed in xterm.js with timeline overlays (captions, spotlights, fades, highlights). +Scripted demos of `ui-tui`. Workflows snapshot real ui-tui components (`MessageLine`, `Panel`, `Box`, `Text`) into ANSI and replay them through xterm.js with cinematic overlays. Recorded once, played any number of times — built for screen capture. ```bash npm run showroom # dev server at http://127.0.0.1:4317 -npm run showroom:record # re-record all workflows (regenerates JSON) -npm run showroom:build # builds dist/.html for every workflow +npm run showroom:record # regenerate every workflow JSON +npm run showroom:build # dist/.html for every workflow npm run showroom:type-check ``` ## Bundled workflows -| File | Demonstrates | -| -------------------------------------- | ----------------------------------------------------- | -| `workflows/feature-tour.json` | Plan → tool trail → result highlight | -| `workflows/subagent-trail.json` | Parallel subagents, hot lanes, summary | -| `workflows/slash-commands.json` | `/skills`, `/model`, `/agents` panels | -| `workflows/voice-mode.json` | VAD capture, transcript, TTS ducking | +| File | Shows | +| -------------------------------------- | -------------------------------------------------------------- | +| `workflows/feature-tour.json` | Plan → tool trail → result highlight | +| `workflows/subagent-trail.json` | Parallel subagents, hot lanes, summary | +| `workflows/slash-commands.json` | `/skills`, `/model`, `/agents`, `/help` typed → echoed → panel | +| `workflows/voice-mode.json` | VAD capture, transcript, TTS ducking | -Use the dropdown in the top-right or pass `?w=` to deep-link a workflow. +Pick a workflow from the dropdown or deep-link with `?w=`. ## Architecture ``` record.tsx ─┐ - ↳ MessageLine, │ Ink renders → custom Writable → ANSI string - Panel, Box, Text │ - ▼ + ↳ MessageLine, │ Ink renders → Writable → ANSI string + Panel, Box, Text │ + ▼ workflows/.json - │ served at /api/workflow/ - ▼ -showroom.js │ xterm.js writes ANSI; DOM overlays target frame ids - ▼ + │ served at /api/workflow/ + ▼ +showroom.js │ xterm.js writes ANSI; DOM overlays target frame ids + ▼ browser ``` -Every `frame` action embeds the ANSI bytes from a real Ink render; the browser replays them via `@xterm/xterm` (loaded from jsDelivr) so the surface is the actual TUI, not a CSS approximation. Cinematic overlays (captions, spotlights, highlights, fades) are positioned by frame `id` and rendered via DOM. +`frame` actions embed ANSI from an Ink render; the browser feeds them into `@xterm/xterm` (jsDelivr CDN) so the surface is the actual TUI. Captions, spotlights, highlights, and fades are DOM overlays anchored to frame `id`s. -## Workflow Shape - -```json -{ - "title": "Hermes TUI · Feature Tour", - "viewport": { "cols": 80, "rows": 16 }, - "composer": "ask hermes anything", - "timeline": [ - { "at": 200, "type": "frame", "id": "user-row", "ansi": "..." }, - { "at": 1500, "type": "frame", "id": "assistant", "ansi": "..." }, - { "at": 1700, "type": "spotlight", "target": "assistant" }, - { "at": 1900, "type": "caption", "target": "assistant", "text": "..." } - ] -} -``` - -## Timeline Actions +## Timeline actions | Action | Required | Optional | | ----------- | -------------------- | ----------------------------------------------------- | @@ -66,19 +50,18 @@ Every `frame` action embeds the ANSI bytes from a real Ink render; the browser r | `fade` | `target` | `to` (default `0`), `duration` | | `clear` | — | — | -`target` references the `id` of an earlier `frame`. `viewport.scale` (default = best-fit integer) controls the upscale factor; manual buttons offer 1x–4x for capture-ready output. +`target` references the `id` of an earlier `frame`. `viewport.scale` (or the 1x–4x picker) controls the upscale factor for capture. ## Player -- Restart, Clear, 1x–4x scale, 0.5x/1x/2x speed. -- Keyboard: `R` restart, `C` clear, `1`/`2`/`3` speed. -- Progress bar tracks elapsed/total based on the slowest action's `at + duration`. +- Restart (`R`), 1x–4x scale, 0.5x/1x/2x speed (`1`/`2`/`3`). +- Progress bar reads `at + duration` from the slowest action. ## Adding a workflow -1. Add a scene fn to `record.tsx` that returns a `{ title, viewport, composer, timeline }` shape. -2. Compose Ink primitives (`Box`, `Text`) or import real ui-tui components (`MessageLine`, `Panel`). -3. Snap each scene with `await snap()` to capture ANSI. -4. Run `npm run showroom:record`. +1. Add a scene fn to `record.tsx` returning `{ title, viewport, composer, timeline }`. +2. Compose Ink primitives or pull `MessageLine` / `Panel` from `../src`. +3. `await snap()` for each frame. +4. `npm run showroom:record`. -Components rendered to ANSI must be **state-free** at first paint — `useEffect` hooks usually haven't fired by the time the recorder unmounts. For accordions like the live `ToolTrail`, render an inline scene with `Box` + `Text` instead. +Components must be state-free at first paint — `useEffect` hooks won't fire by the time the recorder unmounts. For accordions like the live `ToolTrail`, render a flat `Box` + `Text` scene instead. diff --git a/ui-tui/.showroom/record.tsx b/ui-tui/.showroom/record.tsx index c851bbe417..664b588063 100644 --- a/ui-tui/.showroom/record.tsx +++ b/ui-tui/.showroom/record.tsx @@ -245,6 +245,9 @@ const subagentTrail = async () => { } const slashCommands = async () => { + const slashEcho = (text: string) => snap() + + const skillsEcho = await slashEcho('/skills search vibe') const skillsResults = await snap( { } ]} t={t} - title="/skills search vibe" + title="skills · search vibe" />, 180 ) + const modelEcho = await slashEcho('/model claude-4.6-sonnet') const modelSwitch = await snap( { 180 ) + const agentsEcho = await slashEcho('/agents pause') const agentsStatus = await snap( { } ]} t={t} - title="/agents · paused" + title="agents · paused" />, 180 ) + const helpEcho = await slashEcho('/help') + const helpPanel = await snap( + , + 220 + ) + return { - composer: 'press / to open the palette', + composer: '', timeline: [ - { at: 200, duration: 500, text: '/skills search vibe', type: 'compose' }, - { ansi: skillsResults, at: 800, id: 'skills', type: 'frame' }, - { at: 1100, duration: 1500, target: 'skills', type: 'spotlight' }, + { at: 200, duration: 700, text: '/skills search vibe', type: 'compose' }, + { ansi: skillsEcho, at: 1100, type: 'frame' }, + { at: 1100, duration: 200, text: '', type: 'compose' }, + { ansi: skillsResults, at: 1400, id: 'skills', type: 'frame' }, { - at: 1300, - duration: 1700, + at: 1700, + duration: 2000, position: 'right', target: 'skills', - text: 'Slash commands stream live results without blocking the composer.', + text: 'Typed /skills, hit return — same Panel the live TUI renders.', type: 'caption' }, - { at: 3300, duration: 600, text: '/model claude-4.6-sonnet', type: 'compose' }, - { ansi: modelSwitch, at: 4100, id: 'model', type: 'frame' }, + { at: 4000, duration: 700, text: '/model claude-4.6-sonnet', type: 'compose' }, + { ansi: modelEcho, at: 4900, type: 'frame' }, + { at: 4900, duration: 200, text: '', type: 'compose' }, + { ansi: modelSwitch, at: 5200, id: 'model', type: 'frame' }, { - at: 4400, - duration: 1700, + at: 5500, + duration: 1900, position: 'right', target: 'model', - text: '/model also pops the inline picker when no arg is given.', + text: '/model swaps mid-session; transcript and cache stay intact.', type: 'caption' }, - { at: 6300, duration: 600, text: '/agents pause', type: 'compose' }, - { ansi: agentsStatus, at: 7000, id: 'agents', type: 'frame' }, - { at: 7300, duration: 1300, target: 'agents', type: 'highlight' }, + { at: 7600, duration: 600, text: '/agents pause', type: 'compose' }, + { ansi: agentsEcho, at: 8400, type: 'frame' }, + { at: 8400, duration: 200, text: '', type: 'compose' }, + { ansi: agentsStatus, at: 8700, id: 'agents', type: 'frame' }, { - at: 7500, - duration: 1700, + at: 9000, + duration: 1800, position: 'right', target: 'agents', - text: 'Same registry powers TUI, gateway, Telegram, Discord — one source of truth.', + text: 'Same registry powers TUI, gateway, Telegram, Discord — one truth.', type: 'caption' }, - { at: 9300, duration: 600, text: '/resume', type: 'compose' } + { at: 11000, duration: 400, text: '/help', type: 'compose' }, + { ansi: helpEcho, at: 11500, type: 'frame' }, + { at: 11500, duration: 200, text: '', type: 'compose' }, + { ansi: helpPanel, at: 11800, id: 'help', type: 'frame' } ], title: 'Hermes TUI · Slash Commands', viewport: { cols: COLS, rows: ROWS } diff --git a/ui-tui/.showroom/src/showroom.css b/ui-tui/.showroom/src/showroom.css index 0f637833c3..f60b21618d 100644 --- a/ui-tui/.showroom/src/showroom.css +++ b/ui-tui/.showroom/src/showroom.css @@ -40,7 +40,7 @@ body { .showroom-shell { display: grid; - gap: 10px; + gap: 14px; width: max-content; max-width: 100%; opacity: 0; @@ -55,39 +55,6 @@ body { transform: translateY(0); } -.showroom-title { - display: flex; - align-items: end; - justify-content: space-between; - gap: 24px; - color: var(--cornsilk); - font-size: 18px; - letter-spacing: 0.04em; -} - -.showroom-title-name { - display: flex; - align-items: baseline; - gap: 12px; -} - -.showroom-title-tag { - color: var(--dim); - font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.16em; -} - -.showroom-meta { - display: flex; - gap: 12px; - align-items: center; - color: var(--dim); - font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; - font-size: 12px; -} - .showroom-picker { appearance: none; border: 1px solid rgba(205, 127, 50, 0.4); @@ -354,28 +321,38 @@ body { .showroom-controls { display: flex; flex-wrap: wrap; - gap: 10px; + gap: 8px; align-items: center; + font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; + font-size: 12px; } .showroom-controls button { - border: 1px solid rgba(205, 127, 50, 0.35); + border: 1px solid rgba(205, 127, 50, 0.25); border-radius: 999px; - padding: 6px 14px; - background: rgba(205, 127, 50, 0.06); - color: var(--cornsilk); + padding: 4px 10px; + background: rgba(205, 127, 50, 0.04); + color: var(--dim); cursor: pointer; font: inherit; - font-size: 13px; } .showroom-controls button:hover { - background: rgba(205, 127, 50, 0.14); + background: rgba(205, 127, 50, 0.12); + color: var(--cornsilk); +} + +.showroom-controls button[data-action="restart"] { + width: 28px; + height: 28px; + padding: 0; + font-size: 14px; + line-height: 1; } .showroom-segmented { display: inline-flex; - border: 1px solid rgba(205, 127, 50, 0.35); + border: 1px solid rgba(205, 127, 50, 0.25); border-radius: 999px; padding: 2px; background: rgba(205, 127, 50, 0.04); @@ -384,12 +361,11 @@ body { .showroom-segmented button { border: 0; border-radius: 999px; - padding: 4px 12px; + padding: 3px 10px; background: transparent; color: var(--dim); cursor: pointer; font: inherit; - font-size: 12px; } .showroom-segmented button.is-active { @@ -397,31 +373,19 @@ body { color: var(--cornsilk); } -.showroom-segmented-label { - align-self: center; - margin-right: 4px; - color: var(--dim); - font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.12em; -} - .showroom-progress { - display: flex; + display: inline-flex; align-items: center; - gap: 12px; - width: 100%; - margin-top: 4px; + gap: 10px; + flex: 1; + min-width: 140px; color: var(--dim); - font-family: 'JetBrains Mono', 'SFMono-Regular', Consolas, monospace; - font-size: 11px; } .showroom-progress-track { position: relative; flex: 1; - height: 3px; + height: 2px; border-radius: 999px; background: rgba(205, 127, 50, 0.1); overflow: hidden; diff --git a/ui-tui/.showroom/src/showroom.js b/ui-tui/.showroom/src/showroom.js index 4834f8815c..c7b05a5729 100644 --- a/ui-tui/.showroom/src/showroom.js +++ b/ui-tui/.showroom/src/showroom.js @@ -395,9 +395,6 @@ const setScale = next => { for (const button of state.shell.querySelectorAll('[data-segment="scale"] button')) { button.classList.toggle('is-active', Number(button.dataset.value) === next) } - - state.shell.querySelector('[data-role="meta"]').textContent = - `${state.viewport.cols}x${state.viewport.rows} · ${next}x` } const fitScale = () => { @@ -475,16 +472,6 @@ const renderShell = () => { state.shell.style.setProperty('--term-h', `${state.viewport.rows * state.viewport.lineHeight}px`) state.shell.innerHTML = ` -
- - ${escapeHtml(state.workflow.title ?? 'Hermes TUI Showroom')} - ${state.frameMode ? 'real ink' : 'showroom'} - - - ${state.viewport.cols}x${state.viewport.rows} · ${state.scale}x - ${catalog.length > 1 ? `` : ''} - -
@@ -496,17 +483,15 @@ const renderShell = () => {
-
- 0.0s / 0.0s -
-
` @@ -519,10 +504,6 @@ const renderShell = () => { state.progressLabel = state.shell.querySelector('[data-role="time"]') state.shell.querySelector('[data-action="restart"]').addEventListener('click', play) - state.shell.querySelector('[data-action="clear"]').addEventListener('click', () => { - clearTimers() - clearTranscript() - }) for (const button of state.shell.querySelectorAll('[data-segment="speed"] button')) { button.addEventListener('click', () => setSpeed(Number(button.dataset.value))) @@ -574,9 +555,6 @@ const mount = () => { if (key === 'r') { play() - } else if (key === 'c') { - clearTimers() - clearTranscript() } else if (key === '1' || key === '2' || key === '3') { setSpeed(SPEEDS[Number(key) - 1]) } diff --git a/ui-tui/.showroom/workflows/slash-commands.json b/ui-tui/.showroom/workflows/slash-commands.json index 4a3c408af2..93d3315c7a 100644 --- a/ui-tui/.showroom/workflows/slash-commands.json +++ b/ui-tui/.showroom/workflows/slash-commands.json @@ -1,83 +1,121 @@ { - "composer": "press / to open the palette", + "composer": "", "timeline": [ { "at": 200, - "duration": 500, + "duration": 700, "text": "/skills search vibe", "type": "compose" }, { - "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[29C/skills\u001b[1Csearch\u001b[1Cvibe\u001b[30C│\r\n│\u001b[78C│\r\n│\u001b[2Canthropics/skills/frontend-design★\u001b[1Ctrusted\u001b[34C│\r\n│\u001b[2Copenai/skills/skill-creator·\u001b[1Cofficial\u001b[39C│\r\n│\u001b[2Cskills.sh/community/vibe-coding⚙\u001b[1Ccommunity\u001b[33C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", - "at": 800, - "id": "skills", + "ansi": "\u001b[?25l\u001b[?2026h\r\n❯\u001b[2C/skills\u001b[1Csearch\u001b[1Cvibe\r\n\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 1100, "type": "frame" }, { "at": 1100, - "duration": 1500, - "target": "skills", - "type": "spotlight" + "duration": 200, + "text": "", + "type": "compose" }, { - "at": 1300, - "duration": 1700, + "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[29Cskills\u001b[1C·\u001b[1Csearch\u001b[1Cvibe\u001b[29C│\r\n│\u001b[78C│\r\n│\u001b[2Canthropics/skills/frontend-design★\u001b[1Ctrusted\u001b[34C│\r\n│\u001b[2Copenai/skills/skill-creator·\u001b[1Cofficial\u001b[39C│\r\n│\u001b[2Cskills.sh/community/vibe-coding⚙\u001b[1Ccommunity\u001b[33C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 1400, + "id": "skills", + "type": "frame" + }, + { + "at": 1700, + "duration": 2000, "position": "right", "target": "skills", - "text": "Slash commands stream live results without blocking the composer.", + "text": "Typed /skills, hit return — same Panel the live TUI renders.", "type": "caption" }, { - "at": 3300, - "duration": 600, + "at": 4000, + "duration": 700, "text": "/model claude-4.6-sonnet", "type": "compose" }, + { + "ansi": "\u001b[?25l\u001b[?2026h\r\n❯\u001b[2C/model\u001b[1Cclaude-4.6-sonnet\r\n\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 4900, + "type": "frame" + }, + { + "at": 4900, + "duration": 200, + "text": "", + "type": "compose" + }, { "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[32Cmodel\u001b[1Cswitched\u001b[32C│\r\n│\u001b[78C│\r\n│\u001b[2Cfrom\u001b[16Cgpt-5-codex\u001b[45C│\r\n│\u001b[2Cto\u001b[18Cclaude-4.6-sonnet\u001b[39C│\r\n│\u001b[2Cscope\u001b[15Cthis\u001b[1Csession\u001b[44C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", - "at": 4100, + "at": 5200, "id": "model", "type": "frame" }, { - "at": 4400, - "duration": 1700, + "at": 5500, + "duration": 1900, "position": "right", "target": "model", - "text": "/model also pops the inline picker when no arg is given.", + "text": "/model swaps mid-session; transcript and cache stay intact.", "type": "caption" }, { - "at": 6300, + "at": 7600, "duration": 600, "text": "/agents pause", "type": "compose" }, { - "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[31C/agents\u001b[1C·\u001b[1Cpaused\u001b[31C│\r\n│\u001b[78C│\r\n│\u001b[2Cdelegation\u001b[10Cpaused\u001b[50C│\r\n│\u001b[2Cmax\u001b[1Cchildren\u001b[8C4\u001b[55C│\r\n│\u001b[2Crunning\u001b[1Ctasks\u001b[7Cqueued\u001b[1Cfor\u001b[1Cresume\u001b[39C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", - "at": 7000, + "ansi": "\u001b[?25l\u001b[?2026h\r\n❯\u001b[2C/agents\u001b[1Cpause\r\n\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 8400, + "type": "frame" + }, + { + "at": 8400, + "duration": 200, + "text": "", + "type": "compose" + }, + { + "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[31Cagents\u001b[1C·\u001b[1Cpaused\u001b[32C│\r\n│\u001b[78C│\r\n│\u001b[2Cdelegation\u001b[10Cpaused\u001b[50C│\r\n│\u001b[2Cmax\u001b[1Cchildren\u001b[8C4\u001b[55C│\r\n│\u001b[2Crunning\u001b[1Ctasks\u001b[7Cqueued\u001b[1Cfor\u001b[1Cresume\u001b[39C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 8700, "id": "agents", "type": "frame" }, { - "at": 7300, - "duration": 1300, - "target": "agents", - "type": "highlight" - }, - { - "at": 7500, - "duration": 1700, + "at": 9000, + "duration": 1800, "position": "right", "target": "agents", - "text": "Same registry powers TUI, gateway, Telegram, Discord — one source of truth.", + "text": "Same registry powers TUI, gateway, Telegram, Discord — one truth.", "type": "caption" }, { - "at": 9300, - "duration": 600, - "text": "/resume", + "at": 11000, + "duration": 400, + "text": "/help", "type": "compose" + }, + { + "ansi": "\u001b[?25l\u001b[?2026h\r\n❯\u001b[2C/help\r\n\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 11500, + "type": "frame" + }, + { + "at": 11500, + "duration": 200, + "text": "", + "type": "compose" + }, + { + "ansi": "\u001b[?25l\u001b[?2026h╭──────────────────────────────────────────────────────────────────────────────╮\r\n│\u001b[78C│\r\n│\u001b[31C(^_^)?\u001b[1CCommands\u001b[32C│\r\n│\u001b[78C│\r\n│\u001b[2CTools\u001b[1C&\u001b[1CSkills\u001b[62C│\r\n│\u001b[2C/skills\u001b[4Csearch\u001b[1C·\u001b[1Cinstall\u001b[1C·\u001b[1Cinspect\u001b[39C│\r\n│\u001b[2C/model\u001b[5Cswitch\u001b[1Cmodel\u001b[1C·\u001b[1Cpop\u001b[1Cpicker\u001b[40C│\r\n│\u001b[78C│\r\n│\u001b[2CSession\u001b[69C│\r\n│\u001b[2C/agents\u001b[4Cspawn-tree\u001b[1Cdashboard\u001b[45C│\r\n│\u001b[2C/queue\u001b[5Cqueue\u001b[1Cprompt\u001b[1Cfor\u001b[1Cnext\u001b[1Cturn\u001b[39C│\r\n│\u001b[2C/steer\u001b[5Cinject\u001b[1Cafter\u001b[1Cnext\u001b[1Ctool\u001b[1Ccall\u001b[38C│\r\n│\u001b[78C│\r\n│\u001b[2CConfiguration\u001b[63C│\r\n│\u001b[2C/voice\u001b[5Ctoggle\u001b[1Cvoice\u001b[1Cmode\u001b[48C│\r\n│\u001b[2C/details\u001b[3Cthinking\u001b[1C·\u001b[1Ctools\u001b[1C·\u001b[1Csubagents\u001b[1C·\u001b[1Cactivity\u001b[26C│\r\n│\u001b[78C│\r\n╰──────────────────────────────────────────────────────────────────────────────╯\r\n\u001b[?2026l\u001b[?2026h\u001b[?25h\u001b[?2026l\u001b[?25h", + "at": 11800, + "id": "help", + "type": "frame" } ], "title": "Hermes TUI · Slash Commands",