hyperhive/docs/web-ui/terminal-rendering.md
iris 593923375c swarm-ui: read-only agent terminal page consuming the swarm term stream
Moves the TermMsg rendering pipeline (Row.tsx, termMsg.ts, linkify.tsx,
markdown.ts) from @hive/agent into @hive/shared, so swarm-ui becomes a
second consumer of it instead of forking a copy -- CSS was already
shared (@hive/shared/terminal.css). marked+dompurify move from
@hive/agent's deps to @hive/shared's; swarm-ui picks them up
transitively, no new direct dep there.

New swarm-ui route /agents/:name/term (AgentTermPage), linked from
AgentsPage's detail panel via a "terminal" badge next to "link matrix
account". Consumes GET /api/agents/{name}/term/stream: unlike
@hive/agent's own useLiveStream (TermEnvelope-wrapped, history/backfill
dance), the swarm relay forwards one bare TermMsg per SSE event with no
envelope and no history endpoint -- useSwarmTermStream is a much
smaller hook for that shape (EventSource -> parse -> coalesce, nothing
to buffer/dedupe/backfill against).

Verified against a live SSE mock (screenshots in /agents/iris/state/screenshots/
3801-agents-detail-panel-terminal-badge.png and
3801-agent-term-page-live-rows.png -- real rows rendering through the
shared Row component, not just a build/typecheck pass).
2026-09-13 20:23:51 +02:00

3.4 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/shared/src/terminal/Row.tsx — moved out of packages/agent once swarm-ui became a second consumer of it): 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/shared/src/terminal/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.