# Per-agent terminal: row taxonomy (as built) Snapshot of how the per-agent web UI's live pane renders each event kind today. Source of truth lives in `hive-ag3nt/assets/app.js` (`renderStream`, `fmtToolUse`, `renderRichToolUse`, `renderToolResult`, `renderTaskEvent`, `mdNode`, `detailsOpenMd`, `fmtArgsGeneric`) + `hive-fr0nt/assets/terminal.css` (the shared `.live .` styling) + `hive-fr0nt/assets/marked.min.js` (markdown). ## Layout contract Every row — flat `
` and expandable `
` alike — shares one prefix column. The mechanism is `padding-left + negative text-indent` on `.live .row`: the row's first character (the prefix glyph) gets pulled back into the column at ~0.5em, and wrapped continuation lines hang under the body, not under the glyph. `
` summaries inherit those metrics. The disclosure marker (`▸` / `▾`) is supplied by CSS `summary::before` so it lands in the same column as flat-row glyphs. To make that work the JS-side summary text **does not** include a directional `→` / `←` — the row's colour (cyan = outbound, muted = inbound) carries the direction, and the prefix column never has to fit two glyphs side-by-side. Child blocks inside a row (the `.md` markdown wrapper, an inner `
`) get `text-indent: 0` so their content lays out from the body column instead of inheriting the parent's negative pull. ## Row taxonomy | CSS class | Prefix glyph | Color | Triggered by | Source | |---|---|---|---|---| | `.turn-start` | `◆ TURN ← ` | amber, left rule | `LiveEvent::TurnStart` | harness wake | | `.turn-body` | (child div under turn-start) | fg | same | the wake-prompt body | | `.turn-end-ok` | `✓ turn ok` | green, left rule | `LiveEvent::TurnEnd { ok: true }` | harness | | `.turn-end-fail` | `✗ turn fail — note` | red, left rule | `LiveEvent::TurnEnd { ok: false }` | harness | | `.text` | (no prefix; markdown body) | fg | claude `assistant.content[].text` | stream-json | | `.thinking` | `· thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json | | `.tool-use` (flat) | `→ Name args…` | cyan | tool_use w/o rich renderer | stream-json | | `.tool-use` `
` | `Write/Edit · +N` (no `→`) | cyan, body is +/- diff | `renderRichToolUse` Write/Edit | stream-json | | `.tool-use` `
` | `send → to · NL`, `ask → to`, `answer #id` | cyan, body is markdown | rich renderer for send / ask / answer | stream-json | | `.tool-result` (flat) | `← ` | muted | short `tool_result` (≤120c, non-recv) | stream-json | | `.tool-result-block` `
` | `Nl · headline` | muted, body is text | long generic `tool_result` | stream-json | | `.tool-result-block` `
` | `recv ← ` | muted, body is markdown | `tool_result` correlated to a prior `recv` tool_use via id | stream-json | | `.tool-use` | `⌁ task started · [type]` | cyan | claude Task-tool subagent start | `renderTaskEvent` | | `.turn-end-ok` / `.turn-end-fail` / `.tool-result` | `⌁ task ✓/✗/◌ · · → ` | green / red / muted | claude Task-tool result | `renderTaskEvent` | | `.note` | `· ` | muted | harness chatter | `LiveEvent::Note` | | `.note.stderr` | `! stderr: ` | amber/orange | stderr lines off claude | `LiveEvent::Note` (`text` starts `stderr:`) | | `.note.op` | `· operator: ` | mauve italic | operator-initiated notes (/cancel, /compact, /model, new-session) | `LiveEvent::Note` (`text` starts `operator:`) | | `.sys` | `! {json…}` | amber/orange | catch-all for stream shapes `renderStream` didn't classify | catch-all | | Banner shimmer | mauve | turn in flight (ref-counted) | — | `setBannerActive` | ## Renderer dispatch `renderStream(v, api)` walks each stream-json line: 1. Drops `system/init`, `rate_limit_event`, `result` (noise / handled elsewhere — `result` powers the `cost` badge). 2. `subtype == "task_started" | "task_notification"` → `renderTaskEvent` (subagent activity gets the `⌁` glyph). 3. `type == "assistant"` → walk `message.content[]`: - `text` → `.text` row with a markdown body via `mdNode`. - `thinking` → `.thinking` row. - `tool_use` → record `id → name` in `toolNameById`, try `renderRichToolUse` (Write/Edit/send/ask/answer get custom renderings); on miss fall through to a flat `.tool-use` row with `fmtToolUse → fmtArgsGeneric`. 4. `type == "user"` → walk `message.content[]` for `tool_result`; `renderToolResult` correlates via `tool_use_id → toolNameById` to default-open `recv` results with a markdown body, else short = flat / long = collapsed details. 5. Unrecognised shape → `.sys` row (amber, `!` glyph). ## Markdown `mdNode(text)` wraps `window.marked.parse(text)` (vendored v4.0.2 UMD via `hive-fr0nt::MARKED_JS`) in a `
`. CSS in `terminal.css` scopes paragraph / code / list / blockquote / link styling under `.live .row .md` so the markdown body doesn't bleed into the row's own text-indent. Falls back to plain text if `marked` didn't load. Applied to `text` rows and to send / ask / answer / recv message bodies. ## Extra-MCP tools `fmtArgsGeneric(name, input)` is the fallback when a tool isn't in the built-in `fmtToolUse` switch: - single string field → `name k: "v"` - single number/bool field → `name k: v` - multi-field → first 4 pairs trimmed to `k: "v"` / `k: [N]` / `k: {…}` with a `…+N` overflow This keeps `mcp__matrix__send_message` and similar from dumping raw JSON. ## Dashboard side (not covered here) The main dashboard's message-flow pane is a different shape: broker messages render as `.msgrow` grid lines (ts / arrow / from / → / to / body) with their own styling. `.live .msgrow` explicitly resets `text-indent: 0` so the per-agent terminal's hanging-indent metrics don't leak into the flex-grid broker rows.