docs(dashboard): fix misplaced CR3D3NTIALS content, dedupe, trim history

This commit is contained in:
iris 2026-08-15 11:53:15 +02:00 committed by mara
commit 785b59a4bb

View file

@ -212,8 +212,8 @@ Three sub-tabs: **R3BU1LD QU3U3** (default), **M3T4 1NPUTS**,
`/api/state` and subscribes to `/api/dashboard/stream` for `/api/state` and subscribes to `/api/dashboard/stream` for
`rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`. `rebuild_queue_changed`, `meta_inputs_changed`, `meta_update_running`.
The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at The SW4RM tab (`/dashboard.html`) does **not** read this endpoint at
all — mara, on review: "swarm.js should not need to pull in the jobq all, by design — the swarm view stays independent of job-queue
to do its job." Its per-agent pending badges are transient-only, and internals. Its per-agent pending badges are transient-only, and
its queue-summary banner reads the much narrower `GET /api/jobq/rollup` its queue-summary banner reads the much narrower `GET /api/jobq/rollup`
instead (see Container row, below) — a handful of pre-tallied counts, instead (see Container row, below) — a handful of pre-tallied counts,
not the graph. not the graph.
@ -239,23 +239,21 @@ array order, unchanged from the wire). A row shows a state glyph (`⏸`
pending / `▶` running / `◐` finishing — own work done, a sub-node pending / `▶` running / `◐` finishing — own work done, a sub-node
still running / `✔` done / `✖` failed / `⊘` cancelled / `·` skipped) still running / `✔` done / `✖` failed / `⊘` cancelled / `·` skipped)
and each step's own label/agent. A `Node`-kind dep on a sibling shows and each step's own label/agent. A `Node`-kind dep on a sibling shows
as a plain "waits on: `<label>`" text line under the row — an earlier as a plain "waits on: `<label>`" text line under the row rather than a
gutter-rail version was reverted (it broke visually whenever a nested gutter rail — a rail breaks visually whenever a nested subtree sits
subtree sat between the two related rows; text has no such gap). between the two related rows, since a text line needs no continuous
vertical space to draw.
`builds.js` mounts the element with `cancellable` set, which turns on `builds.js` mounts the element with `cancellable` set, which turns on
a per-node cancel button (`✕`) on any non-terminal row — the button a per-node cancel button (`✕`) on any non-terminal row — the button
dispatches `hive-jobq-graph-cancel`, and the page does the actual dispatches `hive-jobq-graph-cancel`, and the page does the actual
`POST /api/rebuild-queue/{id}/cancel`, matching the wire event as its `POST /api/rebuild-queue/{id}/cancel`, matching the wire event as its
own domain concept (the component knows nothing about that endpoint). own domain concept (the component knows nothing about that endpoint).
**Still no source chip, kind label, timing, or build-log deep-link on **Rows carry no source chip, kind label, timing, or build-log
rows** — no equivalent of the old `DagView`'s `source`/`reason`/ deep-link** — the generic graph wire doesn't carry those fields, and
`created_at`, which were `NodeKind::Dag`-specific fields the generic rows are meant to present exactly what the endpoint provides rather
wire doesn't carry; per mara's original steer ("dont feel constrained than reconstruct chrome the backend no longer sends. Settled entries
by what the ui does currently"), rows present what the endpoint render their **full step tree**, not just a bare summary — the wire
actually gives rather than reconstructing the old per-row chrome. does not filter `Done` nodes out.
Settled entries render their **full step tree**, not just a bare
summary — unlike the old `DagView` projection, this wire does not
filter `Done` nodes out.
**State filter (hyperhive#2606).** A row of per-state checkboxes above **State filter (hyperhive#2606).** A row of per-state checkboxes above
the tree — one per lifecycle state, matching the row glyphs — lets the the tree — one per lifecycle state, matching the row glyphs — lets the
@ -359,24 +357,6 @@ The status dot renders these states:
!live` "provisioned but offline" case. !live` "provisioned but offline" case.
- **grey** — no token (not provisioned). - **grey** — no token (not provisioned).
### GITHUB tab
Provision a single per-agent GitHub personal access token (see
[`docs/github.md`](../github.md) for the injection + `gh`/git-push
mechanics). No login flow — the operator pastes an existing PAT for a
dedicated bot account, with a security-warning banner (dedicated account +
minimally-scoped token) and a link to
[github.com/settings/tokens](https://github.com/settings/tokens).
Status reads `GET /api/github-account?agent=<name>`
`{ present: bool }` — whether the agent's `github-token` file exists.
There's no live/heartbeat concept for a static PAT, so this is just a
"token stored ✓" / "not set" line, unlike MATRIX's status-dot taxonomy.
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
`token`) → `200 { ok: true }` on success, or the same `error_response`
shape `/api/matrix-account-login` uses on failure. The token is never
echoed back in either direction.
The container-down cross-reference (`/api/state`) takes precedence over The container-down cross-reference (`/api/state`) takes precedence over
the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout the age check. `as_of_unix` is tooltipped ("live as of N ago") throughout
so freshness is always legible. When `live` is absent (an older backend so freshness is always legible. When `live` is absent (an older backend
@ -398,6 +378,24 @@ regardless of outcome. The account list reflects what is *provisioned*
(an account with a stored token), so a config-declared-but-unprovisioned (an account with a stored token), so a config-declared-but-unprovisioned
account appears only once it has been provisioned through the form. account appears only once it has been provisioned through the form.
### GITHUB tab
Provision a single per-agent GitHub personal access token (see
[`docs/github.md`](../github.md) for the injection + `gh`/git-push
mechanics). No login flow — the operator pastes an existing PAT for a
dedicated bot account, with a security-warning banner (dedicated account +
minimally-scoped token) and a link to
[github.com/settings/tokens](https://github.com/settings/tokens).
Status reads `GET /api/github-account?agent=<name>`
`{ present: bool }` — whether the agent's `github-token` file exists.
There's no live/heartbeat concept for a static PAT, so this is just a
"token stored ✓" / "not set" line, unlike MATRIX's status-dot taxonomy.
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
`token`) → `200 { ok: true }` on success, or the same `error_response`
shape `/api/matrix-account-login` uses on failure. The token is never
echoed back in either direction.
### FORGES tab ### FORGES tab
Store a **label + base URL + access token** for an external Forgejo/Gitea/ Store a **label + base URL + access token** for an external Forgejo/Gitea/
@ -467,10 +465,8 @@ The current capabilities are:
Each row is one agent. Columns are the capability names returned by Each row is one agent. Columns are the capability names returned by
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking `GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
boxes only stages the change in-browser; nothing is written until the boxes only stages the change in-browser; nothing is written until the
page-level **save all** button (described below) is clicked. The page-level **save all** button (described below) is clicked. Row
checkboxes reflect the *effective* set (explicit grant or role default), values follow the `effective`/`assignments` rule described above.
so a default-perms agent shows its real grants rather than blank; absent
agents in the assignment map have no extra capabilities.
**T00L GR0UPS** — per-agent tool-group permissions. Tool groups are **T00L GR0UPS** — per-agent tool-group permissions. Tool groups are
named buckets of MCP tools; each agent starts with a role default named buckets of MCP tools; each agent starts with a role default
@ -752,9 +748,8 @@ isn't currently listed.
The row is a `flex-wrap: wrap` container holding ts / arrow / from / sep The row is a `flex-wrap: wrap` container holding ts / arrow / from / sep
/ to chips inline; the **body wraps to its own full-width line below** the / to chips inline; the **body wraps to its own full-width line below** the
chips (`flex: 1 1 100%`) so the body always gets the full row width down to chips (`flex: 1 1 100%`) so long timestamps and agent names never push the
the content edge — long timestamps + agent names used to push the body ~30ch body into a narrow trailing column. `min-width: 0` keeps
in and force awkward narrow-column wraps. `min-width: 0` keeps
`word-break: break-word` effective so the body doesn't force the row wider `word-break: break-word` effective so the body doesn't force the row wider
than its container. Sticky-bottom auto-scroll + "↓ N new" pill. Below the than its container. Sticky-bottom auto-scroll + "↓ N new" pill. Below the
stream sits a terminal-style compose box: `@name` picks the recipient stream sits a terminal-style compose box: `@name` picks the recipient
@ -783,9 +778,8 @@ latter — never guessed from the operator's browser hostname + a container
port, which is only right by accident off plain localhost. Operators without port, which is only right by accident off plain localhost. Operators without
matrix or forge enabled — or with forge on but no public URL configured — matrix or forge enabled — or with forge on but no public URL configured —
never see a dead or wrong link. `home.js` also fills the swarm/hive identity never see a dead or wrong link. `home.js` also fills the swarm/hive identity
line at the top. Dashboard is now served at `/dashboard.html` (route swap line at the top. All dashboard sub-pages include a `← Home` back-link for
completed in #1464 step 2); the home page at `/` replaces the old dashboard navigation.
root. All dashboard sub-pages include a `← Home` back-link for navigation.
## L0GS page (`/logs.html`) ## L0GS page (`/logs.html`)
@ -862,122 +856,127 @@ is known stopped up front (`ContainerView.running = false`) the
fallback fires immediately, skipping the doomed `<url>/icon` fallback fires immediately, skipping the doomed `<url>/icon`
fetch entirely. fetch entirely.
- Line 1: agent name (link → new tab), m1nd/ag3nt chip, an **Line 1** — agent name (link → new tab), m1nd/ag3nt chip, an
**icon-only nav strip** plus live agent-owned state, all populated **icon-only nav strip** plus live agent-owned state, all populated
async from a single `GET /api/dashboard-state` call to the async from a single `GET /api/dashboard-state` call to the agent's
agent's own backend. The response (`DashboardState`) carries: own backend. The response (`DashboardState`) carries: `links` (nav
`links` (nav strip entries — `📊 stats`, `🖥 screen` when GUI is strip entries — `📊 stats`, `🖥 screen` when GUI is enabled, `⬡ forge
enabled, `⬡ forge profile`, `↳ agent-configs mirror`, plus any profile`, `↳ agent-configs mirror`, plus any agent-declared
agent-declared `dashboardLinks` extras), `status_text` / `dashboardLinks` extras), `status_text` / `status_set_at` (agent
`status_set_at` (agent self-reported status — the `(set N ago)` self-reported status — the `(set N ago)` chip is stamped `data-set-at`
chip is stamped `data-set-at` and ticks every 30s to stay fresh and ticks every 30s to stay fresh across the long-lived keyed row
across the long-lived keyed row cache), `rate_limited`, cache), `rate_limited`, `ctx_tokens` / `context_window_tokens`
`ctx_tokens` / `context_window_tokens` (context-window badge (context-window badge data). The agent backend is the single source
data). The agent backend is the single source of truth for all of truth for all of these. The dashboard resolves each `AgentLink.kind`
of these. The dashboard resolves each `AgentLink.kind` against a against a per-agent base URL depending on whether hive-gateway is in
per-agent base URL depending on whether hive-gateway is in front front (`StateSnapshot.gateway_enabled`, sourced from the
(`StateSnapshot.gateway_enabled`, sourced from the `HIVE_GATEWAY_ENABLED` env the c0re NixOS module sets when
`HIVE_GATEWAY_ENABLED` env the c0re NixOS module sets when `services.hyperhive.gateway.enable = true`). Gateway-on (default):
`services.hyperhive.gateway.enable = true`). Gateway-on (default): base URL is `/agent/<name>` (same origin, gateway proxies to the
base URL is `/agent/<name>` (same origin, gateway proxies to the per-agent harness — TCP or unix-domain depending on the agent's
per-agent harness — TCP or unix-domain depending on the agent's `HIVE_WEB_SOCKET` opt-in, see
`HIVE_WEB_SOCKET` opt-in, see `docs/gateway.md::Per-agent unix-socket upstream`).
`docs/gateway.md::Per-agent unix-socket upstream`). Gateway-off (legacy / local dev): base URL is
Gateway-off (legacy / local dev): base URL is `http://<host>:<container.port>` (direct TCP fallback). Forge
`http://<host>:<container.port>` (direct TCP fallback). Forge links resolve against `state.forge_public_url` (sourced from
links resolve against `state.forge_public_url` (sourced from `services.hyperhive.forge.publicUrl`) and are omitted entirely when
`services.hyperhive.forge.publicUrl`) and are omitted entirely when that's unset — never guessed from `<host>:3000`. External links are
that's unset — never guessed from `<host>:3000`. External links are already absolute. The same base URL drives the primary agent-name
already absolute. The same base URL drives the primary agent-name link + favicon fetch, so the whole row routes through the gateway
link + favicon fetch, so the whole row routes through the gateway as a unit.
as a unit.
**When the container is stopped** (`ContainerView.running = false`),
the async `dashboard-state` fetch is skipped entirely (the agent
web server is down), so the badge chain is replaced by a single
badge, the nav strip is empty, and status text / rate-limited / ctx
badges are suppressed. The agent icon goes straight to the dimmed
`/favicon.svg` fallback instead of attempting a doomed load from the
container's URL. Static fields — `needs_update`, `deployed_sha`,
`pending_reminders`, `parent`, `config` link — remain visible
regardless of run state.
**Which single badge (hyperhive#3139):** `ContainerView.failed`
(systemd `ActiveState=failed` — the unit exhausted its bounded
restarts and gave up on its own) draws a red `✖ gave up` badge;
otherwise a plain muted `■ not running` — a container an operator
stopped deliberately. Both states read `running: false`; `failed` is
the orthogonal fact (a fifth one alongside `paused`/`needs_update`/
`needs_login`, same "independent flags, no state machine" shape —
see `ContainerView`'s own doc comment) that tells them apart. An
older backend without the field serves `failed: undefined`, which
reads falsy — degrades cleanly to the single `not running` badge.
When the container is running, status badges follow — `⊘ rate
limited` (red, while the harness is parked after a 429), `needs
login`, `needs update` — plus **one `◐ pending-state…` pill per
active transient** (replaces buttons during operator-initiated
start / stop / restart / rebuild / destroy). An agent can carry
**several transients at once** (mara: "show all running nodes that
name the agent") — e.g. a lease-exempt `prebuild` running alongside
a `stop_for_update` on the same agent — and each renders as its own
independent badge rather than being collapsed into one label,
matching the existing multi-badge convention this line already uses
for `paused`/`needs_update`/model/ctx.
Any pending badge means the row is actually **running** something
right now — there is no separate queued-but-not-started row state
to visually distinguish it from (see **Pending-badge derivation**
below), so every row carrying ≥1 badge keeps the amber row tint AND
draws a **rotating amber ring** around the agent icon.
**Pending-badge derivation:** transients only (`transientsState`, **When the container is stopped** (`ContainerView.running = false`),
keyed `agent -> Map<kind, since_unix>`) — a transient is **derived the async `dashboard-state` fetch is skipped entirely (the agent
from a job-queue node currently `Running`** against that agent, not web server is down), so the badge chain is replaced by a single
declared per request, so its label follows the operation as it badge, the nav strip is empty, and status text / rate-limited / ctx
progresses (a rebuild reads `stop_for_update`, then `swap`, then badges are suppressed. The agent icon goes straight to the dimmed
`reconcile` rather than one constant `rebuilding` for its whole `/favicon.svg` fallback instead of attempting a doomed load from the
life). Two consequences for anything rendering it: container's URL. Static fields — `needs_update`, `deployed_sha`,
`pending_reminders`, `parent`, `config` link — remain visible
regardless of run state.
- The label vocabulary is **open** — it is the node's own wire tag **Line 2** — status badges only (no per-card action buttons — actions
(`NodeKind::as_str`, the same strings the graph wire's node moved to the **selection bar** or the **per-agent `⋮` menu**, see
labels carry), below).
not a fixed set. Treat it as an opaque display string; do not
switch on specific values. `restarting` in particular no longer
exists, because no node kind is unique to a restart.
- It is **not** exclusively operator-initiated, and **not** limited
to rebuild-shaped work — `running_transients()` on the backend is
a status-only test (any `Running` node whose payload names a
non-empty agent lights a pill), so work the operator never
clicked (a meta-update cascade, a crash-recover rebuild, a
lease-exempt `prebuild`) lights the same mechanism.
Ops with no queue node behind them (destroy, migration) supply ### Status badges
their own label directly via `TransientSet`/`TransientCleared`
events carrying no backing node at all.
**Queued (not-yet-started) work shows nothing on the card.** An **Which single badge (hyperhive#3139):** `ContainerView.failed`
earlier version of this page had a job-queue-backed fallback badge (systemd `ActiveState=failed` — the unit exhausted its bounded
for the `Pending` case (`queuedOpsByAgent()`, reading a `GET restarts and gave up on its own) draws a red `✖ gave up` badge;
/api/jobq/graph` fetch); removed per mara, on review of otherwise a plain muted `■ not running` — a container an operator
hyperhive#3028's PR: *"swarm.js should not need to pull in the jobq stopped deliberately. Both states read `running: false`; `failed` is
to do its job,"* followed by *"remove the per agent pending stuff - the orthogonal fact (a fifth one alongside `paused`/`needs_update`/
only show what is running."* Per-agent badges never came back — only `needs_login`, same "independent flags, no state machine" shape —
the queue-summary banner below did, once a narrow enough endpoint see `ContainerView`'s own doc comment) that tells them apart. An
existed for it to read instead of the full graph. older backend without the field serves `failed: undefined`, which
An **active model badge** (`model · <name>`, blue) appears when the reads falsy — degrades cleanly to the single `not running` badge.
container is running and the harness has persisted a model name
(`harness/hyperhive-model`). Read by hive-c0re's `ContainerView` When the container is running, status badges follow — `⊘ rate
(`read_active_model`); absent until the agent has completed at least limited` (red, while the harness is parked after a 429), `needs
one turn and stale values are suppressed for stopped containers. login`, `needs update` — plus **one `◐ pending-state…` pill per
A `ctx · Nk` chip showing the agent's last-turn context size, active transient** (replaces buttons during operator-initiated
populated from `DashboardState.ctx_tokens` (absent until the start / stop / restart / rebuild / destroy). An agent can carry
agent has completed at least one turn). The chip colour (green / **several transients at once** — e.g. a lease-exempt `prebuild`
yellow / red) is keyed off `DashboardState.context_window_tokens` running alongside a `stop_for_update` on the same agent — and each
(the real context window for the model the agent last ran on, renders as its own independent badge rather than being collapsed
authoritative from the agent side); the badge goes yellow ≥ 50% into one label, matching the existing multi-badge convention this
and red ≥ 75% of that window, matching the harness compaction line already uses for `paused`/`needs_update`/model/ctx.
watermarks. When the window value is absent the badge falls back
to fixed 100k / 150k thresholds. Any pending badge means the row is actually **running** something
- Line 2: status badges only (no per-card action buttons — actions right now — there is no separate queued-but-not-started row state
moved to the **selection bar** or the **per-agent `⋮` menu**, see to visually distinguish it from (see **Pending-badge derivation**
below). below), so every row carrying ≥1 badge keeps the amber row tint AND
draws a **rotating amber ring** around the agent icon.
**Pending-badge derivation:** transients only (`transientsState`,
keyed `agent -> Map<kind, since_unix>`) — a transient is **derived
from a job-queue node currently `Running`** against that agent, not
declared per request, so its label follows the operation as it
progresses (a rebuild reads `stop_for_update`, then `swap`, then
`reconcile` rather than one constant `rebuilding` for its whole
life). Two consequences for anything rendering it:
- The label vocabulary is **open** — it is the node's own wire tag
(`NodeKind::as_str`, the same strings the graph wire's node
labels carry), not a fixed set. Treat it as an opaque display
string; do not switch on specific values. `restarting` in
particular no longer exists, because no node kind is unique to a
restart.
- It is **not** exclusively operator-initiated, and **not** limited
to rebuild-shaped work — `running_transients()` on the backend is
a status-only test (any `Running` node whose payload names a
non-empty agent lights a pill), so work the operator never
clicked (a meta-update cascade, a crash-recover rebuild, a
lease-exempt `prebuild`) lights the same mechanism.
Ops with no queue node behind them (destroy, migration) supply
their own label directly via `TransientSet`/`TransientCleared`
events carrying no backing node at all.
**Queued (not-yet-started) work shows nothing on the card.** Only
running work gets a per-agent pending badge — by design, there is no
fallback badge for work that's merely `Pending` in the queue, since
the swarm view stays independent of job-queue internals. The queue-
summary banner below is the only queued-work indicator on this tab,
and it reads the narrow `/api/jobq/rollup` endpoint rather than the
full graph.
**Active model badge** (`model · <name>`, blue) appears when the
container is running and the harness has persisted a model name
(`harness/hyperhive-model`). Read by hive-c0re's `ContainerView`
(`read_active_model`); absent until the agent has completed at least
one turn and stale values are suppressed for stopped containers.
**`ctx · Nk` chip** shows the agent's last-turn context size,
populated from `DashboardState.ctx_tokens` (absent until the
agent has completed at least one turn). The chip colour (green /
yellow / red) is keyed off `DashboardState.context_window_tokens`
(the real context window for the model the agent last ran on,
authoritative from the agent side); the badge goes yellow ≥ 50%
and red ≥ 75% of that window, matching the harness compaction
watermarks. When the window value is absent the badge falls back
to fixed 100k / 150k thresholds.
**Per-agent `⋮` overflow menu** — a `⋮` button appears on the right **Per-agent `⋮` overflow menu** — a `⋮` button appears on the right
edge of each container row. Clicking it opens a small dropdown with edge of each container row. Clicking it opens a small dropdown with
@ -1011,19 +1010,11 @@ fixed order, zero counts included — rather than the full
`/api/jobq/graph` tree: `running` sums the `Running` and `Finishing` `/api/jobq/graph` tree: `running` sums the `Running` and `Finishing`
entries' `roots` (`Finishing` = own work done, subtree still going, entries' `roots` (`Finishing` = own work done, subtree still going,
still in flight), `queued` reads the `Pending` entry's `roots`. still in flight), `queued` reads the `Pending` entry's `roots`.
`roots` specifically, not `nodes` — the banner has always meant *N `roots` specifically, not `nodes` — the banner means *N whole
whole operations*, not raw steps (one rebuild is ~7 nodes but 1 operations*, not raw steps (one rebuild is ~7 nodes but 1 root);
root); `nodes` exists on the same endpoint for a consumer that wants `nodes` exists on the same endpoint for a consumer that wants
step-level counts instead, unused here. step-level counts instead, unused here.
This banner went through two prior shapes before landing here, both
per mara review comments on hyperhive#3028's PR: a client-side
derivation over the full graph (*"swarm.js should not need to pull in
the jobq to do its job"*), then removed entirely rather than keep
that interim fetch (*"dont replace one legacy thing with another"*).
Restored once the dedicated rollup endpoint (hyperhive#3033) existed
for it to read directly instead.
### Themed dialogs ### Themed dialogs
All confirmations, prompts, and transient error notices use an All confirmations, prompts, and transient error notices use an
@ -1065,67 +1056,35 @@ the dashboard and CLI paths behave identically.)
### Topology tree ### Topology tree
Container rows render as a forest, not a flat list — each agent See **SW4RM tab** above for the parent/child derivation, sibling
sits indented under its declared parent. `swarm.js::buildAgentTree` sort order, and cycle-safety rules (`swarm.js::buildAgentTree` walks
walks `ContainerView.parent` for every container in the snapshot `ContainerView.parent`) — this section covers only how the tree is
and produces a render order with per-row depth + sibling-position *drawn*.
info:
- Top-level rows are agents with `parent = null` OR a parent that
doesn't appear in the container map (orphans get hoisted to root
so they're still visible).
- Within each level children sort alphabetically by name; roots
likewise.
- Cycle safety: any container not reached during the root-walk is
appended at the end as a root, so no agent ever silently
disappears from the list when the topology JSON is malformed.
- The pre-topology rendering shape (every container at depth 0,
flat list) collapses to the same visual today when no parent
field is set — bit-identical fallback path.
The per-row prefix column (`.tree-prefix`) is **DOM-painted, not The per-row prefix column (`.tree-prefix`) is **DOM-painted, not
text-glyph-painted**. Each indent lane is its own positioned text-glyph-painted**: each indent lane is its own positioned `<span>`
`<span>` so CSS can draw full-height vertical bars that bridge the so CSS can draw full-height vertical bars that bridge the gap between
gap between sibling rows; using text box-drawing characters sibling rows. Plain text box-drawing characters (`├─`, `└─`, `│ `)
(`├─`, `└─`, `│ `) only paints one text-line tall and leaves would only paint one text-line tall and leave visible breaks between
visible breaks between the taller-than-one-line container cards. the taller-than-one-line container cards, so the bars are drawn as
The bars come in two flavours: continuation (the ancestor's CSS borders instead: a continuation bar runs the full height of an
subtree extends below this row → vertical line top→bottom) or ancestor's still-open subtree, and the joint at a row's own depth is
blank (ancestor was the last sibling at its level → no line `├` (more siblings below) or `└` (last sibling — the line stops at
needed). The joint at the row's own depth column is `├` (more the row's icon midline). Exact lane widths and positioning live in
siblings below) or `└` (last sibling at this depth — vertical `swarm.js`'s tree-prefix rendering and its paired CSS rules — not
stops at the row's icon midline). reproduced here since they're tuned in pixel units and will drift.
When every container is at depth 0 (no `parent` set) these rules are
**Indent + lane geometry.** Each depth level shifts the row right all no-ops and the layout reads like a plain flat list.
by `1.8em` (the lane width). The per-depth ladders are hardcoded
for six levels — enough for any plausible hive topology, and the
typed `attr()` function from CSS Values 5 that would collapse
this to one rule is still partial-support (Chromium-only as of
2026). The `.tree-prefix` span sits absolutely positioned with
`left: -<depth>*1.8em` so its right edge meets the row content
(the icon) and its leftmost lane lines up with top-level rows'
icons at `x = 0`. Each `.tree-lane` is `flex: 0 0 1.8em` so all
lanes have equal width. Continuation bars are drawn at lane
center (`left: 0.6em`, `border-left: 1px solid currentColor`,
`top: 0; bottom: 0`) and extend through `.containers { gap: 0.4em }`
into the next sibling's prefix (`bottom: -0.4em` on the prefix
span itself) so adjacent ancestor lines visually merge into one
unbroken vertical line. The horizontal stub at a row's own joint
lands at the icon midline so the L/T meets the icon edge cleanly.
When every container has `parent = null` (pre-topology state) the
`[data-depth]` attribute is absent on every row and these rules
are no-ops — the layout reads exactly like the legacy flat list.
## Selection bar ## Selection bar
Per-card action buttons (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` / Bulk actions (`R3ST4RT` / `ST0P` / `ST4RT` / `R3BU1LD` / `DESTR0Y` /
`DESTR0Y` / `PURG3`) used to live on each container row; the `PURG3`) live here rather than as per-card buttons — see **Container
operator picked the bulk-bar model instead. Clicking an agent's row** above. Clicking an agent's icon toggles its selection (an
icon toggles its selection (an in-memory `Set<name>`); `Esc` or in-memory `Set<name>`); `Esc` or the bar's `✕ clear` button drops
the bar's `✕ clear` button drops everything. The selection everything. The selection persists across tab switches in-memory —
persists across tab switches in-memory — the bar just hides on the bar just hides on non-SW4RM tabs since other tabs don't show the
non-SW4RM tabs since other tabs don't show the agent cards needed agent cards needed to cross-reference.
to cross-reference.
When one or more agents are selected (via icon click), a sticky When one or more agents are selected (via icon click), a sticky
frosted-mauve bar slides up from the bottom of the viewport frosted-mauve bar slides up from the bottom of the viewport