chore(tui): strip showroom chrome and beef up slash demo

- 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
This commit is contained in:
Brooklyn Nicholson
2026-04-25 23:39:06 -05:00
parent 72ca0809c4
commit 3eadf10047
5 changed files with 175 additions and 186 deletions
+27 -44
View File
@@ -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/<name>.html for every workflow
npm run showroom:record # regenerate every workflow JSON
npm run showroom:build # dist/<name>.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=<name>` to deep-link a workflow.
Pick a workflow from the dropdown or deep-link with `?w=<name>`.
## Architecture
```
record.tsx ─┐
↳ MessageLine, │ Ink renders → custom Writable → ANSI string
Panel, Box, Text
↳ MessageLine, │ Ink renders → Writable → ANSI string
Panel, Box, Text │
workflows/<name>.json
│ served at /api/workflow/<name>
showroom.js │ xterm.js writes ANSI; DOM overlays target frame ids
│ served at /api/workflow/<name>
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 1x4x for capture-ready output.
`target` references the `id` of an earlier `frame`. `viewport.scale` (or the 1x4x picker) controls the upscale factor for capture.
## Player
- Restart, Clear, 1x4x 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`), 1x4x 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(<Component />)` 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(<Component />)` 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.
+47 -21
View File
@@ -245,6 +245,9 @@ const subagentTrail = async () => {
}
const slashCommands = async () => {
const slashEcho = (text: string) => snap(<Msg kind="slash" role="user" text={text} />)
const skillsEcho = await slashEcho('/skills search vibe')
const skillsResults = await snap(
<Panel
sections={[
@@ -257,11 +260,12 @@ const slashCommands = async () => {
}
]}
t={t}
title="/skills search vibe"
title="skills · search vibe"
/>,
180
)
const modelEcho = await slashEcho('/model claude-4.6-sonnet')
const modelSwitch = await snap(
<Panel
sections={[
@@ -279,6 +283,7 @@ const slashCommands = async () => {
180
)
const agentsEcho = await slashEcho('/agents pause')
const agentsStatus = await snap(
<Panel
sections={[
@@ -291,47 +296,68 @@ const slashCommands = async () => {
}
]}
t={t}
title="/agents · paused"
title="agents · paused"
/>,
180
)
const helpEcho = await slashEcho('/help')
const helpPanel = await snap(
<Panel
sections={[
{ items: ['/skills search · install · inspect', '/model switch model · pop picker'], title: 'Tools & Skills' },
{ items: ['/agents spawn-tree dashboard', '/queue queue prompt for next turn', '/steer inject after next tool call'], title: 'Session' },
{ items: ['/voice toggle voice mode', '/details thinking · tools · subagents · activity'], title: 'Configuration' }
]}
t={t}
title="(^_^)? Commands"
/>,
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 }
+25 -61
View File
@@ -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;
+6 -28
View File
@@ -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 = `
<header class="showroom-title">
<span class="showroom-title-name">
<span data-role="title">${escapeHtml(state.workflow.title ?? 'Hermes TUI Showroom')}</span>
<span class="showroom-title-tag">${state.frameMode ? 'real ink' : 'showroom'}</span>
</span>
<span class="showroom-meta">
<span data-role="meta">${state.viewport.cols}x${state.viewport.rows} · ${state.scale}x</span>
${catalog.length > 1 ? `<select class="showroom-picker" data-action="picker">${buildOptions()}</select>` : ''}
</span>
</header>
<div class="showroom-stage">
<div class="showroom-terminal">
<div class="showroom-status" data-target="status">
@@ -496,17 +483,15 @@ const renderShell = () => {
</div>
<div class="showroom-overlays"></div>
</div>
<div class="showroom-progress">
<span data-role="time">0.0s / 0.0s</span>
<div class="showroom-progress-track"><div class="showroom-progress-fill"></div></div>
</div>
<footer class="showroom-controls">
<button type="button" data-action="restart">Restart</button>
<button type="button" data-action="clear">Clear</button>
<span class="showroom-segmented-label">scale</span>
<button type="button" data-action="restart" title="restart (R)">↻</button>
<span class="showroom-segmented" data-segment="scale">${buildSegmented(SCALES, state.scale)}</span>
<span class="showroom-segmented-label">speed</span>
<span class="showroom-segmented" data-segment="speed">${buildSegmented(SPEEDS, state.speed)}</span>
${catalog.length > 1 ? `<select class="showroom-picker" data-action="picker">${buildOptions()}</select>` : ''}
<span class="showroom-progress">
<span data-role="time">0.0s / 0.0s</span>
<div class="showroom-progress-track"><div class="showroom-progress-fill"></div></div>
</span>
</footer>
`
@@ -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])
}
+70 -32
View File
@@ -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",