recv tool-use rows rendered as a bare recv() regardless of args, hiding whether a turn is parked on a long-poll (wait_seconds) or draining a burst (max). fmtToolUse now surfaces both. Bash rows gain a [bg] flag when run_in_background is set. closes #158
6 KiB
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 .<class>
styling) + hive-fr0nt/assets/marked.min.js (markdown).
Layout contract
Every row — flat <div class="row …"> and expandable
<details class="row …"> 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.
<details> 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 <details>) 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 ← <from> |
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 <details> |
Write/Edit <path> · +N (no →) |
cyan, body is +/- diff | renderRichToolUse Write/Edit |
stream-json |
.tool-use <details open> |
send → to · NL, ask → to, answer #id |
cyan, body is markdown | rich renderer for send / ask / answer | stream-json |
.tool-result (flat) |
← <txt> |
muted | short tool_result (≤120c, non-recv) |
stream-json |
.tool-result-block <details> |
Nl · headline |
muted, body is text | long generic tool_result |
stream-json |
.tool-result-block <details open> |
recv ← <txt> |
muted, body is markdown | tool_result correlated to a prior recv tool_use via id |
stream-json |
.tool-use |
⌁ task <id> started · <desc> [type] |
cyan | claude Task-tool subagent start | renderTaskEvent |
.turn-end-ok / .turn-end-fail / .tool-result |
⌁ task <id> ✓/✗/◌ <status> · <desc> · → <output_file> |
green / red / muted | claude Task-tool result | renderTaskEvent |
.note |
· <text> |
muted | harness chatter | LiveEvent::Note |
.note.stderr |
! stderr: <line> |
amber/orange | stderr lines off claude | LiveEvent::Note (text starts stderr:) |
.note.op |
· operator: <text> |
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:
- Drops
system/init,rate_limit_event,result(noise / handled elsewhere —resultpowers thecostbadge). subtype == "task_started" | "task_notification"→renderTaskEvent(subagent activity gets the⌁glyph).type == "assistant"→ walkmessage.content[]:text→.textrow with a markdown body viamdNode.thinking→.thinkingrow.tool_use→ recordid → nameintoolNameById, tryrenderRichToolUse(Write/Edit/send/ask/answer get custom renderings); on miss fall through to a flat.tool-userow withfmtToolUse → fmtArgsGeneric.fmtToolUsesurfaces the salient arg per built-in tool — e.g.recvshowswait <N>s/max <N>when set (barerecv()otherwise),Bashflags[bg]forrun_in_backgroundcommands.
type == "user"→ walkmessage.content[]fortool_result;renderToolResultcorrelates viatool_use_id → toolNameByIdto default-openrecvresults with a markdown body, else short = flat / long = collapsed details.- Unrecognised shape →
.sysrow (amber,!glyph).
Markdown
mdNode(text) wraps window.marked.parse(text) (vendored
v4.0.2 UMD via hive-fr0nt::MARKED_JS) in a <div class="md">. 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…+Noverflow
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.