docs(terminal-rendering): re-attribute dispatch logic to stream_enrich.rs, fix Write/TodoWrite/Bash dead-path claims
Verified against hive-agent/src/stream_enrich.rs and frontend/packages/agent/src/app.js: - per-tool icon/summary/category classification moved server-side to stream_enrich.rs::enrich() a while ago; fmtToolUse/fmtArgsGeneric/toolIcon no longer exist in app.js -- the client now just dispatches on the backend-stamped _icon/_summary/_category/_body/_body_type fields. Rewrote the 'Renderer dispatch' and 'Salient-arg formatting' (was 'fmtToolUse patterns') sections to match, and retargeted the per-tool table's 'Everything else' row to fmt_args_generic. - Write is no longer a rich diff row -- is_rich_tool() doesn't include it, and fmt_builtin_tool formats it identically to Read (flat, no count). Only Edit still gets the rich diff. - TodoWrite and built-in Bash aren't in ALLOWED_BUILTIN_TOOLS (mcp_config.rs) any more than Task is -- flagged both as dead paths for consistency with the existing Task annotation. - fixed stray 'ask* has an inline answer form' table note that contradicted the doc's own later 'Inline ask-operator answer' section (no such form exists). - terminal.css path: frontend/packages/shared/src/terminal/terminal.css, not .../shared/src/terminal.css; marked npm dep is v18.x, not v4.x.
This commit is contained in:
parent
4388719b76
commit
066cd712ea
1 changed files with 78 additions and 47 deletions
|
|
@ -1,11 +1,16 @@
|
||||||
# Per-agent terminal: row taxonomy (as built)
|
# Per-agent terminal: row taxonomy (as built)
|
||||||
|
|
||||||
Snapshot of how the per-agent web UI's live pane renders each
|
Snapshot of how the per-agent web UI's live pane renders each
|
||||||
event kind today. Source of truth lives in
|
event kind today. The per-tool icon/summary/category (and, for a
|
||||||
`frontend/packages/agent/src/app.js` (`renderStream`, `fmtToolUse`,
|
few rich tools, the expandable body) are pre-computed server-side by
|
||||||
|
`hive-agent/src/stream_enrich.rs::enrich` and stamped onto the
|
||||||
|
stream-json value as `_icon`/`_summary`/`_category`/`_body`/
|
||||||
|
`_body_type` before SSE delivery, so the frontend just dispatches on
|
||||||
|
those fields instead of re-deriving them. Frontend source of truth
|
||||||
|
lives in `frontend/packages/agent/src/app.js` (`renderStream`,
|
||||||
`renderRichToolUse`, `renderToolResult`, `renderTaskEvent`,
|
`renderRichToolUse`, `renderToolResult`, `renderTaskEvent`,
|
||||||
`mdNode`, `detailsOpenMd`, `fmtArgsGeneric`) +
|
`mdNode`, `detailsOpenMd`) +
|
||||||
`frontend/packages/shared/src/terminal.css` (the shared
|
`frontend/packages/shared/src/terminal/terminal.css` (the shared
|
||||||
`.live .<class>` styling) + the `marked` npm package (markdown).
|
`.live .<class>` styling) + the `marked` npm package (markdown).
|
||||||
|
|
||||||
## Layout contract
|
## Layout contract
|
||||||
|
|
@ -56,8 +61,8 @@ parent's negative pull.
|
||||||
| `.turn-time` | `· HH:MM:SS` on turn-start; `· HH:MM:SS · <dur>` on turn-end (child span) | muted, smaller | per-event `ts` (unix seconds) on the live frame + history row | harness |
|
| `.turn-time` | `· HH:MM:SS` on turn-start; `· HH:MM:SS · <dur>` on turn-end (child span) | muted, smaller | per-event `ts` (unix seconds) on the live frame + history row | harness |
|
||||||
| `.text` | (no prefix; markdown body) | fg | claude `assistant.content[].text` | stream-json |
|
| `.text` | (no prefix; markdown body) | fg | claude `assistant.content[].text` | stream-json |
|
||||||
| `.thinking` | `💭 thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json |
|
| `.thinking` | `💭 thinking …` | muted, italic | claude `assistant.content[].thinking` | stream-json |
|
||||||
| `.tool-use` (flat) | `<icon> Name args…` | cyan | tool_use w/o rich renderer; `<icon>` from `toolIcon(name)`: 📤 send · 📥 recv · ❓ ask · ⏰ remind · 🏷️ set_status · 🪢 loose-ends · ✂️ cancel_loose_end · ℹ️ get_agent_meta · ✅ ack_until · 📜 get_logs/get_host_journal · ↻ restart · ⏹️ kill · ▶️ start · 🔄 update · 📋 list_containers/list_rooms/list_room_members/list_invites · 📖 read_room/Read · 👁️ mark_read · 🛑 bash kill · 🖥️ bash other · 💬 matrix send/reply/dm · 📦 request_* · ⏱️ schedule · 🔧 default | stream-json |
|
| `.tool-use` (flat) | `<icon> Name args…` | cyan | tool_use w/o rich renderer; `<icon>` from the backend's `tool_icon(name)` (`stream_enrich.rs`): 📤 send · 📥 recv · ❓ ask · ⏰ remind · 🏷️ set_status · 🪢 loose-ends · ✂️ cancel_loose_end · ℹ️ get_agent_meta · ✅ ack_until · 📜 get_logs/get_host_journal · ↻ restart · ⏹️ kill · ▶️ start · 🔄 update · 📋 list_containers/list_rooms/list_room_members/list_invites · 📖 read_room/Read · 👁️ mark_read · 🛑 bash kill · 🖥️ bash other · 💬 matrix send/reply/dm · 📦 request_* · ⏱️ schedule · 🔧 default | stream-json |
|
||||||
| `.tool-use` `<details>` | `💾/✏️ Write/Edit <path> · +N` (no `→`) | cyan, body is +/- diff | `renderRichToolUse` Write/Edit | stream-json |
|
| `.tool-use` `<details>` | `✏️ Edit <path> · -N +N` (no `→`) | cyan, body is +/- diff | `renderRichToolUse` 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-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` (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>` | `Nl · headline` | muted, body is text | long generic `tool_result` | stream-json |
|
||||||
|
|
@ -87,42 +92,65 @@ suffix, so the terminal degrades cleanly against older event shapes.
|
||||||
|
|
||||||
## Renderer dispatch
|
## Renderer dispatch
|
||||||
|
|
||||||
`renderStream(v, api)` walks each stream-json line:
|
`renderStream(v, api)` walks each stream-json line. Most of the
|
||||||
|
per-event classification it used to do itself is now pre-computed
|
||||||
|
server-side by `hive-agent/src/stream_enrich.rs::enrich` (stamped
|
||||||
|
onto the value as `_category`/`_summary`/`_icon`/`_body`/
|
||||||
|
`_body_type` at SSE-emit time, for both the live tail and history
|
||||||
|
replay) — the client mostly just dispatches on those fields rather
|
||||||
|
than re-deriving them from raw claude field names:
|
||||||
|
|
||||||
1. Drops `system/init`, `rate_limit_event`, `result` (noise /
|
1. `v._category === 'drop'` → dropped without rendering. Covers the
|
||||||
handled elsewhere — `result` powers the `cost` badge).
|
top-level `result` / `rate_limit_event` types (`result` powers the
|
||||||
1a. `system/thinking_tokens` (claude streams a running
|
`cost` badge elsewhere) and the `system` subtypes `init` /
|
||||||
|
`result` / `rate_limit_event`.
|
||||||
|
1a. `system` events with `_category === 'thinking_tok'`
|
||||||
|
(`subtype == "thinking_tokens"`; claude streams a running
|
||||||
`estimated_tokens` counter while thinking — many per turn) →
|
`estimated_tokens` counter while thinking — many per turn) →
|
||||||
collapses into a **single** `🧠 thinking … ~N tokens` `.note`
|
collapses into a **single** `🧠 thinking … ~N tokens` `.note`
|
||||||
row that updates in place. Consecutive ticks reuse the row only
|
row that updates in place, text taken verbatim from the
|
||||||
|
backend-computed `_summary`. Consecutive ticks reuse the row only
|
||||||
while it's still the last one rendered (`nextElementSibling ==
|
while it's still the last one rendered (`nextElementSibling ==
|
||||||
null`); any other event after it makes the next tick start a
|
null`); any other event after it makes the next tick start a
|
||||||
fresh row. Avoids a note-per-tick scrollback flood.
|
fresh row. Avoids a note-per-tick scrollback flood.
|
||||||
1b. `system/plugin_install` → muted note `⚙ plugin install · loading…`
|
1b. `system/plugin_install` (matched on `subtype`, not `_category`,
|
||||||
(on `started`) or `⚙ plugin install · ✓ done` (on `completed`).
|
so start/complete can coalesce into one row) → muted note
|
||||||
Emitted in pairs: started fires before the plugin loads, completed
|
`⚙ plugin install · loading…` (on `started`) or
|
||||||
fires when it's ready. The `uuid` links the pair.
|
`⚙ plugin install · ✓ done` (on `completed`), text from
|
||||||
1c. `system/commands_changed` → collapsible `.note` details row showing
|
`_summary`. Emitted in pairs: started fires before the plugin
|
||||||
the new slash-command count (`⚙ commands changed · N available`).
|
loads, completed fires when it's ready.
|
||||||
Expanding reveals each `/name` and its aliases. Fires after
|
1c. `system/status` (matched on `subtype`) → muted note from
|
||||||
`plugin_install` when a plugin registers new commands.
|
`_summary`, except while the harness's local `turn_state` is
|
||||||
1d. `system/compact_boundary` → muted note showing compaction summary:
|
`compacting`: the client overrides the text with an elapsed-time
|
||||||
`⚙ compact · <trigger> · <pre>→<post> tokens · <dur>`. Fields are
|
counter (`⚙ compact · <N>s…`) computed client-side from
|
||||||
guarded individually — a missing field is silently omitted. Trigger
|
`stateSince`, since the backend can't know client wall-clock time
|
||||||
is `"manual"` (operator `/compact`) or `"auto"`.
|
at emit time.
|
||||||
1e. Other `system/` subtypes → muted note `⚙ <subtype>`.
|
1d. `_category === 'details'` (currently just `system/commands_changed`)
|
||||||
|
→ collapsible `.note` details row: `_summary` as the header
|
||||||
|
(`⚙ commands changed · N available`), `_body` (one `/name` per
|
||||||
|
line) as the expandable content.
|
||||||
|
1e. Other `system/` subtypes (e.g. `compact_boundary`, `api_retry`,
|
||||||
|
`api_error`, or an unrecognised subtype) → `_category === 'note'`,
|
||||||
|
rendered as a single muted note from `_summary` — computed by
|
||||||
|
`system_fields()` in `stream_enrich.rs` (e.g. `compact_boundary`
|
||||||
|
→ `⚙ compact · <trigger> · <pre>→<post> tokens · <dur>` with each
|
||||||
|
field guarded individually; an unrecognised subtype falls back to
|
||||||
|
`⚙ <subtype>`).
|
||||||
2. `subtype == "task_started" | "task_notification"` →
|
2. `subtype == "task_started" | "task_notification"` →
|
||||||
`renderTaskEvent` (subagent activity gets the `⌁` glyph).
|
`renderTaskEvent` (subagent activity gets the `⌁` glyph).
|
||||||
3. `type == "assistant"` → walk `message.content[]`:
|
3. `type == "assistant"` → walk `message.content[]`:
|
||||||
- `text` → `.text` row with a markdown body via `mdNode`.
|
- `text` → `.text` row with a markdown body via `mdNode`.
|
||||||
- `thinking` → `.thinking` row.
|
- `thinking` → `.thinking` row.
|
||||||
- `tool_use` → record `id → name` in `toolNameById`, try
|
- `tool_use` → record `id → name` in `toolNameById`. The backend
|
||||||
`renderRichToolUse` (Write/Edit/send/ask/answer get
|
stamps every `tool_use` entry with `_icon` + `_summary` (via
|
||||||
custom renderings); on miss fall through to a flat
|
`fmt_tool_use()` in `stream_enrich.rs` — see [salient-arg
|
||||||
`.tool-use` row with `fmtToolUse → fmtArgsGeneric`.
|
formatting](#salient-arg-formatting) below) and, for a fixed set
|
||||||
`fmtToolUse` surfaces the salient arg per built-in tool
|
of tools, `_category: "rich"` + `_body`/`_body_type`. When
|
||||||
(see [`fmtToolUse` patterns](#fmttooluse-patterns) below);
|
`_category === "rich"`, `renderRichToolUse` dispatches on
|
||||||
`fmtArgsGeneric` handles everything else.
|
`_body_type` (`"diff"` → `api.detailsDiff`, `"markdown"` →
|
||||||
|
`detailsOpenMd`, else `api.details`) to build the expandable
|
||||||
|
row; otherwise it falls through to a flat `.tool-use` row using
|
||||||
|
`_icon` + `_summary` as-is — no per-tool JS.
|
||||||
4. `type == "user"` → walk `message.content[]` for
|
4. `type == "user"` → walk `message.content[]` for
|
||||||
`tool_result`; `renderToolResult` correlates via
|
`tool_result`; `renderToolResult` correlates via
|
||||||
`tool_use_id → toolNameById` to default-open `recv`
|
`tool_use_id → toolNameById` to default-open `recv`
|
||||||
|
|
@ -130,27 +158,29 @@ suffix, so the terminal degrades cleanly against older event shapes.
|
||||||
long = collapsed details.
|
long = collapsed details.
|
||||||
5. Unrecognised shape → `.sys` row (amber, `!` glyph).
|
5. Unrecognised shape → `.sys` row (amber, `!` glyph).
|
||||||
|
|
||||||
### `fmtToolUse` patterns
|
### Salient-arg formatting
|
||||||
|
|
||||||
The `short` name strips the `mcp__hyperhive__` / `mcp__bash__` /
|
Server-side (`fmt_tool_use()` and its per-tool-family helpers in
|
||||||
`mcp__matrix__` prefix and appends `*` (e.g. `recv*`, `run*`,
|
`hive-agent/src/stream_enrich.rs`), computed into `_summary` and
|
||||||
`send_message*`). Unprefixed tools (Read, Write, etc.) keep their
|
read verbatim by the client. The `short` name strips the
|
||||||
name as-is.
|
`mcp__hyperhive__` / `mcp__bash__` / `mcp__matrix__` prefix and
|
||||||
|
appends `*` (e.g. `recv*`, `run*`, `send_message*`). Unprefixed
|
||||||
|
tools (Read, Write, etc.) keep their name as-is.
|
||||||
|
|
||||||
| Tool | Rendered as |
|
| Tool | Rendered as |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| **Claude built-ins** | |
|
| **Claude built-ins** | |
|
||||||
| `Read` | `Read <path>` |
|
| `Read` | `Read <path>` |
|
||||||
| `Write` | rich diff row `Write <path> · +N` |
|
| `Write` | flat, same shape as Read: `Write <path>` — no diff/count (`content` can be megabytes and is one-sided; open the file to inspect it) |
|
||||||
| `Edit` | rich diff row `Edit <path> · -N +N` |
|
| `Edit` | rich diff row `Edit <path> · -N +N` (just `+N` for a pure insert, i.e. empty `old_string`) |
|
||||||
| `Glob` | `Glob <pattern>` |
|
| `Glob` | `Glob <pattern>` |
|
||||||
| `Grep` | `Grep <pattern>` |
|
| `Grep` | `Grep <pattern>` |
|
||||||
| `Bash` | `Bash [bg] $ <cmd>` (also rich renderer for full body) |
|
| `Bash` | `Bash [bg] $ <cmd>` (dead path — built-in `Bash` isn't in the agent allow-list either; shell execution goes through `mcp__bash__run` / `run*` below instead) |
|
||||||
| `TodoWrite` | `TodoWrite (N items)` |
|
| `TodoWrite` | `TodoWrite (N items)` (dead path — `TodoWrite` isn't in the agent allow-list; its state lives in claude's in-process session and evaporates on `/compact`, so agents plan in `/state` notes instead) |
|
||||||
| **Core hyperhive** | |
|
| **Core hyperhive** | |
|
||||||
| `send*` | rich renderer: `send* → to · NL` (default-open body) |
|
| `send*` | rich renderer: `send* → to · NL` (default-open body) |
|
||||||
| `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` |
|
| `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` |
|
||||||
| `ask*` | rich renderer: `ask* → to` (inline answer form for operator) |
|
| `ask*` | rich renderer: `ask* → to` (no inline answer form — see [Inline ask-operator answer](#inline-ask-operator-answer)) |
|
||||||
| `answer*` | rich renderer: `answer* #id` |
|
| `answer*` | rich renderer: `answer* #id` |
|
||||||
| `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` |
|
| `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` |
|
||||||
| `set_status*` | `set_status* "text"` |
|
| `set_status*` | `set_status* "text"` |
|
||||||
|
|
@ -183,12 +213,12 @@ name as-is.
|
||||||
| `join_room*/open_dm*` | `join_room* room` / `open_dm* @user` |
|
| `join_room*/open_dm*` | `join_room* room` / `open_dm* @user` |
|
||||||
| `invite_user*` | `invite_user* @user → room` |
|
| `invite_user*` | `invite_user* @user → room` |
|
||||||
| `download_file*` | `download_file* room` |
|
| `download_file*` | `download_file* room` |
|
||||||
| **Everything else** | `fmtArgsGeneric` — see [Extra-MCP tools](#extra-mcp-tools) |
|
| **Everything else** | `fmt_args_generic` — see [Extra-MCP tools](#extra-mcp-tools) |
|
||||||
|
|
||||||
## Markdown
|
## Markdown
|
||||||
|
|
||||||
`mdNode(text)` wraps `marked.parse(text)` (the `marked` v4.x npm
|
`mdNode(text)` wraps `marked.parse(text)` (the `marked` npm dep,
|
||||||
dep, bundled by esbuild into the page's `app.js`) in a `<div
|
bundled by esbuild into the page's `app.js`) in a `<div
|
||||||
class="md">`. CSS in `terminal.css` scopes paragraph / code /
|
class="md">`. CSS in `terminal.css` scopes paragraph / code /
|
||||||
list / blockquote / link styling under `.live .row .md` so
|
list / blockquote / link styling under `.live .row .md` so
|
||||||
the markdown body doesn't bleed into the row's own
|
the markdown body doesn't bleed into the row's own
|
||||||
|
|
@ -198,8 +228,9 @@ recv message bodies.
|
||||||
|
|
||||||
## Extra-MCP tools
|
## Extra-MCP tools
|
||||||
|
|
||||||
`fmtArgsGeneric(name, input)` is the fallback when a tool
|
`fmt_args_generic(name, input)` (`hive-agent/src/stream_enrich.rs`)
|
||||||
isn't in the built-in `fmtToolUse` switch:
|
is the fallback when a tool isn't in the built-in `fmt_tool_use`
|
||||||
|
switch, computed into `_summary` server-side:
|
||||||
|
|
||||||
- single string field → `name k: "v"`
|
- single string field → `name k: "v"`
|
||||||
- single number/bool field → `name k: v`
|
- single number/bool field → `name k: v`
|
||||||
|
|
@ -207,7 +238,7 @@ isn't in the built-in `fmtToolUse` switch:
|
||||||
`k: [N]` / `k: {…}` with a `…+N` overflow
|
`k: [N]` / `k: {…}` with a `…+N` overflow
|
||||||
|
|
||||||
This keeps less-frequent tools that don't have a specific
|
This keeps less-frequent tools that don't have a specific
|
||||||
`fmtToolUse` case from dumping raw JSON. Common matrix and
|
`fmt_tool_use` case from dumping raw JSON. Common matrix and
|
||||||
hyperhive tools have their own cases and skip this path.
|
hyperhive tools have their own cases and skip this path.
|
||||||
|
|
||||||
## Inline ask-operator answer
|
## Inline ask-operator answer
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue