refactor(frontend): extract the agent roster into dashboard/state.js (#1451)

First step of splitting the 4.3k-line tabs.js monolith into per-tab
modules. The live agent roster (containersState) is the one piece of
genuinely cross-domain state — read by the container tree, the
capabilities + tool-group matrices, the schedule target-chips, the
container-load poll, the selection bar and the operator inbox. Move it
(and its snapshot-sync) into a new dashboard/state.js that each domain
module imports, so the upcoming per-tab extractions have a single source
of truth to depend on instead of a shared IIFE closure.

Minimal by design — only the roster moves; the container row cache and
the apply-handlers (which also re-render) stay with the container domain.
Behaviour-preserving: the imported Map is mutated in place exactly as
before. esbuild inlines state.js into the existing tabs.js bundle, so the
static output is unchanged. Build green.
This commit is contained in:
iris 2026-06-09 11:33:28 +02:00
commit 59054b1f57
2 changed files with 34 additions and 13 deletions

View file

@ -19,6 +19,7 @@ import {
openStream, renderServerWarnings,
} from './common.js';
import { createTabStrip } from '@hive/shared/tabs.js';
import { containersState, syncContainersFromSnapshot } from './state.js';
// mdNode (in common.js) reads `window.marked` for the markdown side
// panel preview path. Set it here on the dashboard entry so file
@ -149,23 +150,18 @@ window.marked = marked;
}
});
// Derived container state — cold-loaded from /api/state.containers,
// then mutated live by `container_state_changed` (upsert by name)
// and `container_removed` (drop by name). The coordinator's rescan
// helper fires these after every mutation site + on a periodic poll
// in crash_watch. Keyed by ContainerView.name so the lifecycle
// forms' POST → 200 → matching event flips the row without a
// snapshot refetch.
const containersState = new Map();
// The live agent roster (`containersState`) + its snapshot-sync now
// live in `./state.js` — it's the one piece of cross-domain state, read
// by nearly every tab. It's still mutated live by `container_state_changed`
// (upsert by name) and `container_removed` (drop by name) via the apply
// handlers just below; the imported binding reflects those in place.
//
// Keyed container row cache. Maps agent name → { el: <li>, fingerprint }.
// Allows renderContainers to skip rebuilding rows whose displayed state
// hasn't changed — prevents full-wipe flicker + avoids redundant async
// dashboard-state fetches on every SSE event.
// dashboard-state fetches on every SSE event. Container-domain only,
// so it stays here (not in state.js).
const containerRowCache = new Map();
function syncContainersFromSnapshot(s) {
containersState.clear();
for (const c of s.containers || []) containersState.set(c.name, c);
}
function applyContainerStateChanged(ev) {
if (!ev.container || !ev.container.name) return;
containersState.set(ev.container.name, ev.container);