docs: update web-ui.md for tabbed layout, topology tree, per-agent overhaul (#366 #373 #381)

This commit is contained in:
lexis 2026-05-25 00:45:56 +02:00
parent 91a72b5cbb
commit e9cce17828

View file

@ -100,100 +100,136 @@ Both bind their listeners with `SO_REUSEADDR` via
exponential backoff capped at 2s) so an nspawn restart that races exponential backoff capped at 2s) so an nspawn restart that races
the previous process's socket release resolves itself. the previous process's socket release resolves itself.
## Dashboard sections (top to bottom) ## Dashboard layout
1. **Notification row**`🔔 enable notifications` button when The dashboard (`/`) has a fixed chrome header at the top and a
permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted; `<main>` that shows exactly one tab pane at a time. The URL hash
inline "unsupported / blocked" message when applicable. Sits (`#swarm`, `#call`, `#system`) drives which pane is active; hash
under the banner. changes don't reload the page. FL0W is a separate full-page
2. **C0NTAINERS** — live containers with their action surface. terminal at `/flow.html` — its tab-strip entry is a cross-page
Pulsing red banner at the top of this section if any two link (`◆ FL0W ◆ →`), not a pane swap.
sub-agents hash to the same port (`port_conflicts` from
`/api/state`): the operator must rename one of them and **Chrome header** (fixed, overlays the active tab pane):
rebuild. `lifecycle::{spawn,rebuild}` also preflight this and - **Tab strip**: `◆ SW4RM ◆`, `◆ Y3R C4LL ◆`, `◆ SYST3M ◆`, and
refuse with a clear error message naming the conflicting agent. `◆ FL0W ◆ →` (page link). Count pills on SW4RM (container count)
3. **K3PT ST4T3** — destroyed-but-state-kept tombstones (size + and Y3R C4LL (pending approvals + questions); FL0W pill mirrors
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a the operator inbox length (hidden when zero).
Spawn approval; existing state is reused), `PURG3` (wipes - **Notification controls**: `🔔 enable notifications` when
state + applied dirs; `POST /purge-tombstone/{name}`). permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted.
4. **M3T4 1NPUTS** — inputs in `meta/flake.lock` the operator can Always visible in the chrome regardless of active tab.
selectively `nix flake update`, rendered as an indented tree: - **Banner-thin** (`░▒▓█▓▒░ HYPERHIVE / HIVE-C0RE / WE ARE THE WIRED ░▒▓█▓▒░`)
every fetched input at every depth (`hyperhive`, — sits below the tab strip.
`hyperhive/nixpkgs`, `agent-<n>`, `agent-<n>/mcp-<x>`, …), each
shown once at its shallowest path. `read_meta_inputs` walks the ### SW4RM tab
lock graph with a `visited` set — `follows` aliases and rev-less
nodes are skipped (issue #275). A `select all / select none` **C0NTAINERS** — live containers rendered as a depth-first
control sits above the tree. Checking inputs + submitting bumps tree using `ContainerView.parent` (populated by `topology.rs`).
the lock in `/meta/` and rebuilds the selected agents in Each container's row is prefixed with ASCII tree glyphs (`├─`,
sequence; each outcome reaches the manager as a `rebuilt` `└─`, `│ ` continuation columns) showing the agent
system event. parent/child hierarchy. When every container has `parent = null`
`POST /meta-update`. The lock bump + rebuild ripple runs in the (flat topology) the tree collapses to a plain list with no
background; while it does, the panel shows a pulsing "⏳ glyphs. Children are sorted alphabetically within each parent;
meta-update running" banner and the update button is disabled roots likewise. Cycles in the parent graph are tolerated —
(snapshot field `meta_update_running`, live event orphaned containers (not reachable from any root) are appended
`meta_update_running`). as roots so no agent disappears. Pulsing red banner at the top
5. **R3BU1LD QU3U3** — pending and recently-completed container of this section if any two sub-agents hash to the same port
operations: rebuilds, meta-update cascades, and first-spawns. (`port_conflicts` from `/api/state`): the operator must rename
One operation runs at a time; the worker drains FIFO. Each row one of them and rebuild. `lifecycle::{spawn,rebuild}` also
shows a state glyph (`⏸` queued / `▶` running / `✔` done / preflight this and refuse with a clear error message naming the
`✖` failed / `⊘` cancelled), kind glyph + verb (`↻ rebuild`, conflicting agent.
`◆ meta_update`, `✨ spawn`, `🗑 destroy`), agent name, source
chip (`manual | meta_update | auto_update | crash_recover`), `↻ UPD4TE 4LL` button appears above the containers list when any
timing, and an optional reason / error. Meta-update cascade agent is stale.
rebuilds nest under their parent entry (`parent_id` grouping;
`rqe-child` CSS class). Dedup: re-enqueueing a still-queued op ### Y3R C4LL tab
for the same agent collapses into the existing entry. Running
entries tick elapsed seconds live (same pattern as the TTL Things blocked on operator decision — approvals and questions
countdown). Cold-loaded from `/api/state.rebuild_queue`; live share a tab because they're the same concept ("something is
updates via `rebuild_queue_changed` snapshot event. waiting on you").
6. **M1ND H4S QU3STI0NS** — pending operator-targeted `ask`
questions, i.e. rows with `target IS NULL` (peer-to-peer **P3NDING APPR0VALS** — the queue (see "Approval card" below).
questions live in the same table but never surface here) The R3QU3ST SP4WN form lives at the top of this section.
(amber pulsing border). Free-text fallback always rendered
alongside any option list; `multi=true` renders options as **M1ND H4S QU3STI0NS** — pending operator-targeted `ask`
checkboxes; submit merges selections + free text comma-joined. questions (amber pulsing border). Free-text fallback always
Each row has a `✗ CANC3L` button that resolves the question rendered alongside any option list; `multi=true` renders options
with `[cancelled]`. Questions with a `ttl_seconds` show a as checkboxes; submit merges selections + free text
`⏳ MM:SS` chip; the host-side watchdog auto-cancels with comma-joined. Each row has a `✗ CANC3L` button. Questions with
`[expired]` when the deadline fires. a `ttl_seconds` show a `⏳ MM:SS` chip; the host-side watchdog
7. **QU3U3D R3M1ND3RS** — reminders agents have scheduled for auto-cancels with `[expired]` when the deadline fires.
themselves (via the `remind` tool) but not yet delivered.
Each row shows the owner, due time, and message; a `CANC3L` ### SYST3M tab
button hard-deletes (`POST /cancel-reminder/{id}`) and a
`R3TRY` button re-arms one whose delivery failed Passive / rare-interaction state.
(`POST /retry-reminder/{id}`). Backed by `GET /api/reminders`.
8. **P3NDING APPR0VALS** — the queue (see "Approval card" **M3T4 1NPUTS** — inputs in `meta/flake.lock` the operator can
below). The R3QU3ST SP4WN form lives at the top of this selectively `nix flake update`, rendered as an indented tree:
section since submitting it immediately queues an approval every fetched input at every depth (`hyperhive`,
that lands directly below. `hyperhive/nixpkgs`, `agent-<n>`, `agent-<n>/mcp-<x>`, …), each
9. **0PER4T0R 1NB0X** — recent messages addressed to `operator`, shown once at its shallowest path. `read_meta_inputs` walks the
derived client-side from the dashboard event stream (no longer lock graph with a `visited` set — `follows` aliases and rev-less
a snapshot field). Cold load seeds from nodes are skipped (issue #275). A `select all / select none`
`/dashboard/history`'s 200-message backfill; subsequent control sits above the tree. Checking inputs + submitting bumps
`sent` events with `to == "operator"` are appended live. Cap the lock in `/meta/` and rebuilds the selected agents in
50, newest-first. sequence; each outcome reaches the manager as a `rebuilt`
10. **MESS4GE FL0W** — live broker tail wrapped in a system event. `POST /meta-update`. While a lock-bump ripple runs,
`.terminal-wrap` (same chrome as the per-agent terminal). the panel shows a pulsing "⏳ meta-update running" banner and the
Cold load backfills the last ~200 messages from update button is disabled (snapshot field `meta_update_running`,
`/dashboard/history`; live frames arrive on live event `meta_update_running`).
`/dashboard/stream` and dispatch through
`HiveTerminal.create`. Each row is one broker event — **R3BU1LD QU3U3** — pending and recently-completed container
`sent` or `delivered` — with `from → to: body`; per-agent operations: rebuilds, meta-update cascades, and first-spawns.
thinking / tool calls / claude chatter stay out of this One operation runs at a time; the worker drains FIFO. Each row
view, only what passes through hive-c0re's broker. Sticky- shows a state glyph (`⏸` queued / `▶` running / `✔` done /
bottom auto-scroll + "↓ N new" pill match the per-agent `✖` failed / `⊘` cancelled), kind glyph + verb (`↻ rebuild`,
page. Below the stream sits a terminal-style compose box: `◆ meta_update`, `✨ spawn`, `🗑 destroy`), agent name, source
`@name` picks the recipient (sticky across sends via chip (`manual | meta_update | auto_update | crash_recover`),
localStorage; auto-complete from the live container list, timing, and an optional reason / error. Meta-update cascade
Tab/Enter to confirm; `@*` broadcasts to every registered rebuilds nest under their parent entry (`parent_id` grouping;
agent), starting a message with `@<name> body` retargets `rqe-child` CSS class). Dedup: re-enqueueing a still-queued op
in one stroke, plain text sends to the sticky recipient. for the same agent collapses into the existing entry. Running
`POST /op-send` drops `{from:"operator", to, body}` into entries tick elapsed seconds live. Cold-loaded from
the broker and returns 200; the resulting SSE frame `/api/state.rebuild_queue`; live updates via `rebuild_queue_changed`
re-renders both the terminal row and the inbox section snapshot event.
(no `/api/state` refetch). Manager is addressed as
`@manager` (the broker recipient string), not `@hm1nd` **QU3U3D R3M1ND3RS** — reminders agents have scheduled for
(the container name); the auto-complete swaps automatically. themselves (via the `remind` tool) but not yet delivered.
Each row shows the owner, due time, and message; a `CANC3L`
button hard-deletes (`POST /cancel-reminder/{id}`) and a
`R3TRY` button re-arms one whose delivery failed
(`POST /retry-reminder/{id}`). Backed by `GET /api/reminders`.
**K3PT ST4T3** — destroyed-but-state-kept tombstones (size +
age + claude-creds badge). Two actions: `⊕ R3V1V3` (queues a
Spawn approval; existing state is reused), `PURG3` (wipes
state + applied dirs; `POST /purge-tombstone/{name}`).
### FL0W page (`/flow.html`)
A dedicated full-page terminal (not a tab pane — a separate HTML
page). Reuses the same `<header class="dashboard-chrome">` chrome
as the dashboard so the tab strip remains visible; SW4RM / Y3R
C4LL / SYST3M are cross-page links back to `/#<tab>`, and the
FL0W entry is marked active (`aria-current="page"`).
**0PER4T0R 1NB0X** — recent messages addressed to `operator`,
derived client-side from the dashboard event stream. Cold load
seeds from `/dashboard/history`'s 200-message backfill; subsequent
`sent` events with `to == "operator"` are appended live. Cap 50,
newest-first.
**MESS4GE FL0W** — live broker tail wrapped in a `.terminal-wrap`.
Cold load backfills the last ~200 messages from `/dashboard/history`;
live frames arrive on `/dashboard/stream`. Each row is one broker
event — `sent` or `delivered` — with `from → to: body`. Sticky-
bottom auto-scroll + "↓ N new" pill. Below the stream sits a
terminal-style compose box: `@name` picks the recipient (sticky via
localStorage; auto-complete from the live container list, Tab/Enter
to confirm; `@*` broadcasts). `POST /op-send` drops
`{from:"operator", to, body}` into the broker; the resulting SSE
frame re-renders both the terminal row and the inbox section.
Manager is addressed as `@manager` (the broker recipient string),
not `@hm1nd` (the container name).
### Container row ### Container row
@ -436,101 +472,75 @@ Generalised form helpers: `form[data-confirm="…"]` pops
## Per-agent page ## Per-agent page
Layout, top to bottom: Three fixed-position layers frame a full-viewport terminal:
- Banner (gradient shimmer while state=thinking). **Fixed-overlay header** (`<header class="agent-header">`): frosted
- Title with `↑ DASHB04RD` back-link (new tab) + `↻ R3BU1LD`. glass — `backdrop-filter: blur` lets scrolled terminal rows show
- Meta links row: backend-supplied `StateSnapshot.links` (issue through. Left to right:
#262) rendered as `<icon> label →`. Always includes `📊 stats` - Agent icon (`<img src="/icon">`).
(`kind = Container`); `🖥 screen` when the VNC compositor is - Title (`<h2 id="title">`) + meta-nav (`<nav id="meta-links">`):
enabled; `⬡ forge` (profile) + `↳ config` (agent-configs backend-supplied `StateSnapshot.links` rendered as icon-only
mirror, repo root since the agent doesn't know its own deployed anchors. Always includes `📊 stats` (`kind = Container`);
sha) when the agent has a forge account, both `kind = Forge`; `🖥 screen` when the VNC compositor is enabled; `⬡ forge` (profile)
followed by any `hyperhive.dashboardLinks` extras + `↳ config` (agent-configs mirror) when the agent has a forge
(`kind = External`) read from account; followed by any `hyperhive.dashboardLinks` extras
`{state_dir}/hyperhive-dashboard-links.json`. The same list (`kind = External`). The dashboard card's icon strip is the same
feeds the dashboard card's icon strip via the host's list via `GET /api/agent/{name}/links` — agent backend is the
`GET /api/agent/{name}/links` passthrough proxy — agent backend single source of truth.
is the single source of truth for what links it exposes. - **State row** (`<div id="state-row">`): alive badge + state badge
Frontend resolves each `kind` (`container` → same-origin path, + model chip + ctx badge + cost badge + last-turn chip + cancel
`forge``http://host:3000`, `external` → absolute) via DOM button + new-session button.
building, so agent-declared strings never reach `innerHTML`. - Alive badge: `● alive` (green) / `⊘ rate limited` (red) /
- Status section: empty when online (alive-badge in the state `◌ needs login` / `◌ logging in` / `○ offline` / `… connecting`.
row carries the signal), populated with the login form / Driven by `LiveEvent::StatusChanged`.
OAuth URL when `status` is `needs_login_*`.
- **State row**: alive badge + state badge + model chip + ctx
badge + last-turn timing + cancel-turn button + new-session
button. Every chip carries a `title=...` tooltip with the
detailed breakdown.
- Alive badge: `● alive` (green) / `⊘ rate limited` (red, while
the harness is parked after a 429 — clears automatically when
the sleep expires) / `◌ needs login` (amber) / `◌ logging in` /
`○ offline` / `… connecting`. Driven by
`LiveEvent::StatusChanged`; replaces the old "harness alive
— turn loop running" paragraph so the state row carries
every reachability signal.
- State badge: `💤 idle` / `🧠 thinking` / `📦 compacting` / - State badge: `💤 idle` / `🧠 thinking` / `📦 compacting` /
`○ offline` / `… booting`, with an age suffix (`12s`, `○ offline` / `… booting` + age suffix. Driven by
`2m 14s`). Driven by `LiveEvent::TurnStateChanged` `LiveEvent::TurnStateChanged ({ state, since_unix })`.
(`{state, since_unix}`) — the bus emits on every - Model chip: `model · <name>`. Driven by `LiveEvent::ModelChanged`.
`Bus::set_state` so the badge updates without a /api/state - Ctx badge: `ctx · 142k` — last inference's prompt size (the
refetch. Cold-load via `/api/state.turn_state` + context window utilisation number to watch before compacting).
`turn_state_since`. Tooltip shows % of window when `context_window_tokens` is known.
- Model chip: `model · <name>` (e.g. `model · haiku`). Driven - Cost badge: `cost · 1.3M` — cumulative tokens billed across every
by `LiveEvent::ModelChanged`; emitted from `Bus::set_model`. inference in the last turn. Tool-heavy turns rebill the cached
- Ctx badge: `ctx · 142k` — last inference's prompt size prefix per call, so this routinely exceeds the window — cost
(input + cache_read + cache_write of the most recent signal, not size signal.
model call in the just-ended turn). This is the **actual - Both driven by `LiveEvent::TokenUsageChanged { ctx, cost }` at
context window utilisation** — the number to watch when turn-end.
deciding whether to compact. When `context_window_tokens` - `■ cancel turn` (visible while thinking) → `POST /api/cancel`.
is available from `/api/state`, the badge tooltip shows the - `↻ new session` (always, amber) → `POST /api/new-session`; next
percentage of window used. turn drops `--continue`.
- Cost badge: `cost · 1.3M` — cumulative tokens billed - **Inbox pill** (`📬 inbox · N`): hidden when empty; click opens the
across **every inference** in the last turn (sum of all inbox flyout in the side panel.
per-call prompts). Tool-heavy turns rebill the cached - **Loose-ends pill** (`🪢 loose ends · N`): hidden when empty; click
prefix per call, so this routinely exceeds the model's opens the loose-ends flyout in the side panel.
window — it's a cost signal, not a size signal.
- Both badges driven by `LiveEvent::TokenUsageChanged {
ctx, cost }`, emitted once at turn-end from
`Bus::record_turn_usage`. The harness tracks per-inference
usage by walking `assistant` events in the stream-json
and updating `last_inference` on each one; the `result`
event supplies `cost` and triggers the emit.
- Last-turn chip: `last turn 12.3s` appears after the first
turn ends, computed from the state-since deltas.
- `■ cancel turn` button: visible only while state=thinking,
POSTs `/api/cancel`.
- `↻ new session` button: always visible, amber. Confirms
via `window.confirm()` then POSTs `/api/new-session` to
arm a one-shot Bus flag — the next turn drops
`--continue`, starting a fresh claude session. Subsequent
turns resume normal `--continue`.
Polling: `/api/state` is fetched **once** on cold load, and `/api/state` is fetched once on cold load (+ while
again while `status === 'needs_login_in_progress'` (login `status === 'needs_login_in_progress'`); all other updates arrive via
session output isn't event-shaped yet). Every other badge SSE. Snapshot includes `context_window_tokens` for the ctx badge tooltip.
updates from SSE; no periodic refresh timer runs. Snapshot
includes `context_window_tokens` (effective window size for **Main content** (`<main class="agent-main">`): fills the viewport
the agent's current model, from `events::context_window_tokens`) and scrolls behind the fixed header + footer.
used to compute percentage-of-window in the ctx badge tooltip. - `#status` overlay: empty when online; shows the login form / OAuth
- Inbox `<details>` block (collapsed): `inbox · N` — last 30 URL when `status` is `needs_login_*`.
messages addressed to this agent, fetched via
`AgentRequest::Recent { limit: 30 }`. Reply messages (those
with a non-null `in_reply_to`) are indented and prefixed with
`↳ reply ·` in amber. (Separate from
`AgentRequest::Recv { wait_seconds, max }` which the harness
uses internally to long-poll the broker.)
- Loose-ends `<details>` block: `loose ends · N` — questions,
approvals, and reminders pending against this agent (the
`get_loose_ends` data, via `GET /api/loose-ends`). Question
rows carry an inline answer form (textarea — Enter submits,
Shift+Enter newlines); submitting POSTs cross-origin to the
core dashboard's `/answer-question/{id}` so the operator
answers *as operator*. The per-agent socket deliberately gets
no operator-authority path — see `docs/boundary.md`.
- Terminal-wrap: live event tail (sticky-bottom auto-scroll + - Terminal-wrap: live event tail (sticky-bottom auto-scroll +
`↓ N new` pill when not at bottom) followed by an `↓ N new` pill when not at bottom).
operator-input textarea acting as a prompt.
**Fixed-overlay footer** (`<footer class="agent-composer">`): frosted
glass, symmetric with the header. Contains the operator-input
textarea (`#term-input`) — multi-line, Enter sends, Shift+Enter
newlines, Tab-completes slash commands (see "Terminal-embedded
prompt" below).
**Side panel** (slide-in from right): singleton shared with the
dashboard's side panel shape. Carries inbox and loose-ends flyouts
(opened via the header pills) as well as long content (file previews,
diffs, journald logs). Inbox flyout: last 30 messages addressed to
this agent (`AgentRequest::Recent { limit: 30 }`); reply messages
indented with `↳ reply ·` in amber. Loose-ends flyout: questions,
approvals, and reminders pending against this agent (`GET /api/loose-ends`);
question rows carry an inline answer form that POSTs cross-origin to
the core dashboard's `/answer-question/{id}` so the operator answers
*as operator* (see `docs/boundary.md`).
### Live view ### Live view