docs: prune landed todos + refresh scratchpad + as-built terminal-rendering

todo: drop landed entries (terminal coherence pass, get_state_file
defense-in-depth, self-management of loose ends, persist+cold-load
ctx-badge).

claude.md:
- scratchpad: new just-landed entries for ctx+cost badge split,
  terminal coherence pass, loose_ends rename + cancel_loose_end,
  whoami, reminder failure persistence, path linkify, tombstones+
  meta_inputs events, agent open-threads section + container pending-
  reminder chip + task event rendering. drops the meta-flake
  "just landed" — structural facts live in the file map +
  approvals.md, the narrative was no longer load-bearing.
- file map: hive-fr0nt now lists MARKED_JS + marked.min.js + the
  unified prefix-column terminal.css update.
- reading paths: terminal-rendering.md description matches as-built.

docs/terminal-rendering.md: rewritten as as-built reference. layout
contract documents the padding-left + negative text-indent prefix
column + how details inherits it. row taxonomy reflects current
state (notes split into .note / .note.stderr / .note.op; .sys is
amber; recv tool_results default-open with markdown body via
tool_use_id correlation; rich send/ask/answer renderers). new
sections for renderer dispatch flow, markdown integration,
fmtArgsGeneric extra-MCP fallback, dashboard msgrow text-indent
reset.
This commit is contained in:
müde 2026-05-18 19:25:50 +02:00
parent 2b38805c00
commit 69a3ca7469
3 changed files with 195 additions and 212 deletions

View file

@ -1,196 +1,110 @@
# Per-agent terminal: row taxonomy + inconsistencies
# Per-agent terminal: row taxonomy (as built)
Snapshot of how the per-agent web UI's live pane renders each
event kind today, written up so the next coherence pass has a
reference to work from. Source of truth lives in
`hive-ag3nt/assets/app.js` (`renderStream`,
`fmtToolUse`, `renderRichToolUse`, `renderToolResult`,
`renderTaskEvent`) + `hive-fr0nt/assets/terminal.css` (the
shared `.live .<class>` styling).
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, bold, top-margin, amber left rule | `LiveEvent::TurnStart` | harness wake |
| `.turn-body` | (inline under turn-start) | fg dimmed 85% | same | the wake-prompt body |
| `.turn-end-ok` | `✓ turn ok` | green, green left rule | `LiveEvent::TurnEnd { ok: true }` | harness |
| `.turn-end-fail` | `✗ turn fail — note` | red, red left rule | `LiveEvent::TurnEnd { ok: false }` | harness |
| `.text` | none | fg/white, indented | claude `assistant.content[].text` | stream-json |
| `.thinking` | `·` or `· thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json |
| `.tool-use` | `→ Name args…` | cyan | `assistant.content[].tool_use` | stream-json |
| `.tool-use` `<details>` | `→ Name path · +N` | cyan, body is diff | rich tool_use (Write/Edit) | renderRichToolUse |
| `.tool-use` `<details>` | `→ send → to · headline` | cyan, body is text | mcp__hyperhive__send | renderRichToolUse |
| `.tool-result` | `← <txt>` | muted | short `user.content[].tool_result` (≤120c) | stream-json |
| `.tool-result-block` `<details>` | `▸ ← Nl · headline` | muted, body is text | long `tool_result` (>120c) | stream-json |
| `.tool-use` | `⌁ task <id> started · <desc> [type]` | cyan | claude Task-tool subagent event | 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 | `LiveEvent::Note` (harness chatter, /cancel /compact /new-session, stderr lines, etc.) | harness |
| **`.sys`** | `· {json…}` | **muted** | **anything `renderStream` doesn't recognise** | catch-all |
| `.result` | (defined, never emitted today) | green | — | — |
| Banner shimmer | mauve | turn in flight (ref-counted) | `setBannerActive` |
| `.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` |
## Where the inconsistencies live
## Renderer dispatch
1. **Glyph vocabulary drifts**:
- tool_use uses `→`
- tool_result uses `←`
- thinking uses `·`
- notes use `·`
- turn-end ok/fail use `✓ / ✗`
- turn-start uses `◆`
- task events use `⌁`
- The `·` glyph is overloaded across thinking, notes, sys.
`renderStream(v, api)` walks each stream-json line:
2. **What gets a `<details>` block vs a flat row** is per-tool
ad-hoc: Write/Edit always expand, send always expands, every
other tool_use is flat regardless of input size.
`tool_result` is flat if ≤120 chars otherwise `<details>`.
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).
3. **Stderr handling**: claude's stderr lines come through as
`LiveEvent::Note` with `text: "stderr: <line>"` — they
render as muted `· stderr: …`, identical styling to harness
notes about /compact / /model. No red-tinted "this is an
error" affordance for stderr.
## Markdown
4. **Catch-all `.sys` rows** are visually identical to `.note`
rows — both muted, both `·` prefix. They look like normal
notes despite usually being "an event renderStream couldn't
classify". Unmatched stream shapes (rate limit warnings under
odd type/subtype combos, future claude additions, etc.)
silently fall through.
`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.
5. **`.tool-use` ranges from one-liner (`Read foo.md`) to
multi-page collapsed diff** — same color, same prefix glyph,
very different visual weight. A small marker on the collapsed
`<details>` summary would help (the `▸` is present in
`.tool-result-block` summaries but absent in tool-use
`<details>` summaries).
## Extra-MCP tools
6. **Cancel/compact/new-session notes** are styled the same as
autonomous harness chatter; nothing flags them as "operator
initiated."
`fmtArgsGeneric(name, input)` is the fallback when a tool
isn't in the built-in `fmtToolUse` switch:
7. **Turn-start / turn-end are visually overweight**: the
triggering event row + the closing row both get bold text,
top margins, and a full coloured background tint. The
coloured left rule alone already says "this is a turn
boundary" — the heavy chrome adds noise without information.
Drop the bold/margin/tint, keep the left rule.
- 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
8. **Left-alignment is incoherent across row types**: the `▸`
disclosure marker on expandable rows doesn't sit in the
same column as flat-row prefix glyphs (`→`, `←`, `·`), so
the prefix column wanders by row kind. Expandable tool_use
(`→ Name …`) and expandable tool_result (`▸ ← Nl …`) use
different layouts from each other too. Pick one prefix
column and align every row kind into it; the disclosure
marker should be visually inside that column, not pushed
to the side.
9. **Continuation lines aren't inset**: when a row's body
wraps (e.g. `→ foo bar baz quux …`), the second visual
line starts at column 0 rather than under the `f` of
`foo`, so wrapped content blurs into the next row. Want a
`text-indent` / hanging-indent rule so continuation lines
align with the start of the body, not the prefix glyph.
10. **Most message-bearing rows are collapsed by default**
even when the body is the whole point: `send`, `recv`,
`ask`, and friends hide their text behind a `<details>`
summary. The summary headline is rarely enough context.
Want these expanded by default, with collapse reserved
for genuinely heavy payloads (multi-page diffs, long
tool_result blocks).
11. **Tools from extra MCP servers aren't pretty-printed**:
`renderRichToolUse` only special-cases the built-in
hyperhive tools (Write/Edit/send). Anything coming from
`extraMcpServers` (matrix, bitburner, …) falls through
to the generic `→ Name args…` JSON dump. Either a
plugin-style hook in the renderer map, or at minimum a
nicer generic args-pretty-printer that handles common
shapes (single string arg, single dict arg, etc.).
12. **No markdown rendering on message bodies**: `send` /
`recv` / `ask` / `answer` / agent text content all
arrive as markdown-flavoured prose (bullets, fenced
code, bold, links) but render as raw text. Want a
minimal markdown pass — at least lists, fenced code,
inline code, bold/italic, links — applied to message
bodies and probably to the `assistant.content[].text`
`.text` row.
## Suggested coherence pass
Pick one scheme and audit all renderers to match. A concrete
proposal:
- **`→` cyan**: outbound action (tool_use, send)
- **`←` muted**: inbound result (tool_result)
- **`◆` amber**: turn framing (turn_start)
- **`✓ / ✗`**: success / failure, green / red (turn_end,
task_notification)
- **`⌁` mauve**: subagent / background event (task_*)
- **`·` muted**: ambient note, italic for thinking
- **`!` orange**: caught error (stderr lines, .sys catch-all
that landed something the renderer didn't recognise)
Plus: every tool_use `<details>` summary gets `▸` so collapsed
content is visually announced. Operator-initiated notes get a
distinct prefix (`op·` or similar) so they're easier to spot in
the scrollback.
The `.sys` catch-all should escalate visually — a louder
"unrecognised event" rendering surfaces silently-dropped event
shapes for future fix-up rather than hiding them in the muted
note stream.
### Layout rules to apply uniformly
- **One prefix column for every row kind** — flat rows and
expandable rows alike. The disclosure marker (`▸`) lives
inside that column, not as a separate gutter, so glyph
alignment doesn't shift between row types.
- **Hanging indent on wrapped bodies** so continuation lines
start under the first character of the body, not under the
prefix glyph. Probably `display: grid` with
`grid-template-columns: <prefix-col> 1fr` per row, or
`text-indent`/`padding-left` with negative `text-indent`.
- **Turn boundaries are rule-only** — drop the bold + margin
+ tint on `.turn-start` / `.turn-body` / `.turn-end-*`.
The coloured left rule alone carries the boundary signal.
- **Default-expanded message rows**: `send`, `recv`, `ask`,
`answer`, and short-ish `text` rows render their body
inline (no `<details>`). Reserve collapse for genuinely
heavy bodies — multi-page diffs, long tool_results,
thinking blocks past N lines.
### Renderer surface for extra MCP tools
`renderRichToolUse` currently switches on hard-coded tool
names. Replace with a registry keyed by
`mcp__<server>__<tool>` (and the built-in claude tool names)
that maps to a `(toolUse) -> Node` renderer. Per-agent extra
MCPs can register their own renderers via a small JS hook
(loaded the same way `extra-mcp.json` is loaded server-side);
the fallback is a generic args-pretty-printer that handles
single-string and single-dict shapes nicely instead of dumping
raw JSON.
### Markdown rendering
Apply a minimal markdown pass to message bodies (`send` /
`recv` / `ask` / `answer`) and to assistant `text` rows.
Scope: paragraphs, lists, fenced + inline code, bold/italic,
links. No HTML passthrough, no images, no tables —
explicitly bounded so we don't import a kitchen-sink parser
into the per-agent page. A small handwritten pass or a tiny
dep (e.g. `micromark` / `marked`) both work.
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 beast:
broker messages render as `.msgrow` grid lines (ts / arrow /
from / → / to / body) with separate styling. The current file
focuses only on the per-agent terminal.
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.