From 69a3ca7469a71f764183f2ab9863cf4fb9e29bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Mon, 18 May 2026 19:25:50 +0200 Subject: [PATCH] docs: prune landed todos + refresh scratchpad + as-built terminal-rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 134 ++++++++++++++----- TODO.md | 5 - docs/terminal-rendering.md | 268 +++++++++++++------------------------ 3 files changed, 195 insertions(+), 212 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b135dea..d60dfe1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,19 +77,27 @@ hive-c0re/ host daemon + CLI (one binary, subcommand-dispatched) assets/ index.html, dashboard.css, app.js (include_str!) hive-fr0nt/ shared frontend-assets crate (browser only). - src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS - re-exports; both binaries `include_str!` them - and prepend to their per-page serving routes. + src/lib.rs pub const BASE_CSS / TERMINAL_CSS / TERMINAL_JS / + MARKED_JS re-exports; both binaries + `include_str!` them and prepend to their per- + page serving routes. assets/base.css Catppuccin palette + body typography (one source of truth, no per-page redeclaration). assets/terminal.css `.terminal-wrap` + `.live` + `.tail-pill` + `.row` / `details.row` styling for both - pages' lit log panes. + pages' lit log panes. Unified prefix-column + (padding-left + negative text-indent) so glyph + alignment is consistent across row kinds + a + `.md` block scope for marked-rendered bodies. assets/terminal.js `window.HiveTerminal.create(opts)`: scroll- sticky log + "↓ N new" pill + history backfill + SSE subscribe-buffer-snapshot- dedupe dance. Pages register a kind→renderer map; the terminal owns the lifecycle. + assets/marked.min.js vendored marked v4.0.2 UMD bundle. Per-agent + terminal uses the global `marked.parse` for + markdown bodies on send / recv / ask / answer + / assistant text rows. hive-ag3nt/ in-container harness crate; produces TWO binaries src/lib.rs re-exports + DEFAULT_SOCKET, DEFAULT_WEB_PORT @@ -141,7 +149,8 @@ read them à la carte. [`docs/web-ui.md`](docs/web-ui.md). - **"How does the per-agent terminal classify + colour events?"** → [`docs/terminal-rendering.md`](docs/terminal-rendering.md) - (taxonomy + known inconsistencies + a proposed coherence pass). + (as-built row taxonomy + layout contract + markdown + + extra-MCP fallback). - **"How does claude get its prompt and what tools does it have?"** → [`docs/turn-loop.md`](docs/turn-loop.md). - **"How do config changes flow from manager to operator to @@ -174,31 +183,96 @@ read them à la carte. In-flight or recent context that hasn't earned a section yet. Prune freely. -- **Just landed:** meta-flake overhaul. Each agent's applied - repo is a module-only flake (forwards every `inputs.*` - through to `agent.nix` as the `flakeInputs` module arg — - manager edits `inputs` to pull in external flakes like an - MCP server's own flake; the new sha lands in the agent's - own `flake.lock` and rolls up to meta's). A single - hive-c0re-owned repo at `/var/lib/hyperhive/meta/` - declares one input per agent and one - `nixosConfigurations.` output, wrapping the agent's - `nixosModules.default` with identity + `HIVE_PORT` / - `HIVE_LABEL` / `HIVE_DASHBOARD_PORT` / - `HIVE_OPERATOR_PRONOUNS`. Containers run against - `meta#`. Every approve uses two-phase staging - (prepare → build → finalize/abort) so meta's git log only - records successful deploys; failures + denials live as - annotated tags in applied. All meta operations - serialize behind a tokio mutex; stale `.git/index.lock` - is cleared on hive-c0re startup. Manager has `/applied` - + `/meta` RO-bound + the `applied` remote pre-wired in - every proposed repo. Migration runs idempotently on - startup (`HIVE_SKIP_META_MIGRATION=1` skips). Operator - pronouns are a NixOS module option - (`services.hive-c0re.operatorPronouns`, default - `"she/her"`); the harness substitutes them into the - system prompt at boot. +- **Just landed:** ctx + cost badges split. The per-agent + page now shows TWO chips — `ctx · N` (last inference's + prompt size = actual context window utilisation, parsed + from each `assistant` event's `.message.usage`; the + number to watch for compaction) and `cost · M` (sum + across every inference in the turn, the previous + behaviour now correctly labelled — tool-heavy turns + rebill the cached prefix per call and blow past the + model's window). Both fed by a single + `TokenUsageChanged { ctx, cost }` SSE event at turn-end + via `Bus::record_turn_usage`. `turn_stats` grew four + `last_*_tokens` columns (idempotent ALTER migration) so + cold-load seeds both badges from the most recent row. + Pre-migration rows yield no `ctx` seed (empty badge + until next turn) rather than a misleading zero. +- **Just landed:** per-agent terminal coherence pass. + Unified prefix column (padding-left + negative + text-indent so every row kind aligns); `
` + summaries drop the directional glyph and let CSS + `▸/▾` sit in the prefix column; turn boundaries + de-weighted (border-left rule only, no bold/margin/ + tint); stderr lines render orange `!`, + operator-initiated notes mauve italic, catch-all `.sys` + escalated to orange so unrecognised stream-json + surfaces. Message-bearing tool calls + (`send`/`ask`/`answer`/`recv`) render default-open + with markdown bodies via vendored `marked` v4.0.2 + (`hive-fr0nt::MARKED_JS`); assistant `text` rows also + markdown-rendered. Extra-MCP tools get a generic + args pretty-printer (`fmtArgsGeneric`) instead of + raw JSON. tool_use_id → name map carries through the + stream so `renderToolResult` knows when a result came + from `recv` and should default-open with markdown. + See `docs/terminal-rendering.md` for the as-built + taxonomy. Bonus: ctx badge seeded from `turn_stats` + on cold load via `Bus::seed_usage` so the chip paints + real numbers before the next turn finishes. +- **Just landed:** `open_threads` → `loose_ends` rename + (more honest about what the list is) + new + `cancel_loose_end(kind, id)` MCP tool on both + surfaces. `kind = "question"` posts a `[cancelled by + ]` answer to unblock the asker; `kind = + "reminder"` hard-deletes before fire. Auth: sub-agent + must own the row (`asker == self` / `owner == self`); + manager bypasses for hive-wide cleanup. `LooseEnd` + enum gained a `Reminder { id, owner, message, due_at, + age_seconds }` variant; sub-agent flavour filters by + owner, manager unfiltered. Shared dispatch in + `hive-c0re/src/questions.rs::handle_cancel_loose_end`. + Per-agent web UI's `/api/open-threads` → + `/api/loose-ends`. Closes the agent-side "I have no + way to cancel what I queued" friction from the + ergonomics wishlist. +- **Just landed:** `whoami` MCP tool on both surfaces — + returns `{ name, role, pronouns, hyperhive_rev }`. + Lets an agent self-identify without scraping its own + system prompt; pronouns are pulled from the + `HIVE_OPERATOR_PRONOUNS` env that the system prompt + also substitutes, so the two stay in sync. +- **Just landed:** reminder delivery failures persist + + surface. The 5s scheduler now records the failure + reason on the `reminders` row instead of silently + dropping; the web UI surfaces them in the loose-ends + section so the owner agent can see "this reminder + never landed because the target was destroyed" without + reading journald. +- **Just landed:** path linkify. The broker tags every + message body server-side with `file_refs` at ingest + via `/api/state-file/check` (validates each path, + attaches container→host resolution). Last segment + must look like `name.ext` so directory mentions don't + fire spurious links. Dashboard renders refs as + collapsible path-preview blocks below the message + body; per-agent terminal picks them up from the same + field. Server-side validation means the JS doesn't + re-walk allow-lists on every render. +- **Just landed:** tombstones + meta_inputs as + `DashboardEvent`s. Closes the last two refetch loops + on the dashboard side — `purge-tombstone` and + `meta-update` POSTs now flip to 200 with + `data-no-refresh`. The 5s `/api/state` poll is gone + entirely; everything event-driven. +- **Just landed:** per-agent UI gained an open-threads + section (questions + approvals + reminders pending + against this agent) + the container row on the + dashboard gained a `⏰ N` pending-reminder count + chip. task_started / task_notification stream-json + events now pretty-render in the terminal with the + `⌁` glyph so subagent (Task tool) activity is + visually distinct from main-session tool calls. - **Just landed:** per-agent extra MCP servers via the `hyperhive.extraMcpServers.` NixOS option in `agent.nix`. Declares `{ command, args, env, diff --git a/TODO.md b/TODO.md index c4c2b86..56c0ce6 100644 --- a/TODO.md +++ b/TODO.md @@ -18,13 +18,11 @@ ## Dashboard -- ~~**Per-agent terminal coherence pass**~~ ✓ landed — unified prefix column (padding-left + negative text-indent so every row kind aligns), `
` summaries drop directional glyph (CSS `▸/▾` sits in prefix column instead), turn boundaries de-weighted (border-left only, no bold/margin/tint), stderr lines render orange `!`, operator-initiated notes mauve italic, catch-all `.sys` escalated to orange so unrecognised stream-json surfaces. Message-bearing tool calls (`send`/`ask`/`answer`/`recv`) render default-open with markdown bodies via vendored `marked` v4.0.2; assistant text also markdown-rendered. Extra-MCP tools get a generic args pretty-printer instead of raw JSON. See [`docs/terminal-rendering.md`](docs/terminal-rendering.md) for the as-built taxonomy. - **Delivered-reminder rollups**: per-agent delivered-count chip (last 24h) + histogram of attempts-vs-successes on the container row. Needs `Broker::count_delivered_reminders_since(agent, ts)` (cheap COUNT against the `reminders` table, `WHERE agent = ?1 AND sent_at >= ?2`). ## Security - **Privsep the dashboard from the privileged daemon**: hive-c0re runs as root (it has to — `nixos-container` create / start / destroy, the meta git repo, every per-agent bind mount). The HTTP server lives in the same process, so every read-endpoint (`/api/state-file`, `/api/journal/{name}`, `/api/agent-config/{name}`) is one allow-list bug away from serving arbitrary host files. Split the architecture: keep the privileged daemon doing lifecycle + git + ipc, run the web UI as an unprivileged user that talks to the daemon over a unix socket with a narrow request surface (`ReadAgentStateFile { agent, rel_path }` etc.). The unprivileged process can't read `/etc/shadow` even if every check in `get_state_file` is bypassed — it doesn't have the bits. Container-lifecycle POSTs (`/restart`, `/destroy`, etc.) become forwarded RPCs the privileged side authorises on its terms. -- ~~**Defense in depth on `get_state_file`**~~ ✓ landed — `resolve_state_path` (shared by `get_state_file` + `scan_validated_paths`) now: (a) walks each path component below the matched root via `symlink_metadata` and refuses outright if any is a symlink (so an agent planting `ln -s /var/lib/hyperhive/agents/other/state/secret /agents/me/state/peek` can't have its target proxied — `canonicalize` would happily resolve past the allow-list check otherwise); (b) refuses any `..` traversal below the root with a friendlier error than "escapes allow-list"; (c) refuses files whose mode isn't world-readable (`mode & 0o004 == 0`) so a 0600 file inside `state/` doesn't leak via the endpoint; (d) bundles the metadata fetch into the resolve helper so callers don't restat. New tests in `hive-c0re/src/dashboard.rs::tests` cover leaf-symlink, mid-path-symlink, `..` traversal, and plain-dir-passthrough cases. ## Harness Ergonomics (agent-side wishlist) @@ -40,7 +38,6 @@ how often the friction bites in normal use. into the prompt builder in `hive-ag3nt::turn.rs`. Even better: add a one-shot `recv_batch(max: u32)` MCP tool that returns up to `max` pending messages in a single round-trip. -- ~~**Self-management of own asks + reminders**~~ ✓ landed — unified with `get_loose_ends` (renamed from `get_open_threads` per the naming pass). `LooseEnd` enum (renamed from `OpenThread`) gained a `Reminder { id, owner, message, due_at, age_seconds }` variant (sub-agent flavour filters by `owner == self`; manager unfiltered). New `mcp__hyperhive__cancel_loose_end(kind, id)` on both surfaces — `kind` is `"question"` (asker gets `[cancelled by ]` answer, unblocks) or `"reminder"` (hard-deleted before fire). Auth: sub-agent must own the row; manager bypasses for hive-wide cleanup. New helpers `OperatorQuestions::cancel` + `Broker::cancel_reminder_as` push the auth check down so both flavours stay aligned. Shared dispatch in `hive-c0re/src/questions.rs::handle_cancel_loose_end`. Per-agent web UI's `/api/open-threads` → `/api/loose-ends` too, with reminder-row rendering added. - **Optional `in_reply_to: ` on send** — pure wire addition; no behavioural change. The dashboard could render conversation threads (already wants this for the agent-to-agent question UI in the @@ -57,8 +54,6 @@ how often the friction bites in normal use. ## Harness Behaviour -- **Persist + cold-load current context size on the per-agent page**: the `ctx-badge` (Claude Code's bottom-right "N tokens" indicator) currently only populates after the first `TokenUsageChanged` SSE event arrives, which is the *next* turn — until then the badge is empty. Operator can't see "this agent is at 78% context" before deciding to manually compact / reset / message it. Last known token usage should be persisted (likely a small `/state/hyperhive-token-usage` blob, or a row in turn_stats already has it — pull last row's totals on cold load) and returned by `/api/state` so the badge paints with real numbers on first render. - - **Auto session-reset when context is large and cache is cold**: today every turn uses `--continue`, so a long-lived agent carries its entire transcript forward indefinitely. When the next turn's context is above some threshold (rough starting point: ~50% of the session limit — hive startup alone burned ~15%, so the headroom disappears fast) *and* the prompt cache is no longer warm (last turn ended past the cache TTL), it's cheaper to start fresh than to re-send the whole history uncached. Open question: drop `--continue` vs. trigger `--compact` first — needs measurement of what each actually costs (uncached re-read of N tokens vs. a compact turn's own token spend + the post-compact uncached re-read). Decision should be data-driven, not guessed. Needs: a context-size estimate per turn (turn_stats already tracks token usage), a cache-warmth heuristic (time since last turn vs. cache TTL), and a one-shot fresh-session path in `turn.rs` mirroring the existing `↻ new session` button. ## Bugs diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index a7f3633..51eedf4 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -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 .` 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 .` +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, 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` `
` | `→ Name path · +N` | cyan, body is diff | rich tool_use (Write/Edit) | renderRichToolUse | -| `.tool-use` `
` | `→ send → to · headline` | cyan, body is text | mcp__hyperhive__send | renderRichToolUse | -| `.tool-result` | `← ` | muted | short `user.content[].tool_result` (≤120c) | stream-json | -| `.tool-result-block` `
` | `▸ ← Nl · headline` | muted, body is text | long `tool_result` (>120c) | stream-json | -| `.tool-use` | `⌁ task started · [type]` | cyan | claude Task-tool subagent event | renderTaskEvent | -| `.turn-end-ok` / `.turn-end-fail` / `.tool-result` | `⌁ task ✓/✗/◌ · · → ` | green / red / muted | claude Task-tool result | renderTaskEvent | -| `.note` | `· ` | 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 ← ` | 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` | -## 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 `
` 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 `
`. +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: "` — 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 `
`. 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 - `
` summary would help (the `▸` is present in - `.tool-result-block` summaries but absent in tool-use - `
` 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 `
` - 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 `
` 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: 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 `
`). 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____` (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.