diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js
index c9ef2aa6..eb949387 100644
--- a/frontend/packages/dashboard/src/tabs.js
+++ b/frontend/packages/dashboard/src/tabs.js
@@ -156,6 +156,11 @@ window.marked = marked;
// forms' POST → 200 → matching event flips the row without a
// snapshot refetch.
const containersState = new Map();
+ // Keyed container row cache. Maps agent name → { el:
, 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.
+ const containerRowCache = new Map();
function syncContainersFromSnapshot(s) {
containersState.clear();
for (const c of s.containers || []) containersState.set(c.name, c);
@@ -563,6 +568,248 @@ window.marked = marked;
return prefix;
}
+ // Serialise the visible state of a container row into a stable string
+ // for change-detection. Includes everything that affects what the row
+ // renders — container fields, derived pending/selection state, tree
+ // position, and link-base context. The async dashboard-state (nav
+ // strip, ctx badge, status text) is intentionally excluded: it
+ // populates in-place and is preserved when a row is reused.
+ function containerRowFingerprint(c, node, pending, opRunning, selected,
+ askerCount, targetCount, gatewayLinks, hostname) {
+ return JSON.stringify({
+ running: c.running,
+ needs_login: c.needs_login,
+ needs_update: c.needs_update,
+ pending_reminders: c.pending_reminders,
+ port: c.port,
+ pending,
+ opRunning,
+ selected,
+ askerCount,
+ targetCount,
+ depth: node.depth,
+ isLast: node.isLast,
+ ancestorIsLast: node.ancestorIsLast,
+ gatewayLinks,
+ hostname,
+ });
+ }
+
+ // Build a single container-row from scratch. Extracted so
+ // renderContainers can call this only for rows whose fingerprint
+ // changed (keyed cache), skipping the build + async dashboard-state
+ // fetch for stable rows.
+ function buildContainerLi(c, node, opts) {
+ const {
+ pending, opRunning, selected,
+ askerCount, targetCount, agentQCount,
+ url, containerBase, forgeBase, s,
+ } = opts;
+ const li = el('li', {
+ class: 'container-row'
+ + (pending ? ' pending' : '')
+ + (opRunning ? ' pending-running' : '')
+ + (selected ? ' selected' : ''),
+ });
+ // Topology: depth contributes left-padding; the glyph string in
+ // the .tree-prefix span draws the ├─ / └─ joint + continuation
+ // lines (`│ `) for ancestors whose subtree extends below this
+ // row. Both are CSS-driven from the data attributes so the
+ // legacy flat layout (every container at depth 0) is bit-
+ // identical to today's render — no glyph, no indent.
+ if (node.depth > 0) li.dataset.depth = String(node.depth);
+ const prefix = treePrefixDom(node);
+ if (prefix) li.prepend(prefix);
+
+ // Agent icon: 5em square wrapper with an absolutely-positioned
+ //
+ fire-and-forget load with /favicon.svg fallback. The
+ // wrapper doubles as the selection toggle (click / keyboard).
+ // See docs/web-ui.md::Container row for the layout + load-strategy
+ // rationale.
+ const iconImg = el('img', { class: 'container-icon-img', alt: '' });
+ const icon = el('div', {
+ class: 'container-icon',
+ role: 'button',
+ tabindex: '0',
+ 'aria-pressed': selected ? 'true' : 'false',
+ title: selected
+ ? `deselect ${c.name} (or press Esc to clear all)`
+ : `select ${c.name} for bulk actions`,
+ }, iconImg);
+ icon.addEventListener('click', (e) => {
+ e.preventDefault();
+ toggleSelection(c.name);
+ });
+ icon.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleSelection(c.name);
+ }
+ });
+ if (c.running) {
+ iconImg.src = `${url}icon`;
+ iconImg.addEventListener('error', () => {
+ if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s
+ iconImg.dataset.fallback = '1';
+ icon.classList.add('icon-unreachable');
+ iconImg.src = '/favicon.svg';
+ });
+ } else {
+ // Container stopped — skip the doomed `${url}icon` fetch and
+ // go straight to the dimmed hyperhive mark. Avoids a noisy
+ // failed request in the console + the brief broken-image flash.
+ icon.classList.add('icon-unreachable');
+ iconImg.src = '/favicon.svg';
+ }
+ // Card body: the three stacked content lines, right of the icon.
+ const body = el('div', { class: 'card-body' });
+
+ // ── identity ─────────────────────────────────────────────────
+ const head = el('div', { class: 'head' });
+ head.append(
+ el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
+ );
+ // Icon-only nav strip — populated async from the agent's own
+ // `GET /api/dashboard-state` (via gateway when enabled, direct
+ // TCP otherwise). The agent is the single source of truth for its
+ // link list: stats / screen (GUI agents only — c0re's disk-based
+ // fallback cannot detect this) / forge profile / agent-configs /
+ // extras. DOM-built — link strings come from the agent's process
+ // and must never reach the HTML parser.
+ const navStrip = el('span', { class: 'nav-strip' });
+ head.append(navStrip);
+ if (c.running) {
+ // Fetch the lean dashboard-state snapshot from the agent directly.
+ // Populates: nav strip links (including the screen link that
+ // c0re's disk-based build cannot detect), rate_limited badge,
+ // ctx-window badge, and self-reported status text.
+ // Fails gracefully when the agent is starting up or the gateway
+ // is not yet routing to it — badges simply don't appear.
+ // Only runs when the row is first built (fingerprint changed) —
+ // reused rows keep their previously-fetched nav strip + badges.
+ fetch(`${containerBase}/api/dashboard-state`)
+ .then((r) => (r.ok ? r.json() : null))
+ .then((ds) => {
+ if (!ds) return;
+ // ── nav strip ───────────────────────────────────────────
+ if (Array.isArray(ds.links)) {
+ for (const lnk of ds.links) {
+ const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
+ : lnk.kind === 'external' ? (lnk.url || '')
+ : /* container */ containerBase + '/' + (lnk.url || '');
+ const a = el('a', {
+ class: 'nav-link',
+ href,
+ target: '_blank',
+ rel: 'noopener',
+ title: lnk.label || '',
+ });
+ // Plain text — agent-controlled strings stay out of innerHTML.
+ a.textContent = lnk.icon || lnk.label || '';
+ navStrip.append(a);
+ }
+ }
+ // ── agent-owned status badges ────────────────────────────
+ // rate_limited: only show when no other critical badge is
+ // already shown (pending / not-running already handled sync).
+ if (ds.rate_limited) {
+ head.append(el('span',
+ { class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
+ '⊘ rate limited'));
+ }
+ // ctx-window badge
+ if (ds.ctx_tokens != null) {
+ const k = Math.round(ds.ctx_tokens / 1000);
+ const win = ds.context_window_tokens;
+ const warn = win != null ? win * CTX_WARN_FRACTION : CTX_WARN_TOKENS;
+ const caution = win != null ? win * CTX_CAUTION_FRACTION : CTX_CAUTION_TOKENS;
+ const ctxClass = ds.ctx_tokens >= warn ? 'badge-ctx-warn'
+ : ds.ctx_tokens >= caution ? 'badge-ctx-caution'
+ : 'badge-ctx-ok';
+ const title = win != null
+ ? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
+ + `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
+ : `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
+ head.append(el('span', { class: `badge ${ctxClass}`, title }, `ctx·${k}k`));
+ }
+ // ── agent status text (self-reported via set_status) ─────
+ if (ds.status_text) {
+ const nowUnix = Math.floor(Date.now() / 1000);
+ const ageStr = ds.status_set_at != null
+ ? ` (set ${fmtAgeSecs(nowUnix - ds.status_set_at)} ago)` : '';
+ body.append(el('div', {
+ class: 'agent-status',
+ title: `agent self-reported status${ageStr}`,
+ },
+ el('span', { class: 'status-icon' }, '◈ '),
+ ds.status_text,
+ el('span', { class: 'status-age' }, ageStr),
+ ));
+ }
+ })
+ .catch(() => { /* graceful: agent starting / gateway miss → no data */ });
+ }
+ // Status / runtime badges. Pending transients always win
+ // (start / stop / restart / rebuild is in progress). Otherwise,
+ // when the container is stopped, surface a single `■ not running`
+ // badge. `needs_login` is still c0re-owned (reads auth sentinel
+ // files on the host). rate_limited / ctx / status_text are
+ // agent-owned and rendered by the async dashboard-state fetch above.
+ if (pending) {
+ head.append(el('span', { class: 'pending-state' },
+ el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
+ } else if (!c.running) {
+ head.append(el('span',
+ { class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
+ '■ not running'));
+ } else if (c.needs_login) {
+ head.append(el('a',
+ { class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
+ 'needs login →'));
+ }
+ if (c.needs_update) {
+ head.append(form(
+ '/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
+ 'rebuild ' + c.name + '? hot-reloads the container.',
+ {}, { noRefresh: true },
+ ));
+ }
+
+ if (c.pending_reminders && c.pending_reminders > 0) {
+ head.append(el('span',
+ {
+ class: 'badge badge-reminder',
+ title: 'pending reminders queued for this agent — see the reminders section to view / cancel',
+ },
+ `⏰ ${c.pending_reminders}`));
+ }
+ // Pending questions where this agent is the asker (awaiting an
+ // answer) or the target (owes a reply). Derived live from
+ // questionsState so the badge updates instantly on QuestionAdded /
+ // QuestionResolved without a separate backend field.
+ if (agentQCount > 0) {
+ const parts = [];
+ if (askerCount > 0) parts.push(`${askerCount} asked`);
+ if (targetCount > 0) parts.push(`${targetCount} to answer`);
+ head.append(el('span',
+ {
+ class: 'badge badge-loose-ends',
+ title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`,
+ },
+ `❓ ${agentQCount}`));
+ }
+ body.append(head);
+
+ // Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
+ // DESTR0Y / PURG3) moved to the selection bar — see
+ // docs/web-ui.md::Selection bar. The contextual `needs update ↻`
+ // chip in the head row stays — it's a state-hint, not an
+ // action button.
+
+ li.append(icon, body, buildAgentMenu(c, forgeBase));
+ return li;
+ }
+
function renderContainers(s) {
const root = $('containers-section');
// #containers-section only exists on /index.html. tabs.js is the
@@ -572,7 +819,6 @@ window.marked = marked;
// no-op-when-target-absent convention the other renderers
// (renderTombstones, etc.) follow.
if (!root) return;
- root.replaceChildren();
// Containers come from the derived map (event-driven) rather than
// `s.containers`; `s` still supplies hostname (for the web-ui
@@ -582,6 +828,13 @@ window.marked = marked;
const portConflicts = derivePortConflicts(containers);
const anyStale = containers.some((c) => c.needs_update);
+ // Preserve the keyed container list across the header-section
+ // rebuild. We wipe the banners/buttons above the list on every
+ // render (simple), but recycle elements for unchanged rows
+ // (keyed) to avoid full-DOM thrash and redundant async fetches.
+ const existingUl = root.querySelector('ul.containers');
+ root.replaceChildren();
+
// Port-hash collisions: rename one of the listed agents and
// rebuild. The banner sits above the agent list so it's the
// first thing the operator sees when something's wedged.
@@ -637,18 +890,30 @@ window.marked = marked;
// `agent-sockets.json`). See
// `docs/web-ui.md::Container row` + `docs/gateway.md::Vhost map`.
const gatewayLinks = !!(s && s.gateway_enabled);
- const ul = el('ul', { class: 'containers' });
+ // Forge public URL: prefer state.forge_public_url (set by the NixOS
+ // module when forge.behindGateway=true), fall back to
+ // ":3000" for gateway-off / local-dev deploys.
+ const forgeBase = (s && s.forge_public_url) || `http://${hostname}:3000`;
+ const ul = existingUl ?? el('ul', { class: 'containers' });
const tree = buildAgentTree(containers);
// In-flight rebuild / meta-update / destroy ops per agent name —
// see docs/web-ui.md::Container row for the building… badge
// rationale (covers the SYST3M-shows-rebuild-but-SW4RM-shows-stopped
// gap when no operator transient is set).
const inFlight = inFlightOpsByAgent();
+
+ // Build the ordered list of elements, reusing cached rows
+ // whose displayed state hasn't changed.
+ const orderedLis = [];
for (const node of tree) {
const c = node.container;
const url = gatewayLinks
? `/agent/${encodeURIComponent(c.name)}/`
: `http://${hostname}:${c.port}/`;
+ // Container nav-strip base: gateway prefix or direct TCP.
+ const containerBase = gatewayLinks
+ ? `/agent/${encodeURIComponent(c.name)}`
+ : `http://${hostname}:${c.port}`;
// Pending-state derivation + queued-vs-running split — see
// docs/web-ui.md::Container row for the transient → in-flight
// queue priority order and the opRunning rationale.
@@ -667,222 +932,57 @@ window.marked = marked;
const opRunning = transientKind != null
|| (op != null && op.state === 'running');
const selected = selectionState.has(c.name);
- const li = el('li', {
- class: 'container-row'
- + (pending ? ' pending' : '')
- + (opRunning ? ' pending-running' : '')
- + (selected ? ' selected' : ''),
- });
- // Topology: depth contributes left-padding; the glyph string in
- // the .tree-prefix span draws the ├─ / └─ joint + continuation
- // lines (`│ `) for ancestors whose subtree extends below this
- // row. Both are CSS-driven from the data attributes so the
- // legacy flat layout (every container at depth 0) is bit-
- // identical to today's render — no glyph, no indent.
- if (node.depth > 0) li.dataset.depth = String(node.depth);
- const prefix = treePrefixDom(node);
- if (prefix) li.prepend(prefix);
-
- // Agent icon: 5em square wrapper with an absolutely-positioned
- //
+ fire-and-forget load with /favicon.svg fallback. The
- // wrapper doubles as the selection toggle (click / keyboard).
- // See docs/web-ui.md::Container row for the layout + load-strategy
- // rationale.
- const iconImg = el('img', { class: 'container-icon-img', alt: '' });
- const icon = el('div', {
- class: 'container-icon',
- role: 'button',
- tabindex: '0',
- 'aria-pressed': selected ? 'true' : 'false',
- title: selected
- ? `deselect ${c.name} (or press Esc to clear all)`
- : `select ${c.name} for bulk actions`,
- }, iconImg);
- icon.addEventListener('click', (e) => {
- e.preventDefault();
- toggleSelection(c.name);
- });
- icon.addEventListener('keydown', (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- toggleSelection(c.name);
- }
- });
- if (c.running) {
- iconImg.src = `${url}icon`;
- iconImg.addEventListener('error', () => {
- if (iconImg.dataset.fallback) return; // guard: don't loop if the favicon itself 404s
- iconImg.dataset.fallback = '1';
- icon.classList.add('icon-unreachable');
- iconImg.src = '/favicon.svg';
- });
- } else {
- // Container stopped — skip the doomed `${url}icon` fetch and
- // go straight to the dimmed hyperhive mark. Avoids a noisy
- // failed request in the console + the brief broken-image flash.
- icon.classList.add('icon-unreachable');
- iconImg.src = '/favicon.svg';
- }
- // Card body: the three stacked content lines, right of the icon.
- const body = el('div', { class: 'card-body' });
-
- // ── identity ─────────────────────────────────────────────────
- const head = el('div', { class: 'head' });
- head.append(
- el('a', { class: 'name', href: url, target: '_blank', rel: 'noopener' }, c.name),
- );
- // Icon-only nav strip — populated async from the agent's own
- // `GET /api/dashboard-state` (via gateway when enabled, direct
- // TCP otherwise). The agent is the single source of truth for its
- // link list: stats / screen (GUI agents only — c0re's disk-based
- // fallback cannot detect this) / forge profile / agent-configs /
- // extras. DOM-built — link strings come from the agent's process
- // and must never reach the HTML parser.
- const navStrip = el('span', { class: 'nav-strip' });
- head.append(navStrip);
- // Forge public URL: prefer state.forge_public_url (set by the NixOS
- // module when forge.behindGateway=true, e.g.
- // "https://forge.pr1ma.darkest.space"), fall back to
- // ":3000" for gateway-off / local-dev deploys.
- const forgeBase = (s && s.forge_public_url) || `http://${hostname}:3000`;
- // Container nav-strip base: gateway prefix or direct TCP.
- const containerBase = gatewayLinks
- ? `/agent/${encodeURIComponent(c.name)}`
- : `http://${hostname}:${c.port}`;
- if (c.running) {
- // Fetch the lean dashboard-state snapshot from the agent directly.
- // Populates: nav strip links (including the screen link that
- // c0re's disk-based build cannot detect), rate_limited badge,
- // ctx-window badge, and self-reported status text.
- // Fails gracefully when the agent is starting up or the gateway
- // is not yet routing to it — badges simply don't appear.
- fetch(`${containerBase}/api/dashboard-state`)
- .then((r) => (r.ok ? r.json() : null))
- .then((ds) => {
- if (!ds) return;
- // ── nav strip ───────────────────────────────────────────
- if (Array.isArray(ds.links)) {
- for (const lnk of ds.links) {
- const href = lnk.kind === 'forge' ? forgeBase + (lnk.url || '')
- : lnk.kind === 'external' ? (lnk.url || '')
- : /* container */ containerBase + '/' + (lnk.url || '');
- const a = el('a', {
- class: 'nav-link',
- href,
- target: '_blank',
- rel: 'noopener',
- title: lnk.label || '',
- });
- // Plain text — agent-controlled strings stay out of innerHTML.
- a.textContent = lnk.icon || lnk.label || '';
- navStrip.append(a);
- }
- }
- // ── agent-owned status badges ────────────────────────────
- // rate_limited: only show when no other critical badge is
- // already shown (pending / not-running already handled sync).
- if (ds.rate_limited) {
- head.append(el('span',
- { class: 'badge badge-rate-limited', title: 'API rate-limited — harness is parked, will retry automatically' },
- '⊘ rate limited'));
- }
- // ctx-window badge
- if (ds.ctx_tokens != null) {
- const k = Math.round(ds.ctx_tokens / 1000);
- const win = ds.context_window_tokens;
- const warn = win != null ? win * CTX_WARN_FRACTION : CTX_WARN_TOKENS;
- const caution = win != null ? win * CTX_CAUTION_FRACTION : CTX_CAUTION_TOKENS;
- const ctxClass = ds.ctx_tokens >= warn ? 'badge-ctx-warn'
- : ds.ctx_tokens >= caution ? 'badge-ctx-caution'
- : 'badge-ctx-ok';
- const title = win != null
- ? `last turn context: ${ds.ctx_tokens.toLocaleString()} / ${win.toLocaleString()} `
- + `tokens (${Math.round((ds.ctx_tokens / win) * 100)}% of the window)`
- : `last turn context size: ${ds.ctx_tokens.toLocaleString()} tokens`;
- head.append(el('span', { class: `badge ${ctxClass}`, title }, `ctx·${k}k`));
- }
- // ── agent status text (self-reported via set_status) ─────
- if (ds.status_text) {
- const nowUnix = Math.floor(Date.now() / 1000);
- const ageStr = ds.status_set_at != null
- ? ` (set ${fmtAgeSecs(nowUnix - ds.status_set_at)} ago)` : '';
- body.append(el('div', {
- class: 'agent-status',
- title: `agent self-reported status${ageStr}`,
- },
- el('span', { class: 'status-icon' }, '◈ '),
- ds.status_text,
- el('span', { class: 'status-age' }, ageStr),
- ));
- }
- })
- .catch(() => { /* graceful: agent starting / gateway miss → no data */ });
- }
- // Status / runtime badges. Pending transients always win
- // (start / stop / restart / rebuild is in progress). Otherwise,
- // when the container is stopped, surface a single `■ not running`
- // badge. `needs_login` is still c0re-owned (reads auth sentinel
- // files on the host). rate_limited / ctx / status_text are
- // agent-owned and rendered by the async dashboard-state fetch above.
- if (pending) {
- head.append(el('span', { class: 'pending-state' },
- el('span', { class: 'spinner' }, '◐'), ' ', pending + '…'));
- } else if (!c.running) {
- head.append(el('span',
- { class: 'badge badge-muted', title: 'container is shut down — start it to bring the harness back up' },
- '■ not running'));
- } else if (c.needs_login) {
- head.append(el('a',
- { class: 'badge badge-warn', href: url, target: '_blank', rel: 'noopener' },
- 'needs login →'));
- }
- if (c.needs_update) {
- head.append(form(
- '/rebuild/' + c.name, 'badge badge-warn btn-inline', 'needs update ↻',
- 'rebuild ' + c.name + '? hot-reloads the container.',
- {}, { noRefresh: true },
- ));
- }
-
- if (c.pending_reminders && c.pending_reminders > 0) {
- head.append(el('span',
- {
- class: 'badge badge-reminder',
- title: 'pending reminders queued for this agent — see the reminders section to view / cancel',
- },
- `⏰ ${c.pending_reminders}`));
- }
// Pending questions where this agent is the asker (awaiting an
// answer) or the target (owes a reply). Derived live from
// questionsState so the badge updates instantly on QuestionAdded /
// QuestionResolved without a separate backend field.
+ const askerCount = questionsState.pending.filter((q) => q.asker === c.name).length;
+ const targetCount = questionsState.pending.filter((q) => q.target === c.name).length;
const agentQCount = questionsState.pending.filter(
(q) => q.asker === c.name || q.target === c.name,
).length;
- if (agentQCount > 0) {
- const askerCount = questionsState.pending.filter((q) => q.asker === c.name).length;
- const targetCount = questionsState.pending.filter((q) => q.target === c.name).length;
- const parts = [];
- if (askerCount > 0) parts.push(`${askerCount} asked`);
- if (targetCount > 0) parts.push(`${targetCount} to answer`);
- head.append(el('span',
- {
- class: 'badge badge-loose-ends',
- title: `pending questions: ${parts.join(', ')} — see the Q33R1ES tab`,
- },
- `❓ ${agentQCount}`));
+
+ const fp = containerRowFingerprint(c, node, pending, opRunning, selected,
+ askerCount, targetCount, gatewayLinks, hostname);
+ const cached = containerRowCache.get(c.name);
+
+ let li;
+ if (cached && cached.fingerprint === fp) {
+ // Row unchanged — reuse the existing DOM node. The async
+ // dashboard-state (nav strip, ctx badge, status text) stays
+ // intact from the previous build, avoiding a redundant fetch.
+ li = cached.el;
+ } else {
+ li = buildContainerLi(c, node, {
+ pending, opRunning, selected,
+ askerCount, targetCount, agentQCount,
+ url, containerBase, forgeBase, s,
+ });
+ containerRowCache.set(c.name, { el: li, fingerprint: fp });
}
- body.append(head);
-
- // Per-card action buttons (R3ST4RT / ST0P / ST4RT / R3BU1LD /
- // DESTR0Y / PURG3) moved to the selection bar — see
- // docs/web-ui.md::Selection bar. The contextual `needs update ↻`
- // chip in the head row stays — it's a state-hint, not an
- // action button.
-
- li.append(icon, body, buildAgentMenu(c, forgeBase));
- ul.append(li);
+ orderedLis.push(li);
}
+
+ // Remove cache entries for agents that no longer exist.
+ for (const [name, entry] of containerRowCache) {
+ if (!liveNames.has(name)) {
+ entry.el.remove();
+ containerRowCache.delete(name);
+ }
+ }
+
+ // Apply correct DOM order without a full wipe. insertBefore is a
+ // no-op when the node is already at position i, so stable sections
+ // of the list cause zero layout work.
+ for (let i = 0; i < orderedLis.length; i++) {
+ if (ul.children[i] !== orderedLis[i]) {
+ ul.insertBefore(orderedLis[i], ul.children[i] ?? null);
+ }
+ }
+ // Trim any excess children (defensive — shouldn't happen after the
+ // cache-removal pass above, but keeps the ul length exact).
+ while (ul.children.length > orderedLis.length) ul.lastChild.remove();
+
root.append(ul);
renderSelectionBar(containers);
}