From 8288b457daf4f0623b43b5f08c255f74a8a67f92 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 25 May 2026 00:19:51 +0100 Subject: [PATCH] Tighten swarm dashboard layout --- scripts/render-voidbot-swarm-dashboard.mjs | 167 +++++++++++++++------ state/evidence.jsonl | 1 + 2 files changed, 122 insertions(+), 46 deletions(-) diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs index b5d902f..9e85e9b 100644 --- a/scripts/render-voidbot-swarm-dashboard.mjs +++ b/scripts/render-voidbot-swarm-dashboard.mjs @@ -658,7 +658,6 @@ function renderHtml(snapshot) { --coral: #ff7b72; --violet: #b99dff; --rail: 124px; - --status-size: clamp(112px, 16vmin, 150px); --font: "Ubuntu", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --display: "Montserrat", "Ubuntu", ui-sans-serif, system-ui, sans-serif; --arcade: "Press Start 2P", "VT323", monospace; @@ -714,6 +713,7 @@ function renderHtml(snapshot) { background: rgba(3, 7, 13, 0.86); backdrop-filter: blur(18px); overflow: auto; + min-width: 0; scrollbar-width: thin; scrollbar-color: rgba(142, 223, 176, 0.58) rgba(3, 7, 13, 0.32); } @@ -768,21 +768,25 @@ function renderHtml(snapshot) { gap: 12px; padding: 12px; overflow: hidden; - grid-template-columns: minmax(210px, 0.78fr) minmax(280px, 1.06fr) minmax(320px, 1.34fr) minmax(240px, 0.86fr); + grid-template-columns: minmax(230px, 0.82fr) minmax(360px, 1.15fr) minmax(420px, 1.38fr); grid-template-rows: minmax(0, 1fr); grid-template-areas: - "inspector tree memory command"; + "inspector tree memory"; } .status-panel { - width: 100%; - aspect-ratio: 1 / 1; - align-self: start; - justify-self: stretch; + position: sticky; + right: 10px; + z-index: 2; + flex: 0 0 250px; + width: 250px; + height: 96px; + margin-left: auto; + align-self: center; display: grid; grid-template-rows: auto minmax(0, 1fr); - gap: 7px; - padding: 9px; + gap: 5px; + padding: 7px; border: 1px solid rgba(255, 174, 88, 0.34); border-radius: 8px; background: @@ -810,7 +814,7 @@ function renderHtml(snapshot) { min-height: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 5px 7px; + gap: 4px 7px; align-content: start; } @@ -825,7 +829,7 @@ function renderHtml(snapshot) { justify-content: space-between; gap: 3px; color: rgba(239, 252, 248, 0.74); - font-size: 0.5rem; + font-size: 0.43rem; line-height: 1; text-transform: uppercase; white-space: nowrap; @@ -844,7 +848,7 @@ function renderHtml(snapshot) { } .hud-bar { - height: 5px; + height: 4px; overflow: hidden; border: 1px solid rgba(239, 252, 248, 0.16); border-radius: 999px; @@ -874,10 +878,10 @@ function renderHtml(snapshot) { overflow: hidden; } .pane-scroll { overflow: auto; } - .inspector-pane { grid-area: inspector; grid-template-rows: auto auto minmax(0, 1fr); } + .inspector-pane { grid-area: inspector; grid-template-rows: auto auto auto minmax(0, 1fr); } .tree-pane { grid-area: tree; grid-template-rows: auto minmax(0, 1fr); } + .tree-pane .mono { overflow-wrap: anywhere; } .memory-pane { grid-area: memory; grid-template-rows: auto minmax(0, 1fr); } - .command-pane { grid-area: command; grid-template-rows: minmax(0, auto) minmax(0, 1fr); align-content: start; } .kicker { color: var(--green); font-size: 0.74rem; text-transform: uppercase; } h1 { font-size: clamp(1.55rem, 3.4vw, 3rem); line-height: 0.98; } @@ -886,7 +890,7 @@ function renderHtml(snapshot) { .mono { font-family: var(--mono); } .metrics { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; } - .metric, .fact { + .metric, .fact, .agent-stat { min-width: 0; padding: 10px; border: 1px solid rgba(142, 223, 176, 0.12); @@ -897,7 +901,15 @@ function renderHtml(snapshot) { .metric strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 1.12rem; } .fact strong { display: block; overflow-wrap: anywhere; font: 0.9rem/1.15 var(--mono); } - .control-grid { display: grid; gap: 10px; align-content: start; } + .control-grid { + display: grid; + gap: 8px; + align-content: start; + padding: 10px; + border: 1px solid rgba(105, 226, 239, 0.16); + border-radius: 8px; + background: rgba(3, 7, 13, 0.36); + } .control-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; } .force-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; } @@ -914,6 +926,43 @@ function renderHtml(snapshot) { color: var(--muted); font-size: 0.83rem; } + .agent-stats { display: grid; gap: 8px; } + .agent-stat { + display: grid; + gap: 6px; + padding: 8px; + } + .agent-stat-head { + display: flex; + justify-content: space-between; + gap: 10px; + color: rgba(239, 252, 248, 0.88); + font: 0.72rem/1.1 var(--mono); + text-transform: uppercase; + } + .agent-stat-head span:first-child { + color: var(--muted); + font-family: var(--display); + font-weight: 300; + } + .agent-bar { + height: 8px; + overflow: hidden; + border: 1px solid rgba(239, 252, 248, 0.16); + border-radius: 3px; + background: rgba(0, 0, 0, 0.38); + box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.34); + } + .agent-fill { + display: block; + width: calc(var(--fill, 0) * 1%); + max-width: 100%; + height: 100%; + background: linear-gradient(90deg, var(--green), var(--cyan)); + box-shadow: 0 0 12px rgba(105, 226, 239, 0.28); + } + .agent-fill.hot { background: linear-gradient(90deg, var(--amber), var(--coral)); } + .agent-fill.cool { background: linear-gradient(90deg, var(--violet), var(--cyan)); } .channel-list, .state-tree, .detail-body { min-height: 0; overflow: auto; @@ -938,7 +987,7 @@ function renderHtml(snapshot) { border: 1px solid rgba(142, 223, 176, 0.12); border-radius: 8px; background: rgba(142, 223, 176, 0.045); - overflow: hidden; + overflow: clip; } details.state-node[open] { border-color: rgba(105, 226, 239, 0.22); } details.state-node > summary { @@ -954,7 +1003,7 @@ function renderHtml(snapshot) { .state-children { display: grid; gap: 6px; - padding: 0 8px 8px 14px; + padding: 6px 8px 8px 12px; border-top: 1px solid rgba(142, 223, 176, 0.08); } .state-leaf { @@ -969,7 +1018,20 @@ function renderHtml(snapshot) { text-align: left; } .state-leaf:hover, .state-leaf.selected { border-color: var(--amber); background: rgba(255, 174, 88, 0.09); } - .state-leaf strong, .state-leaf span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .state-leaf strong { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow-wrap: anywhere; + } + .state-leaf span { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow-wrap: anywhere; + } .state-leaf span { color: var(--muted); font: 0.72rem/1.15 var(--mono); } .facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } @@ -1036,27 +1098,27 @@ function renderHtml(snapshot) { .workspace { grid-row: 1; grid-column: 2; - grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); - grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); - grid-template-areas: - "inspector command" + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); + grid-template-areas: + "inspector memory" "tree memory"; } - .status-panel { max-height: 34vh; } .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } .turn-card { width: 72px; height: 96px; } + .status-panel { flex-basis: 230px; width: 230px; height: 84px; } } @media (max-width: 760px) { .workspace { padding: 10px; gap: 10px; } .pane { padding: 12px; } .inspector-lore { display: none; } - .status-panel { max-height: none; } + .status-panel { flex-basis: 210px; width: 210px; } } @media (max-width: 980px) and (orientation: landscape) { .workspace { - grid-template-columns: minmax(180px, 0.8fr) minmax(220px, 1fr) minmax(260px, 1.16fr) minmax(210px, 0.86fr); + grid-template-columns: minmax(190px, 0.8fr) minmax(300px, 1.1fr) minmax(340px, 1.28fr); } .inspector-lore { display: none; } } @@ -1099,14 +1161,14 @@ function renderHtml(snapshot) { } const selected = participants.find((agent) => agent.identityId === selectedIdentity) || participants[0] || null; const selectedLeaf = findSelectedLeaf(selected); + if (selectedLeaf) selectedStatePath = selectedLeaf.path; app.innerHTML = [ "
", - renderCtb(snapshot.upcomingTurns || []), + renderCtb(snapshot.upcomingTurns || [], snapshot, selected), "
", - renderInspector(selected), + renderInspector(snapshot, participants, selected), renderStateTree(selected), renderStateDetail(selected, selectedLeaf), - renderCommandColumn(snapshot, participants, selected), "
", "
", ].join(""); @@ -1125,23 +1187,24 @@ function renderHtml(snapshot) { }); } - function renderCtb(turns) { + function renderCtb(turns, snapshot, selected) { return ""; + }).join("") + renderStatusPanel(snapshot, selected) + ""; } - function renderInspector(agent) { + function renderInspector(snapshot, participants, agent) { if (!agent) return "

Face

No selected Face.

"; const counts = agent.faceState?.counts || {}; - return "
" + avatarHtml(agent) + "

Selected Face

" + esc(agent.displayName) + "

" + esc(agent.identityId) + " / " + esc(agent.repoName) + "

" + - fact("Turn", minutes(agent.nextTurnInMinutes)) + - fact("Status", agent.activeJobId ? "running" : agent.status) + - fact("Memory", number((counts.shortTerm || 0) + (counts.memories || 0))) + - fact("Pressure", number(counts.pressures || 0)) + - fact("Heat", number(agent.heat)) + - fact("Load", number(agent.currentLoad)) + + const memoryCount = (counts.shortTerm || 0) + (counts.memories || 0); + return "
" + renderControls(snapshot, participants) + "
" + avatarHtml(agent) + "

Selected Face

" + esc(agent.displayName) + "

" + esc(agent.identityId) + " / " + esc(agent.repoName) + "

" + + statBar("Turn", minutes(agent.nextTurnInMinutes), turnPercent(agent.nextTurnInMinutes), "cool") + + statBar("Memory", number(memoryCount), Math.min(100, memoryCount * 7), "cool") + + statBar("Pressure", number(counts.pressures || 0), Math.min(100, (counts.pressures || 0) * 14), (counts.pressures || 0) > 4 ? "hot" : "") + + statBar("Heat", number(agent.heat), Math.min(100, (agent.heat || 0) * 34), (agent.heat || 0) > 1.5 ? "hot" : "") + + statBar("Load", number(agent.currentLoad), Math.min(100, (agent.currentLoad || 0) * 100), (agent.currentLoad || 0) > 0.7 ? "hot" : "") + + statBar("Speed", number(agent.effectiveSpeed), Math.min(100, (agent.effectiveSpeed || 0) * 34), "") + "

" + esc(agent.description || "No Face description registered.") + "

" + (agent.channelPermissions || []).map((channel) => "
" + esc(channel.label || "channel") + "x" + esc(number(channel.speedMultiplier || 1)) + "" + esc(channel.topic || "no topic") + "" + esc(channel.speechThreshold || "threshold") + "
").join("") + "
"; } @@ -1155,17 +1218,27 @@ function renderHtml(snapshot) { "
"; } + function statBar(label, value, fill, variant = "") { + return "
" + esc(label) + "" + esc(value) + "
"; + } + + function turnPercent(value) { + if (typeof value !== "number") return 0; + return value <= 0 ? 100 : clampPercent(100 - value * 5); + } + function renderStateTree(agent) { const state = agent?.faceState; if (!agent) return "

State Graph

No Face selected.

"; if (!state?.readable) return "

State Graph

" + esc(state?.error || "Face state unreadable.") + "

"; - return "

State Graph

" + esc(agent.displayName) + " memory tree

" + esc(state.path || "state path missing") + "

" + renderTreeNodes(state.tree || []) + "
"; + return "

State Graph

" + esc(agent.displayName) + " memory tree

" + esc(state.path || "state path missing") + "

" + renderTreeNodes(state.tree || [], 0) + "
"; } - function renderTreeNodes(nodes) { + function renderTreeNodes(nodes, depth = 0) { return (nodes || []).map((node) => { if (node.kind === "branch") { - return "
" + esc(node.label) + "" + esc(node.count || 0) + "
" + renderTreeNodes(node.children || []) + "
"; + const open = depth === 0 || branchContainsSelection(node); + return "
" + esc(node.label) + "" + esc(node.count || 0) + "
" + renderTreeNodes(node.children || [], depth + 1) + "
"; } const klass = "state-leaf" + (node.path === selectedStatePath ? " selected" : ""); return ""; @@ -1181,10 +1254,6 @@ function renderHtml(snapshot) { return "

State Detail

" + esc(selected.title || selected.label) + "

" + esc(selected.path) + "

" + esc(selected.detail || selected.preview || "") + "
"; } - function renderCommandColumn(snapshot, participants, selected) { - return "
" + renderStatusPanel(snapshot, selected) + renderControls(snapshot, participants) + "
"; - } - function findSelectedLeaf(agent) { const leaves = flattenLeaves(agent?.faceState?.tree || []); if (!leaves.length) return null; @@ -1218,6 +1287,12 @@ function renderHtml(snapshot) { return output; } + function branchContainsSelection(node) { + if (!selectedStatePath) return false; + if (node.path && selectedStatePath.startsWith(node.path)) return true; + return (node.children || []).some((child) => child.kind === "branch" ? branchContainsSelection(child) : child.path === selectedStatePath); + } + function renderStatusPanel(snapshot, agent) { const summary = snapshot.summary || {}; const organs = snapshot.orchestrator?.organs || []; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 715f300..ee7cd73 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -333,3 +333,4 @@ {"ts":"2026-05-24T22:48:35Z","type":"implementation","status":"ok","note":"Repo Face heartbeat parallelism increased: live REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK set to 3, and tracked default/example now allow three concurrent independent Face turns per CTB tick while active-turn freeze still prevents duplicate turns per Face."} {"ts":"2026-05-24T23:05:48Z","type":"implementation","status":"ok","note":"Operator absence now drives Face sleep projection: when idle cooling reaches nap mode, repo Face sleep cycles are forced/extended into napping with operator-absence-dreaming and memory-distillation themes before speaking pressure, CTB eligibility, and nap memory maintenance are computed."} {"ts":"2026-05-24T23:06:38Z","type":"implementation","status":"ok","note":"Rebuilt the local swarm dashboard into a CTB-owned four-column SPA: Face inspector, typed Face-state tree, selected memory/detail viewer, and command/watchdog column. The page now reads registered Face .cc state through the typed loader for display-only memory browsing while cadence/manual-turn controls remain the only writers."} +{"ts":"2026-05-24T23:19:32Z","type":"implementation","status":"ok","note":"Adjusted the local swarm dashboard layout from iPad screenshot evidence: watchdog HUD is now a sticky module in the CTB rail, controls moved to the top of the Face inspector, agent facts became game-style stat bars, and the Face state tree gets the freed column width with readable wrapping plus selected-path expansion."}