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