docs: fix genuine passive-voice hits in docs/web-ui (small files)

agent.md, css-vars.md, design-guide.md, shape.md, terminal-rendering.md --
26 of 40 write-good.Passive hits rewritten to name the actor (hive-agent,
browsers, the harness, the client, lifecycle::{spawn,rebuild}, CSS, the
router, ...), reusing an actor already named nearby wherever one exists.
14 left alone: established config-state conditionals ("when X is
set/enabled/given/called"), negative-capability/state-descriptor idioms
("is gone", "is hidden", "is unchanged"), one false-positive
("typed slash commands" as a compound noun, not a passive action), a
backward-looking changelog fact with no actor worth naming, and two
deliberately-parallel rhetorical contrasts (sanitized vs XSS-safe;
cache-hit vs cache-miss) left symmetric on purpose.

dashboard.md (70 more hits) is the next docs/web-ui/ batch, not part of
this one -- big enough to deserve its own PR.
This commit is contained in:
iris 2026-09-08 14:58:43 +02:00 committed by mara
commit e9d59f3d10
5 changed files with 43 additions and 43 deletions

View file

@ -21,8 +21,8 @@ output, not something a selector reaches by id).
glass — `backdrop-filter: blur` lets scrolled terminal rows show glass — `backdrop-filter: blur` lets scrolled terminal rows show
through. Measures its own rendered height via `ResizeObserver` through. Measures its own rendered height via `ResizeObserver`
(`Header.tsx`) and writes it to a CSS custom property the content (`Header.tsx`) and writes it to a CSS custom property the content
below reads for its offset — a fixed `6em` guess used to be baked into below reads for its offset — `agent.css` used to bake in a fixed `6em`
`agent.css`, which silently broke (content overlapping the header) the guess, which silently broke (content overlapping the header) the
moment any row of badges/pills wrapped onto an extra line at some moment any row of badges/pills wrapped onto an extra line at some
viewport width; measuring instead of guessing closes that bug class viewport width; measuring instead of guessing closes that bug class
structurally rather than for one specific trigger. Two columns: structurally rather than for one specific trigger. Two columns:
@ -77,8 +77,8 @@ structurally rather than for one specific trigger. Two columns:
extras) — all as real `<a>` elements, not a `Dropdown`-style extras) — all as real `<a>` elements, not a `Dropdown`-style
command list, so ctrl/middle-click and "copy link address" keep command list, so ctrl/middle-click and "copy link address" keep
working. Each `AgentLink.kind` resolves differently: `container` working. Each `AgentLink.kind` resolves differently: `container`
same-origin path; `forge``state.forge_public_url + url`, and the same-origin path; `forge``state.forge_public_url + url`, and
link is dropped entirely when that's unset (never guessed from hive-agent drops the link entirely when that's unset (never guessed from
`<host>:3000`); `external` → already absolute. Same source `<host>:3000`); `external` → already absolute. Same source
(`GET /api/state`'s `links` field) also feeds (`GET /api/state`'s `links` field) also feeds
`DashboardState.links` for the dashboard card's icon strip — `DashboardState.links` for the dashboard card's icon strip —
@ -121,7 +121,7 @@ and scrolls behind the fixed header + footer.
shoulder-surfers or screenshots. `autocomplete="one-time-code"` shoulder-surfers or screenshots. `autocomplete="one-time-code"`
is the semantic value for OAuth codes (per WHATWG): browsers may is the semantic value for OAuth codes (per WHATWG): browsers may
silently ignore `autocomplete="off"` on `type="password"`, but silently ignore `autocomplete="off"` on `type="password"`, but
`one-time-code` is honoured and suppresses the "save password browsers honour `one-time-code` and suppress the "save password
for this site?" prompt that would otherwise fire on submit. for this site?" prompt that would otherwise fire on submit.
- Terminal-wrap: live event tail (sticky-bottom autoscroll + - Terminal-wrap: live event tail (sticky-bottom autoscroll +
`↓ N new` pill when not at bottom). The pill is **anchored in `↓ N new` pill when not at bottom). The pill is **anchored in
@ -154,8 +154,8 @@ journald logs). Inbox flyout: unread messages addressed to this agent
with `↳ reply ·` in amber. A `✓ mark all read` button appears in the with `↳ reply ·` in amber. A `✓ mark all read` button appears in the
flyout header when the inbox is non-empty; selecting it confirms then flyout header when the inbox is non-empty; selecting it confirms then
POSTs cross-origin to the core dashboard's `POST POSTs cross-origin to the core dashboard's `POST
/api/agent/{name}/mark-all-read` — all pending messages for this agent /api/agent/{name}/mark-all-read`, which acks all pending messages for
are acked, the harness won't receive wake-prompts for them. A `{ this agent so the harness won't receive wake-prompts for them. A `{
marked: N }` pill surfaces the count. After the drain the inbox list marked: N }` pill surfaces the count. After the drain the inbox list
empties on reload (the filter is `acked_at IS NULL`, so drained empties on reload (the filter is `acked_at IS NULL`, so drained
messages disappear). Todos flyout ("loose-ends v2"): the harness-local messages disappear). Todos flyout ("loose-ends v2"): the harness-local
@ -219,7 +219,7 @@ the full row taxonomy and dispatch logic):
`compact_boundary` emit muted notes; `commands_changed` emits an `compact_boundary` emit muted notes; `commands_changed` emits an
expandable details row listing slash commands; `thinking_tokens` expandable details row listing slash commands; `thinking_tokens`
updates a single in-place `🧠` counter; `init`, `result`, and updates a single in-place `🧠` counter; `init`, `result`, and
`rate_limit_event` are dropped (noise / used elsewhere); other `rate_limit_event` drop silently (noise / used elsewhere); other
subtypes → muted `⚙ <subtype>` note. subtypes → muted `⚙ <subtype>` note.
- `Note``· text`. - `Note``· text`.
- `TurnStart``◆ TURN ← <from>` with the wake-prompt body; a - `TurnStart``◆ TURN ← <from>` with the wake-prompt body; a
@ -259,8 +259,8 @@ Slash commands today:
Arms a one-shot on the Bus; next turn runs without Arms a one-shot on the Bus; next turn runs without
`--continue`, dropping the resume session entirely. `--continue`, dropping the resume session entirely.
- `/logout``POST /api/logout` (confirms first). Wipes OAuth - `/logout``POST /api/logout` (confirms first). Wipes OAuth
credential files, parks the agent in `needs_login`. Session credential files, parks the agent in `needs_login`, and preserves
history (`~/.claude/projects/`) is preserved. session history (`~/.claude/projects/`).
Unknown `/foo` shows an error row instead of being silently sent. Unknown `/foo` shows an error row instead of being silently sent.
@ -314,17 +314,17 @@ shaped).
session continuity — logout must stay narrowed to just the session continuity — logout must stay narrowed to just the
credential files. credential files.
3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note` 3. Flip `LoginState::NeedsLogin` + emit a `LiveEvent::Note`
describing exactly what was wiped, then emit naming the files it just deleted, then emit
`needs_login_idle`. The turn-loop's next iteration parks `needs_login_idle`. The turn-loop's next iteration parks
into `wait_for_login`, which snapshots the credential dir into `wait_for_login`, which snapshots the credential dir
(now missing the wiped files) and resumes when a fresh (now missing the wiped files) and resumes when a fresh
credentials file appears via the dashboard's `/login/code` credentials file appears via the dashboard's `/login/code`
flow (the same mtime-resumption path manual re-login uses). flow (the same mtime-resumption path manual re-login uses).
Always returns 200 with a body describing what happened — Always returns 200 with a body describing what happened — it
per-file errors are folded into the response + the Note so the folds per-file errors into the response + the Note so the
operator sees them in the live panel rather than as an HTTP operator sees them in the live panel rather than as an HTTP
error. Missing files (already logged out) are treated as error. Missing files (already logged out) count as
idempotent. idempotent.
- `GET /api/state` — cold-load snapshot (`StateSnapshot`) consumed by - `GET /api/state` — cold-load snapshot (`StateSnapshot`) consumed by
the frontend (`useAgentState.ts`) on page load and, on a poll, while the frontend (`useAgentState.ts`) on page load and, on a poll, while
@ -374,7 +374,7 @@ shaped).
dimensions; enabled once the server advertises the dimensions; enabled once the server advertises the
`ExtendedDesktopSize` pseudo-encoding (`-308` rect in the header). `ExtendedDesktopSize` pseudo-encoding (`-308` rect in the header).
Fit-mode state persists in `localStorage` (`screen-fit`); default Fit-mode state persists in `localStorage` (`screen-fit`); default
is on. Pointer coordinates are rescaled in `sendPointer` so clicks is on. `sendPointer` rescales pointer coordinates so clicks
land on the right pixel regardless of CSS scale. land on the right pixel regardless of CSS scale.
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket - `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
frames to the weston VNC server at `127.0.0.1:<vnc_port>`. frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
@ -400,8 +400,8 @@ Bus events (new vocabulary on `/events/stream`):
- `status_changed { status }``online` / `rate_limited` / - `status_changed { status }``online` / `rate_limited` /
`needs_login_idle` / `needs_login_in_progress`. Drives the `needs_login_idle` / `needs_login_in_progress`. Drives the
alive-badge. `rate_limited` is set when the harness detects a alive-badge. The harness sets `rate_limited` when it detects a
429 response and cleared when the retry sleep expires. 429 response and clears it when the retry sleep expires.
- `model_changed { model }` — drives the model chip. - `model_changed { model }` — drives the model chip.
- `effort_changed { effort }` — drives the effort picker chip. - `effort_changed { effort }` — drives the effort picker chip.
Emitted from `Bus::set_effort` on `POST /api/effort`; applies on Emitted from `Bus::set_effort` on `POST /api/effort`; applies on
@ -452,7 +452,7 @@ input tokens of the most recent fresh claude session's first turn —
a proxy for system-prompt + CLAUDE.md sprawl (a fresh session's first a proxy for system-prompt + CLAUDE.md sprawl (a fresh session's first
turn pays the full static prefix uncached). It's derived in `stats.rs` turn pays the full static prefix uncached). It's derived in `stats.rs`
(`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of (`first_turn_ctx`: the first turn, `ORDER BY started_at LIMIT 1`, of
the latest `sessions` row in the window) and is omitted from the JSON the latest `sessions` row in the window) and stays out of the JSON
until the `sessions` / `turn_stats.session_id` capture has rows — so until the `sessions` / `turn_stats.session_id` capture has rows — so
the chip stays hidden on older dbs (inert-until-capture). the chip stays hidden on older dbs (inert-until-capture).
`stats.rs` opens the sqlite db read-only and degrades to an `stats.rs` opens the sqlite db read-only and degrades to an

View file

@ -19,7 +19,7 @@ for the raw `--baseNN` slots directly. (`base.css` holds only the shared
`colors.css` declares each palette's 16 hex values exactly once — `colors.css` declares each palette's 16 hex values exactly once —
`--mocha-baseNN` (dark, Catppuccin Mocha) and `--latte-baseNN` (light, `--mocha-baseNN` (dark, Catppuccin Mocha) and `--latte-baseNN` (light,
Catppuccin-Latte-derived; `base08`-`base0F` are darkened from stock Catppuccin-Latte-derived; `base08`-`base0F` run darker than stock
Latte to actually clear WCAG AA against how this app uses them — real Latte to actually clear WCAG AA against how this app uses them — real
contrast failures found in review, see the comment above those contrast failures found in review, see the comment above those
declarations in `colors.css` for the numbers and why). Everything else declarations in `colors.css` for the numbers and why). Everything else
@ -43,8 +43,8 @@ them **first** (`colors.css` then `theme.css`) ahead of the page CSS. Note
`theme.css` does **not** `@import` `colors.css` — that would bake the `theme.css` does **not** `@import` `colors.css` — that would bake the
slots back into it; they're two separate output files so a swap replaces slots back into it; they're two separate output files so a swap replaces
just `colors.css`. (Load order doesn't actually affect resolution — just `colors.css`. (Load order doesn't actually affect resolution —
custom properties resolve at computed-value time — but `colors.css` is custom properties resolve at computed-value time — but each page still
linked first for clarity.) links `colors.css` first for clarity.)
## Semantic palette (`theme.css`) ## Semantic palette (`theme.css`)

View file

@ -89,7 +89,7 @@ needs a refresh story; a value that's silently gone stale with no way to
tell is worse than one that's visibly stale. tell is worse than one that's visibly stale.
- **Relative-time labels must actually tick.** A label like "fresh (5s - **Relative-time labels must actually tick.** A label like "fresh (5s
ago)" is computed from a stored timestamp and re-renders itself on an ago)" derives from a stored timestamp and re-renders itself on an
interval — never a value frozen at fetch time that quietly goes stale interval — never a value frozen at fetch time that quietly goes stale
while still reading as current. Pauses while its tab is hidden. while still reading as current. Pauses while its tab is hidden.
- **Pages that poll get a refresh-interval control, not silent - **Pages that poll get a refresh-interval control, not silent
@ -143,8 +143,8 @@ just bare column headers floating over nothing.
## Component-first design ## Component-first design
Build the `src/ui/` primitive before or alongside the first real page Build the `src/ui/` primitive before or alongside the first real page
that needs it, not after it's been styled inline and left for later — that needs it, not after a contributor styles it inline and leaves it
a page reaching for a component that's already there has ready-made for later — a page reaching for a component that's already there has ready-made
blocks to build with, instead of the next contributor duplicating blocks to build with, instead of the next contributor duplicating
ad-hoc styling that someone then has to hunt down and consolidate. ad-hoc styling that someone then has to hunt down and consolidate.
`Panel`/`Badge`/`Table`/`TextField`/`SelectField`/`Button` are the `Panel`/`Badge`/`Table`/`TextField`/`SelectField`/`Button` are the

View file

@ -8,8 +8,8 @@
- `GET /``index.html` from the bundled frontend dist (see - `GET /``index.html` from the bundled frontend dist (see
`frontend/`). Both binaries' routers declare their dynamic `frontend/`). Both binaries' routers declare their dynamic
endpoints first and then `fallback_service(ServeDir::new(...))` endpoints first and then `fallback_service(ServeDir::new(...))`
pointed at `HIVE_STATIC_DIR`anything not matched by an API or pointed at `HIVE_STATIC_DIR`that fallback serves anything not
action route is served from the dist. Dashboard dist lives at matched by an API or action route from the dist. Dashboard dist lives at
`${frontend}/dashboard`; per-agent dist is the merged `${frontend}/dashboard`; per-agent dist is the merged
`hyperhive.frontend.mergedDist` (default agent dist + per-agent `hyperhive.frontend.mergedDist` (default agent dist + per-agent
`extraFiles` overlay). `extraFiles` overlay).
@ -43,7 +43,7 @@
to `target="_blank"`. to `target="_blank"`.
- `GET /api/state` → JSON snapshot the JS app renders into the - `GET /api/state` → JSON snapshot the JS app renders into the
DOM. Includes a top-level `seq` (the dashboard event channel's DOM. Includes a top-level `seq` (the dashboard event channel's
high-water mark at the moment the snapshot was assembled); high-water mark at the moment the server assembled the snapshot);
clients use it to dedupe their buffered SSE traffic against clients use it to dedupe their buffered SSE traffic against
the snapshot (drop frames with `seq <= snapshot.seq`). the snapshot (drop frames with `seq <= snapshot.seq`).
- `GET /api/dashboard/stream` (dashboard) / `GET /events/stream` - `GET /api/dashboard/stream` (dashboard) / `GET /events/stream`
@ -88,9 +88,9 @@
## Shared terminal pane ## Shared terminal pane
Both surfaces' scrollable log streams (`#msgflow` on the dashboard, The shared terminal factory in `@hive/shared/terminal.js` backs both
`#live` on the per-agent page) are backed by the shared terminal surfaces' scrollable log streams (`#msgflow` on the dashboard, `#live`
factory in `@hive/shared/terminal.js`. The factory wires up on the per-agent page). The factory wires up
sticky-bottom autoscroll, a "↓ N new" pill, history backfill, and sticky-bottom autoscroll, a "↓ N new" pill, history backfill, and
SSE replay. Pages register a `kind → renderer` map; unknown kinds SSE replay. Pages register a `kind → renderer` map; unknown kinds
fall through to a JSON-dump note row. The factory ships three row fall through to a JSON-dump note row. The factory ships three row
@ -175,8 +175,8 @@ initial visual lag to one frame instead of one microtask + frame.
**Backfill + SSE.** Cold load fetches `historyUrl` (replay), then **Backfill + SSE.** Cold load fetches `historyUrl` (replay), then
subscribes to `streamUrl` (live tail). Both endpoints return subscribes to `streamUrl` (live tail). Both endpoints return
`{ seq, events }` so the client can dedupe — events with `{ seq, events }` so the client can dedupe — the client silently
`seq <= snapshot.seq` from the SSE stream are dropped silently drops events with `seq <= snapshot.seq` from the SSE stream
(the snapshot already covers them). History rows render with a (the snapshot already covers them). History rows render with a
`.no-anim` class so they don't stagger in like live events. The `.no-anim` class so they don't stagger in like live events. The
optional `streamFactory(url)` callback lets the dashboard hand optional `streamFactory(url)` callback lets the dashboard hand
@ -221,8 +221,8 @@ leave it empty for a paint (on async builders) or a whole frame (on
large synchronous ones), which reads as a visible flash on every large synchronous ones), which reads as a visible flash on every
poll cycle. Builders receive the fragment as their poll cycle. Builders receive the fragment as their
`root`, so existing renderer code carries over unchanged; early `root`, so existing renderer code carries over unchanged; early
returns inside the builder still commit whatever was appended returns inside the builder still commit whatever it appended
before they returned. before returning.
**Keyed DOM caching:** for sections whose rows hold interactive state **Keyed DOM caching:** for sections whose rows hold interactive state
(textarea drafts, checkboxes, focused inputs) `paintAtomic` isn't (textarea drafts, checkboxes, focused inputs) `paintAtomic` isn't
@ -260,8 +260,8 @@ the left edge captures pointer events, resizes the drawer in
real-time (pointer capture keeps dragging even if the cursor real-time (pointer capture keeps dragging even if the cursor
outpaces the handle), and persists the chosen width to outpaces the handle), and persists the chosen width to
`localStorage` (key `hyperhive:side-panel-width`) so it `localStorage` (key `hyperhive:side-panel-width`) so it
survives page reload. Width is clamped to CSS `min-width: 320px` survives page reload. CSS clamps the width between `min-width: 320px`
/ `max-width: 96vw`; the viewport-resize handler re-clamps and `max-width: 96vw`; the viewport-resize handler re-clamps
persisted values after a window shrink. File persisted values after a window shrink. File
previews are type-aware: previews are type-aware:
@ -286,9 +286,9 @@ Both bind their TCP listener with `SO_REUSEADDR` via
the previous process's socket release resolves itself, but the two the previous process's socket release resolves itself, but the two
binaries differ on the attempt budget. The dashboard's binaries differ on the attempt budget. The dashboard's
`bind_with_retry` (`hive-c0re/src/dashboard/mod.rs`) has **no `bind_with_retry` (`hive-c0re/src/dashboard/mod.rs`) has **no
attempt cap** — retrying forever is deliberate, since genuine port attempt cap** — retrying forever is deliberate, since
collisions are preflighted host-side (`lifecycle::{spawn,rebuild}` `lifecycle::{spawn,rebuild}` preflights genuine port collisions
refuses with a clear error, surfaced on the dashboard as a banner), host-side (refuses with a clear error, surfaced on the dashboard as a banner),
so at this layer a persistent `AddrInUse` always reflects a so at this layer a persistent `AddrInUse` always reflects a
recoverable stale socket. Its first 12 attempts log at WARN; after recoverable stale socket. Its first 12 attempts log at WARN; after
that the level drops to INFO so a long-held stale socket doesn't that the level drops to INFO so a long-held stale socket doesn't
@ -325,10 +325,10 @@ at without rebuilding the dist. The cases that matter:
The gateway upstream config strips the prefix before forwarding to The gateway upstream config strips the prefix before forwarding to
the per-agent server, so the agent's Rust routes (`api/state`, the per-agent server, so the agent's Rust routes (`api/state`,
`events/stream`, `screen/ws`, `login/start`, …) keep their absolute `events/stream`, `screen/ws`, `login/start`, …) keep their absolute
paths server-side. Only the browser-facing URLs are gated on the paths server-side. Only the browser-facing URLs key off the
mount prefix. mount prefix.
Subpages (`stats`, `screen`) are served without a trailing slash so The router serves subpages (`stats`, `screen`) without a trailing slash so
the relative-path resolution stays correct: `static/stats.js` from the relative-path resolution stays correct: `static/stats.js` from
`/stats` becomes `/static/stats.js` (last segment `stats` gets `/stats` becomes `/static/stats.js` (last segment `stats` gets
replaced), not `/stats/static/stats.js`. Adding a trailing slash to replaced), not `/stats/static/stats.js`. Adding a trailing slash to

View file

@ -4,8 +4,8 @@ The per-agent web UI's live pane renders one row per `TermMsg`
(`hive-agent/src/term_msg.rs`): `{icon?, level: debug|info|warn|error, (`hive-agent/src/term_msg.rs`): `{icon?, level: debug|info|warn|error,
summary, body?, body_format?: markdown|diff, coalesce_key?}`. Classification summary, body?, body_format?: markdown|diff, coalesce_key?}`. Classification
(icon, summary text, whether a tool call gets an expandable body) happens (icon, summary text, whether a tool call gets an expandable body) happens
server-side, once — `term_msg.rs` + `stream_enrich.rs` — and is served server-side, once — `term_msg.rs` + `stream_enrich.rs` — and both
identically by both `GET /api/events/history` and `GET /api/events/stream` `GET /api/events/history` and `GET /api/events/stream` serve it identically
as `TermEnvelope { ts, seq?, msgs: TermMsg[] }` frames as `TermEnvelope { ts, seq?, msgs: TermMsg[] }` frames
(`hive-agent/src/web_ui/stream.rs`). (`hive-agent/src/web_ui/stream.rs`).