A terminal row published on `$SWARM.term.<hive>.<agent>` goes out bare, with no envelope around it and no server-side stamp, so a subscriber had nothing to place the row in time with beyond its own receipt clock — wrong by the queue's latency and meaningless for anything read later than live. `TermMsg` gains `ts`, ISO 8601 UTC. `classify` takes the event's own unix-seconds stamp and applies it to every row that event expands into, so a row replayed out of sqlite says when it happened rather than when it was read, and a row that sat in a lagging subscriber's buffer does not lie about its time. The oversize degrade keeps it; only the body is ever spent. `TermEnvelope` stops duplicating `ts` and keeps `seq`: the dedup counter is a real transport concern, the event's time is not, now that it rides on the row. Nothing in the frontend read `envelope.ts` — only the type declared it. Refs #4321
3.3 KiB
Per-agent terminal: row taxonomy (as built)
The per-agent web UI's live pane renders one row per TermMsg
(hive-agent/src/term_msg.rs): {ts, icon?, level: debug|info|warn|error, summary, body?, body_format?: markdown|diff, coalesce_key?}. Classification
(icon, summary text, whether a tool call gets an expandable body) happens
server-side, once — term_msg.rs + stream_enrich.rs — and both
GET /api/events/history and GET /api/events/stream serve it identically
as TermEnvelope { seq?, msgs: TermMsg[] } frames
(hive-agent/src/web_ui/stream.rs).
ts is ISO 8601 UTC (2026-09-13T12:35:03Z) and is the source event's
time, stamped at classify time from the event's own stamp — so a row
replayed from sqlite says when it happened, not when it was fetched. It
lives on the row rather than on the envelope because the swarm queue
publishes rows bare, with no envelope around them
(hive-agent/src/swarm_term.rs); a subscriber there has nowhere else to
read the time from.
The frontend renders a TermMsg close to as-is
(frontend/packages/agent/src/components/Row.tsx): level picks the
CSS colour (terminal.css's .live .level-*), an empty summary +
markdown body renders as a flat row with just the body (assistant
text), and any other bodied row is an expandable <details>, opened by
default according to the operator's expand-tool-output preference
(getExpandDetailsPref()) — uniformly, no per-tool override. There's no
separate client-side row model or classification step.
Layout
Every row is its own 2-column grid — icon column (.row-glyph, width
from the --row-icon-col custom property) then content column
(.row-content, or a markdown body's own .md class) — so an icon's
left edge lands in the same place regardless of row kind. A <details>
row grids its <summary> instead of itself (so the <pre> body below
can stack full-width), using the exact same grid-template-columns value,
with the disclosure caret leading the summary text rather than the icon.
One shared column definition, not two independently computed offsets
kept in sync by hand — see terminal.css's own comment on the incident
that motivated the change.
Levels
| Level | Colour | Roughly |
|---|---|---|
debug |
muted | thinking, coalesced ticks, ambient harness chatter |
info |
default fg | turn start/ok, assistant text, tool calls + results |
warn |
amber, left rule | stderr, an unrecognised event shape, API retries |
error |
red, left rule | turn failed, a tool result with is_error: true |
The settings menu's "hide debug output" toggle (getHideDebugPref(),
@hive/shared/prefs.js) skips debug-level rows entirely rather than
muting them further — client-side only, same live-read-no-reload shape
as the expand-tool-output preference above.
Per-tool icon + summary formatting (what a Read/Edit/send call's row
actually says) lives in stream_enrich.rs's fmt_tool_use() family —
read that when you need the specifics, this doc doesn't duplicate it.
Markdown
frontend/packages/agent/src/lib/markdown.ts's renderMarkdown() runs
marked.parse() through DOMPurify into a <div class="md">. Applied to
any TermMsg with body_format: "markdown".
Dashboard side (not covered here)
The main dashboard's message-flow pane is a different shape: broker
messages render as .msgrow grid lines, not agent-terminal rows.