Phase 4 of #273 — the actual switch. Both axum routers now serve their static surface via `tower_http::services::ServeDir` mounted as a fallback service, reading the dist path from `HIVE_STATIC_DIR` (set by Phase 3's NixOS module wiring). Deletes: - `hive-c0re/assets/{index.html, app.js, dashboard.css}` - `hive-ag3nt/assets/{index.html, app.js, agent.css, stats.html, stats.js, screen.html}` - The whole `hive-fr0nt/` crate (workspace member dropped, both hive-c0re and hive-ag3nt drop their `hive-fr0nt.workspace = true` dep). Its contents now live as `@hive/shared` under `frontend/packages/shared/`. Rust changes: - `hive-c0re/src/dashboard.rs`: remove `serve_index`, `serve_css`, `serve_app_js`, `serve_shared_js`, `serve_marked_js`, `serve_favicon` (all six `include_str!` handlers); replace their routes with a single `.fallback_service(ServeDir::new(static_dir))` on the router. Fail closed (anyhow::bail) if `HIVE_STATIC_DIR` is unset or not a directory at startup. - `hive-ag3nt/src/web_ui.rs`: remove `serve_index`, `serve_css`, `serve_app_js`, `serve_shared_js`, `serve_marked_js`, `serve_stats`, `serve_stats_js`, `serve_screen`; same `fallback_service` pattern. `serve_icon` stays (consumes `/etc/hyperhive/icon.svg` + `branding/hyperhive.svg` fallback, neither of which lives under the frontend dist). - `AgentLink` URLs for stats/screen switched from `/stats` / `/screen` to `/stats.html` / `/screen.html` since ServeDir doesn't auto- append the extension and the on-disk filename is the natural URL post-cutover. - `Cargo.toml` (workspace): drop `hive-fr0nt` member + workspace dep, add `tower-http = { version = "0.6", features = ["fs"] }`. - `hive-c0re/Cargo.toml` + `hive-ag3nt/Cargo.toml`: drop the `hive-fr0nt.workspace = true` dep, add `tower-http.workspace = true`. Docs updated: - `CLAUDE.md`: file map reflects `frontend/` (was `hive-fr0nt/` + `assets/`) and the ServeDir/HIVE_STATIC_DIR shape. - `docs/web-ui.md` 'Shape (shared by both)' section: describes the ServeDir fallback + bundled-by-esbuild surface, no more `include_str!` references. - `docs/terminal-rendering.md`: src paths point at `frontend/packages/{agent,shared}/src/`; marked is the npm dep, not vendored UMD. Validation: - `cargo check --workspace` — clean (5 warnings, all pre-existing in `rebuild_queue.rs`, none on changed files). - `cargo clippy --workspace --all-targets` — clean (11 warnings, same pre-existing source). - `cd frontend && npm run build` from the prior commit's lockfile produces the dist directories the new routers consume: dashboard: `dist/{index.html, static/{app.js, dashboard.css}}` agent: `dist/{index.html, stats.html, screen.html, static/{app.js, stats.js, agent.css}}` (favicon.svg lands in dashboard/ during the nix build — `nix/frontend.nix` install phase copies `branding/hyperhive.svg` there, since it's outside the npm tree.) Refs #273.
114 lines
6.1 KiB
Markdown
114 lines
6.1 KiB
Markdown
# Per-agent terminal: row taxonomy (as built)
|
|
|
|
Snapshot of how the per-agent web UI's live pane renders each
|
|
event kind today. Source of truth lives in
|
|
`frontend/packages/agent/src/app.js` (`renderStream`, `fmtToolUse`,
|
|
`renderRichToolUse`, `renderToolResult`, `renderTaskEvent`,
|
|
`mdNode`, `detailsOpenMd`, `fmtArgsGeneric`) +
|
|
`frontend/packages/shared/src/terminal.css` (the shared
|
|
`.live .<class>` styling) + the `marked` npm package (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, 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` |
|
|
|
|
## Renderer dispatch
|
|
|
|
`renderStream(v, api)` walks each stream-json line:
|
|
|
|
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`.
|
|
`fmtToolUse` surfaces the salient arg per built-in tool —
|
|
e.g. `recv` shows `wait <N>s` / `max <N>` when set (bare
|
|
`recv()` otherwise), `Bash` flags `[bg]` for
|
|
`run_in_background` commands.
|
|
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).
|
|
|
|
## Markdown
|
|
|
|
`mdNode(text)` wraps `marked.parse(text)` (the `marked` v4.x npm
|
|
dep, bundled by esbuild into the page's `app.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.
|
|
|
|
## Extra-MCP tools
|
|
|
|
`fmtArgsGeneric(name, input)` is the fallback when a tool
|
|
isn't in the built-in `fmtToolUse` switch:
|
|
|
|
- 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
|
|
|
|
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
|
|
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.
|