Files
skills/orchestrator/templates/page-graph3d.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 fdf8722fc1 feat(orchestrator): S5.3 agent states, Status menu and 3D faces
Every Agents view marks each agent as working, alive, sleeping, suspending, unreachable or dead with an animated symbol; a Status menu hides states and switches the 3D faces, kept through a reload. The hub's container sees neither ~/.claude/sessions nor host processes, so the host timer kvb-orchestrator-sessions.timer runs client/sessions.py every 15 seconds while the page polls: it reads the session records without following symlinks, only regular files up to 64 KiB, keeps name, status and statusUpdatedAt within type and length bounds, checks the pid's process start, and writes agent-sessions.json atomically. The hub ignores a snapshot that is missing, malformed, older than five minutes or dated in the future. make orchestrator-install enables the timer and orchestrator-check reports it.

🎯 Fix: 90 %
💥 Break: 5 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 86
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 07:44:02 +00:00

436 lines
17 KiB
JavaScript

const LAYOUTS = [
["none", "Force", "bi-magnet", "Forces arrange freely: linked nodes attract, everything repels"],
["time", "Time", "bi-clock", "Depth = time: earlier lies behind, later in front"],
["spawn", "Spawn depth", "bi-layers", "Depth = spawn depth: each level one generation"],
];
const KIND_COLORS = {operator: "accent", chair: "accent", session: "info", subagent: "violet", twig: "violet", repo: "warn", branch: "info", worktree: "teal", agent: "accent", artefact: "violet", task: "muted"};
const LINK_COLORS = {spawn: "violet", contains: "line-strong", merge: "grant", overlap: "alarm", direct: "direct", broadcast: "broadcast", alarm: "alarm", security: "security", grant: "grant", after: "violet", base: "info", queue: "warn"};
const TONE_ROLES = {ok: "ok", bad: "bad", info: "info", warn: "warn", neutral: "muted"};
const DEPENDENCIES = new Set(["after", "base", "queue"]);
const SPAWNED = new Set(["twig", "subagent"]);
const DEPTH_GAP = 90;
const TIME_DEPTH = 640;
const TOP = {operator: 6, chair: 4, session: 2.5};
const g3 = {lib: null, three: null, THREE: null, graph: null, host: null, tab: "", sig: "", planes: [], colors: null, failed: "", picked: null, fit: false, badges: new Map(), faces: new Set(), textures: new Map(), pulse: 0};
const layoutsOf = (tab) => (tab === "tasks" ? LAYOUTS.filter(([id]) => id !== "spawn") : LAYOUTS);
const layoutOf = (tab) => (layoutsOf(tab).some(([id]) => id === space.views.graph.axis) ? space.views.graph.axis : "none");
function fit3d(ms) {
if (g3.graph) {
g3.graph.zoomToFit(ms, 40);
}
}
function builtFit() {
if (g3.fit) {
g3.fit = false;
fit3d(calm.matches ? 0 : 400);
}
}
function loadScript(src) {
return new Promise((resolve, reject) => {
const tag = document.createElement("script");
tag.src = src;
tag.nonce = NONCE;
tag.async = true;
tag.addEventListener("load", resolve);
tag.addEventListener("error", () => reject(new Error(`${src} not loaded`)));
document.head.append(tag);
});
}
function forceGraph() {
if (!g3.lib) {
g3.lib = loadScript("/vendor/3d-force-graph.min.js").then(() => window.ForceGraph3D);
}
return g3.lib;
}
function threeModule() {
if (!g3.three) {
g3.three = import("/vendor/three.module.js").catch(() => null);
}
return g3.three;
}
function layoutTools(tab) {
const group = h("div", {class: "segmented", role: "group", "aria-label": "Layout"}, layoutsOf(tab).map(([id, label, name, hint]) => h("button", {
type: "button", class: "icon-only", "data-layout": id, ...named(label, `${label}: ${tab === "tasks" && id === "time" ? "depth = start: earlier lies behind, later in front, forecasts behind their predecessors" : hint}`), "aria-pressed": String(layoutOf(tab) === id), on: {click: () => chooseLayout(tab, id, group)},
}, icon(name))));
return h("div", {class: "space-tools"}, group,
iconBtn("bi-arrow-counterclockwise", "Reset view", "btn small ghost", () => fit3d(calm.matches ? 0 : 600)),
h("span", {class: "muted"}, "Drag turns · right drag pans · wheel zooms · click opens the popup"),
nodeSearch(() => model3d(tab).nodes, (node) => pick3d(tab, node)));
}
function chooseLayout(tab, id, group) {
for (const button of group.querySelectorAll("button")) {
button.setAttribute("aria-pressed", String(button.dataset.layout === id));
}
if (id !== layoutOf(tab)) {
setView("graph", {...space.views.graph, axis: id}, false);
g3.sig = "";
g3.fit = true;
if (ui.screen === tab) {
stage3d(tab, g3.host);
}
}
}
function agentModel() {
const depthOf = {operator: 0, chair: 1, session: 2, subagent: 3};
const born = (node) => ((spawn.subs.get(node.id) || {}).data || {}).since;
const nodes = state.graph.nodes.map((node) => ({
id: node.id, kind: node.kind, label: node.label, depth: depthOf[node.kind], ts: born(node) || (node.timeline[0] || {}).since || state.since,
parent: node.kind === "operator" ? null : node.kind === "chair" ? HUMAN : node.kind === "session" ? chairKey() : node.parent || chairKey(),
face: look.faces && Object.hasOwn(TOP, node.kind), badge: node.kind === "chair" || node.kind === "session",
}));
for (const node of state.graph.nodes.filter((entry) => entry.kind === "session" || entry.kind === "chair")) {
const walk = (twig, parent, depth) => {
nodes.push({id: twig.id, kind: "twig", label: twig.label, depth, ts: twig.row.since || twig.row.started, parent, session: twig.session, sub: twig.row.id});
twig.children.forEach((child) => walk(child, twig.id, depth + 1));
};
twigs(node.id).forEach((twig) => walk(twig, node.id, depthOf[node.kind] + 1));
}
const ids = new Set(nodes.map((node) => node.id));
const links = nodes.filter((node) => node.parent && ids.has(node.parent)).map((node) => ({source: node.parent, target: node.id, kind: "spawn"}));
for (const link of aggregate().filter((entry) => entry.target !== "*" && ids.has(entry.source) && ids.has(entry.target) && entry.source !== entry.target)) {
links.push({source: link.source, target: link.target, kind: tone(link.cls), count: link.count});
}
return {nodes: SpaceView.notBeforeParent(nodes, SPAWNED), links};
}
function model3d(tab) {
if (tab === "agents") {
return agentModel();
}
if (tab === "tasks") {
return taskModel3d();
}
const built = tree.model || {nodes: [], links: []};
return {nodes: built.nodes.map((node) => ({id: node.id, kind: node.kind, label: node.label, depth: node.depth, ts: node.ts, parent: node.parent})), links: built.links.map((link) => ({...link}))};
}
function place(nodes, layout) {
const times = nodes.map((node) => node.ts).filter(Number.isFinite);
const [low, high] = [Math.min(...times), Math.max(...times)];
const span = high - low;
for (const node of nodes) {
if (layout === "spawn") {
node.fz = -node.depth * DEPTH_GAP;
} else if (layout === "time") {
node.fz = span > 0 && Number.isFinite(node.ts) ? ((node.ts - low) / span - 0.5) * TIME_DEPTH : 0;
} else {
delete node.fz;
}
}
}
function colors3d() {
const css = window.getComputedStyle(document.documentElement);
const value = (name) => css.getPropertyValue(`--${name}`).trim() || "#888";
const roles = new Set([...Object.values(KIND_COLORS), ...Object.values(LINK_COLORS), ...Object.values(TONE_ROLES), "text", "alarm"]);
g3.colors = Object.fromEntries([...roles].map((role) => [role, value(role)]));
return g3.colors;
}
async function drawPlanes(layout, nodes) {
const graph = g3.graph;
const scene = graph.scene();
for (const plane of g3.planes) {
scene.remove(plane);
}
g3.planes = [];
if (layout === "none") {
return;
}
const THREE = await threeModule();
if (!THREE || g3.graph !== graph) {
return;
}
const levels = [...new Set(nodes.map((node) => node.fz))].filter(Number.isFinite).sort((a, b) => a - b);
const shown = layout === "spawn" ? levels : [levels[0], levels[levels.length - 1]].filter(Number.isFinite);
for (const z of new Set(shown)) {
const grid = new THREE.GridHelper(520, 8, g3.colors["line-strong"], g3.colors["line-strong"]);
grid.rotation.x = Math.PI / 2;
grid.position.z = z;
grid.material.transparent = true;
grid.material.opacity = 0.18;
scene.add(grid);
g3.planes.push(grid);
}
}
function pick3d(tab, node) {
if (node.kind === "twig") {
focusSub(node.session, node.sub);
return;
}
({artifacts: artSelect, tasks: taskSelect}[tab] || select)(node.id, anchor3d(node.id), () => mark3d(null));
mark3d(node.id);
}
function anchor3d(id) {
return () => {
const node = g3.graph && g3.host && g3.host.isConnected ? g3.graph.graphData().nodes.find((entry) => entry.id === id) : null;
if (!node || !Number.isFinite(node.x)) {
return null;
}
const at = g3.graph.graph2ScreenCoords(node.x, node.y, node.z);
const box = g3.host.querySelector(".graph3d").getBoundingClientRect();
return {left: box.left + at.x - 8, top: box.top + at.y - 8, width: 16, height: 16};
};
}
function mark3d(id) {
g3.picked = id;
if (g3.graph) {
g3.graph.nodeColor(g3.graph.nodeColor()).nodeVal(g3.graph.nodeVal());
}
const box = g3.host && g3.host.querySelector(".graph3d");
if (box && id) {
box.dataset.picked = id;
} else if (box) {
delete box.dataset.picked;
}
}
function signature3d(tab, data) {
return JSON.stringify([tab, layoutOf(tab), data.nodes.map((node) => [node.id, node.kind, node.depth, node.ts, node.tone, node.face]), data.links.map((link) => [link.source, link.target, link.kind])]);
}
function canvasTexture(size, paint) {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
paint(canvas.getContext("2d"));
const texture = new g3.THREE.CanvasTexture(canvas);
texture.colorSpace = g3.THREE.SRGBColorSpace;
return texture;
}
function cached(key, make) {
if (!g3.textures.has(key)) {
g3.textures.set(key, make());
}
return g3.textures.get(key);
}
function paintFace(context, key) {
context.scale(128 / Faces.SIZE, 128 / Faces.SIZE);
for (const [tag, part] of Faces.of(key).parts) {
const path = tag === "path" ? new Path2D(part.d) : new Path2D();
if (tag === "ellipse") {
path.ellipse(part.cx, part.cy, part.rx, part.ry, 0, 0, 2 * Math.PI);
} else if (tag === "rect") {
path.roundRect(part.x, part.y, part.width, part.height, part.rx || 0);
}
context.globalAlpha = part.opacity ?? 1;
if (part.fill && part.fill !== "none") {
context.fillStyle = part.fill;
context.fill(path);
}
if (part.stroke) {
context.globalAlpha *= part["stroke-opacity"] ?? 1;
context.strokeStyle = part.stroke;
context.lineWidth = part["stroke-width"] || 1;
context.lineCap = part["stroke-linecap"] || "butt";
context.lineJoin = part["stroke-linejoin"] || "miter";
context.stroke(path);
}
}
}
function badgeTexture(value) {
const [, name, tone] = PRESENCE[value];
const color = g3.colors[tone] || g3.colors.text;
return cached(`${value}:${color}`, () => canvasTexture(64, (context) => {
context.fillStyle = "rgba(255, 255, 255, 0.92)";
context.arc(32, 32, 31, 0, 2 * Math.PI);
context.fill();
context.fillStyle = color;
context.translate(12, 12);
context.scale(2.5, 2.5);
for (const path of document.querySelectorAll(`#${name} path`)) {
context.fill(new Path2D(path.getAttribute("d")), path.getAttribute("fill-rule") || "nonzero");
}
}));
}
function count3d() {
const box = g3.host && g3.host.querySelector(".graph3d");
if (box) {
box.dataset.faces = String(g3.faces.size);
box.dataset.badges = String([...g3.badges.values()].filter((badge) => badge.visible).length);
}
if (!g3.pulse && g3.badges.size && !calm.matches) {
g3.pulse = window.requestAnimationFrame(pulse3d);
}
}
function paintBadge(key, badge) {
const value = presenceOf(key);
badge.visible = value !== null;
badge.userData.state = value;
const map = value ? badgeTexture(value) : null;
if (map && badge.material.map !== map) {
badge.material.map = map;
badge.material.needsUpdate = true;
}
}
function badges3d() {
for (const [key, badge] of g3.badges) {
paintBadge(key, badge);
}
count3d();
}
function pulse3d(now) {
g3.pulse = 0;
if (!g3.graph || !g3.badges.size || calm.matches) {
return;
}
for (const badge of g3.badges.values()) {
const {state: value, size} = badge.userData;
const beat = value === "alive" ? 1 + 0.2 * Math.max(0, Math.sin(now / 150)) : 1;
badge.scale.set(size * beat, size * beat, 1);
badge.material.rotation = value === "working" ? now / 500 : value === "suspending" ? Math.PI * (Math.floor(now / 1200) % 2) : 0;
badge.material.opacity = ["sleeping", "dead", "unreachable"].includes(value) ? 0.6 + 0.4 * Math.cos(now / (value === "unreachable" ? 200 : 900)) ** 2 : 1;
}
g3.pulse = window.requestAnimationFrame(pulse3d);
}
function object3d(node) {
const THREE = g3.THREE;
if (!THREE || !(node.face || node.badge)) {
return null;
}
const radius = Math.cbrt(TOP[node.kind]) * 5;
const group = new THREE.Group();
if (node.face) {
const face = new THREE.Sprite(new THREE.SpriteMaterial({map: cached(`face:${node.id}`, () => canvasTexture(128, (context) => paintFace(context, node.id)))}));
face.scale.set(radius * 2.4, radius * 2.4, 1);
group.add(face);
g3.faces.add(node.id);
}
if (node.badge) {
const badge = new THREE.Sprite(new THREE.SpriteMaterial());
badge.userData.size = radius * 1.1;
badge.scale.set(badge.userData.size, badge.userData.size, 1);
badge.position.set(0, radius * 1.7, 0);
paintBadge(node.id, badge);
g3.badges.set(node.id, badge);
group.add(badge);
}
count3d();
return group;
}
async function stage3d(tab, host) {
if (!host) {
return;
}
if (tab === "agents") {
loadSpawn();
}
const data = model3d(tab);
const sig = signature3d(tab, data);
if (g3.host === host && g3.sig === sig) {
badges3d();
return;
}
if (g3.host !== host) {
dispose3d();
g3.host = host;
g3.tab = tab;
host.replaceChildren(h("div", {class: "graph3d", role: "img", "aria-label": `3D graph of the ${tab}; the search above finds every node`}), h("div", {class: "space-note"}, "Loading 3D …"));
}
g3.sig = sig;
const box = host.querySelector(".graph3d");
let Graph = null;
try {
Graph = await forceGraph();
} catch (error) {
g3.failed = String(error.message || error);
}
if (g3.host !== host) {
return;
}
if (!Graph) {
host.replaceChildren(emptyState("3D not loaded", g3.failed || "3d-force-graph is missing."));
return;
}
if (tab === "agents" && !g3.THREE) {
g3.THREE = await threeModule();
if (g3.host !== host) {
return;
}
}
if (!g3.graph) {
try {
g3.graph = Graph({controlType: "orbit"})(box);
} catch (error) {
host.replaceChildren(emptyState("3D unavailable", `This browser offers no WebGL: ${error.message || error}`));
g3.host = null;
return;
}
const colors = colors3d();
g3.graph.backgroundColor("rgba(0,0,0,0)").showNavInfo(false).nodeRelSize(5).nodeOpacity(0.92).linkOpacity(0.45)
.nodeLabel((node) => h("span", {}, node.label)).nodeVal((node) => ({operator: 6, chair: 4, repo: 6, branch: 3, session: 2.5}[node.kind] || 1) * (node.id === g3.picked ? 4 : 1))
.nodeColor((node) => (node.id === g3.picked ? g3.colors.alarm : g3.colors[node.tone || KIND_COLORS[node.kind]] || colors.text)).linkColor((link) => g3.colors[LINK_COLORS[link.kind]] || colors.text)
.linkWidth((link) => (link.kind === "contains" || link.kind === "spawn" ? 0.6 : Math.min(0.8 + Math.log2(1 + (link.count || 1)), 4)))
.linkDirectionalArrowLength((link) => (DEPENDENCIES.has(link.kind) ? 5 : 0)).linkDirectionalArrowRelPos(1)
.nodeThreeObject(object3d).nodeThreeObjectExtend((node) => !node.face)
.onNodeClick((node) => pick3d(g3.tab, node)).onEngineStop(builtFit);
g3.fit = true;
g3.graph.warmupTicks(calm.matches ? 160 : 80);
if (calm.matches) {
g3.graph.cooldownTicks(0);
} else {
g3.graph.cooldownTime(4000);
}
}
const layout = layoutOf(tab);
place(data.nodes, layout);
g3.badges = new Map();
g3.faces = new Set();
g3.graph.width(box.clientWidth).height(box.clientHeight).graphData(data);
drawPlanes(layout, data.nodes);
const words = `${data.nodes.length} nodes · ${data.links.length} edges · ${LAYOUTS.find(([id]) => id === layout)[1]}${calm.matches ? " · without motion (prefers-reduced-motion)" : ""}`;
host.replaceChildren(box, h("div", {class: "space-note"}, words));
window.requestAnimationFrame(() => g3.host === host && g3.fit && fit3d(0));
}
function dispose3d() {
if (g3.graph) {
g3.graph.pauseAnimation();
if (typeof g3.graph._destructor === "function") {
g3.graph._destructor();
}
}
window.cancelAnimationFrame(g3.pulse);
g3.pulse = 0;
g3.graph = null;
g3.host = null;
g3.sig = "";
g3.planes = [];
g3.badges = new Map();
g3.faces = new Set();
}
function refresh3d() {
if (!g3.graph || !g3.host) {
return;
}
if (!g3.host.isConnected) {
dispose3d();
return;
}
colors3d();
const box = g3.host.querySelector(".graph3d");
g3.graph.width(box.clientWidth).height(box.clientHeight).nodeColor(g3.graph.nodeColor()).linkColor(g3.graph.linkColor());
}