Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # docs/requirements/001-agent-orchestrator.md # orchestrator/templates/page-board.js # tests/e2e/test_orchestrator_e2e.py
191 lines
10 KiB
JavaScript
191 lines
10 KiB
JavaScript
function orbit() {
|
||
const nodes = state.graph.nodes;
|
||
positions = layout(nodes);
|
||
const svg = chartFrame(400, "Orbit: operator in the middle; whoever works or radios together moves closer, the silent drift outward");
|
||
svg.append(...RINGS.map((r) => s("circle", {class: "ring", r})));
|
||
const edgeLayer = s("g", {});
|
||
const halos = new Map();
|
||
for (const link of aggregate()) {
|
||
const from = positions.get(link.source);
|
||
const shade = tone(link.cls);
|
||
const words = `${link.count}× ${CLASSES[link.cls] || link.cls}`;
|
||
if (!from) {
|
||
continue;
|
||
}
|
||
if (link.target === "*") {
|
||
const index = halos.get(link.source) || 0;
|
||
halos.set(link.source, index + 1);
|
||
edgeLayer.append(s("circle", {class: `halo t-${shade}`, cx: fixed(from.x), cy: fixed(from.y), r: radiusOf(link.source) + 7 + index * 7, "stroke-width": width(link.count)},
|
||
s("title", {}, `${link.source} to all: ${words}`)));
|
||
continue;
|
||
}
|
||
const to = positions.get(link.target);
|
||
if (!to || to === from) {
|
||
continue;
|
||
}
|
||
const control = Space.bend(from, to);
|
||
const start = toward(from, control, radiusOf(link.source) + 2);
|
||
const end = toward(to, control, radiusOf(link.target) + 4);
|
||
edgeLayer.append(s("path", {class: `edge t-${shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(link.count)), "marker-end": `url(#tip-${shade})`},
|
||
s("title", {}, `${link.source} → ${link.target}: ${words}`)));
|
||
}
|
||
const nodeLayer = s("g", {}, nodes.map((node) => nodeMark(node, positions.get(node.id), node.kind === "operator" || node.kind === "subagent" ? null : RADIUS[node.kind] + 9)));
|
||
const pulseLayer = s("g", {});
|
||
svg.append(edgeLayer, nodeLayer, pulseLayer);
|
||
graphDom.pulses = pulseLayer;
|
||
return svg;
|
||
}
|
||
|
||
function chord() {
|
||
const nodes = state.graph.nodes.filter((node) => node.kind !== "subagent");
|
||
const fresh = targets();
|
||
const radius = 250;
|
||
const step = (2 * Math.PI) / Math.max(nodes.length, 1);
|
||
const spots = new Map(nodes.map((node) => {
|
||
const angle = spotOf(node.id, fresh).chord;
|
||
return [node.id, {x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle}];
|
||
}));
|
||
const svg = chartFrame(450, "Chord: messages between agents, ordered by closeness");
|
||
const arcs = s("g", {});
|
||
const point = (angle, r) => `${fixed(r * Math.cos(angle))},${fixed(r * Math.sin(angle))}`;
|
||
for (const node of nodes) {
|
||
const at = spots.get(node.id);
|
||
const half = Math.min(step * 0.36, 0.5);
|
||
arcs.append(s("path", {class: `group-arc k-${node.kind}`, d: `M${point(at.angle - half, radius + 14)} A${radius + 14},${radius + 14} 0 0 1 ${point(at.angle + half, radius + 14)}`}));
|
||
}
|
||
const pairs = new Map();
|
||
const fans = new Map();
|
||
for (const link of aggregate()) {
|
||
const shade = tone(link.cls);
|
||
if (!spots.has(link.source)) {
|
||
continue;
|
||
}
|
||
if (link.target === "*") {
|
||
const key = `${link.source}\u0000${shade}`;
|
||
fans.set(key, {source: link.source, shade, count: (fans.has(key) ? fans.get(key).count : 0) + link.count});
|
||
continue;
|
||
}
|
||
if (!spots.has(link.target) || link.target === link.source) {
|
||
continue;
|
||
}
|
||
const [a, b] = [link.source, link.target].sort();
|
||
const key = `${a}\u0000${b}\u0000${shade}`;
|
||
pairs.set(key, {a, b, shade, count: (pairs.has(key) ? pairs.get(key).count : 0) + link.count});
|
||
}
|
||
const lanes = LEGEND.map(([name]) => name);
|
||
for (const pair of pairs.values()) {
|
||
const from = spots.get(pair.a);
|
||
const to = spots.get(pair.b);
|
||
const pull = 0.06 + 0.12 * lanes.indexOf(pair.shade);
|
||
const control = {x: ((from.x + to.x) / 2) * pull, y: ((from.y + to.y) / 2) * pull};
|
||
const start = toward(from, control, RADIUS.session + 2);
|
||
const end = toward(to, control, RADIUS.session + 2);
|
||
arcs.append(s("path", {class: `edge t-${pair.shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(pair.count))},
|
||
s("title", {}, `${pair.a} ↔ ${pair.b}: ${pair.count}× ${LEGEND.find(([name]) => name === pair.shade)[1]}`)));
|
||
}
|
||
const offsets = new Map();
|
||
for (const fan of fans.values()) {
|
||
const at = spots.get(fan.source);
|
||
const index = offsets.get(fan.source) || 0;
|
||
offsets.set(fan.source, index + 1);
|
||
const angle = at.angle + (index - 1) * 0.05;
|
||
const length = Math.min(8 + 6 * Math.log2(1 + fan.count), 40);
|
||
arcs.append(s("line", {class: `tick t-${fan.shade}`, x1: fixed((radius + 22) * Math.cos(angle)), y1: fixed((radius + 22) * Math.sin(angle)), x2: fixed((radius + 22 + length) * Math.cos(angle)), y2: fixed((radius + 22 + length) * Math.sin(angle)), "stroke-width": 4},
|
||
s("title", {}, `${fan.source} to all: ${fan.count}×`)));
|
||
}
|
||
svg.append(arcs, s("g", {}, nodes.map((node) => nodeMark(node, spots.get(node.id), 74))));
|
||
return svg;
|
||
}
|
||
|
||
function swimlane() {
|
||
const rank = new Map([HUMAN, ...state.graph.layout.order].map((id, index) => [id, index]));
|
||
const lanes = state.graph.nodes.filter((node) => node.kind !== "subagent").sort((a, b) => (rank.has(a.id) ? rank.get(a.id) : 1e6) - (rank.has(b.id) ? rank.get(b.id) : 1e6));
|
||
const begin = state.since;
|
||
const end = Math.max(state.until, state.since + 1);
|
||
const left = 160;
|
||
const right = 24;
|
||
const top = 40;
|
||
const lane = 36;
|
||
const full = 1120;
|
||
const height = top + lanes.length * lane + 12;
|
||
const x = (ts) => left + ((ts - begin) / (end - begin)) * (full - left - right);
|
||
const y = new Map(lanes.map((node, index) => [node.id, top + index * lane + lane / 2]));
|
||
const svg = s("svg", {viewBox: `0 0 ${full} ${height}`, class: "chart", role: "img", "aria-label": "Swimlane: messages over time"}, defs());
|
||
lanes.forEach((node, index) => {
|
||
svg.append(s("rect", {class: index % 2 ? "lane odd" : "lane", x: 0, y: top + index * lane, width: full, height: lane}));
|
||
svg.append(s("text", {class: "lane-label", x: 14, y: top + index * lane + lane / 2, dy: "0.35em", "text-anchor": "start", "data-pick": node.id, on: {click: () => select(node.id)}}, node.label));
|
||
svg.append(...[presenceGlyph(node.id, left - 14, top + index * lane + lane / 2, 12)].filter(Boolean));
|
||
});
|
||
for (let index = 0; index <= 6; index += 1) {
|
||
const ts = begin + ((end - begin) * index) / 6;
|
||
svg.append(s("line", {class: "grid", x1: fixed(x(ts)), x2: fixed(x(ts)), y1: top - 6, y2: height - 8}));
|
||
svg.append(s("text", {class: "axis", x: fixed(x(ts)), y: top - 14, "text-anchor": index === 0 ? "start" : index === 6 ? "end" : "middle"}, (end - begin > DAY ? absolute : clock).format(stampOf(ts))));
|
||
}
|
||
if (live()) {
|
||
svg.append(s("line", {class: "now", x1: fixed(x(end)), x2: fixed(x(end)), y1: top - 6, y2: height - 8}));
|
||
}
|
||
const marks = s("g", {});
|
||
const arrows = (cls, ts, from, to, words) => {
|
||
const x0 = fixed(x(ts));
|
||
const y0 = y.get(from);
|
||
if (y0 === undefined) {
|
||
return;
|
||
}
|
||
const shade = tone(cls);
|
||
const hint = s("title", {}, `${clock.format(stampOf(ts))} · ${dash(labelOf(from))} → ${to.length ? to.map(labelOf).join(", ") : "all"}\n${words}`);
|
||
if (!to.length) {
|
||
const rays = [[-6, -13], [0, -15], [6, -13], [-6, 13], [0, 15], [6, 13]].map(([dx, dy]) => `M${x0},${y0} l${dx},${dy}`).join(" ");
|
||
marks.append(s("g", {class: `mark t-${shade}`}, hint, s("path", {d: rays}), s("circle", {cx: x0, cy: y0, r: 3.5})));
|
||
return;
|
||
}
|
||
const bundle = s("g", {class: `mark t-${shade}`}, hint, s("circle", {cx: x0, cy: y0, r: 3}));
|
||
for (const target of to) {
|
||
const y1 = y.get(target);
|
||
if (y1 !== undefined && y1 !== y0) {
|
||
bundle.append(s("line", {x1: x0, y1: y0, x2: x0, y2: y1 > y0 ? y1 - 6 : y1 + 6, "marker-end": `url(#tip-${shade})`}));
|
||
}
|
||
}
|
||
marks.append(bundle);
|
||
};
|
||
for (const row of state.radio) {
|
||
arrows(row.class, row.ts, row.source, row.targets, row.text.length > 120 ? `${row.text.slice(0, 120)}…` : row.text);
|
||
}
|
||
for (const grant of state.grants) {
|
||
arrows("grant", grant.ts, chairKey(), [grant.target], `Queue grant ${grant.kind} (${grant.id})`);
|
||
}
|
||
svg.append(marks);
|
||
return svg;
|
||
}
|
||
|
||
function legend() {
|
||
return h("div", {class: "legend"}, LEGEND.map(([name, label]) => h("span", {}, h("i", {class: `swatch t-${name}`}), label)),
|
||
h("span", {class: "muted"}, "Line width = count · closeness = recent collaboration"));
|
||
}
|
||
|
||
function nodePopup(node) {
|
||
const messages = state.radio.filter((row) => row.source === node.id || row.targets.includes(node.id)).slice(-10).reverse();
|
||
return [
|
||
popupHead(null, node.label, h("span", {class: "muted"}, NODE_KINDS[node.kind])),
|
||
h("div", {class: "popup-body"}, h("h3", {}, "Latest messages", counter(messages.length)),
|
||
messages.length ? h("ul", {class: "mini"}, messages.map((row) => h("li", {},
|
||
h("div", {class: "row"}, klass(row.class), when(row.ts)),
|
||
h("span", {class: "muted"}, `${dash(row.from)} → ${targetsOf(row)}`),
|
||
h("span", {}, row.text)))) : h("p", {class: "muted"}, "No messages in the chosen time range.")),
|
||
];
|
||
}
|
||
|
||
function viewSwitch(screen) {
|
||
return h("div", {class: "segmented", role: "group", "aria-label": "View"}, Nav.viewsOf(screen).map(([id, name]) => h("button", {
|
||
type: "button", class: "icon-only", ...named(name), "aria-pressed": String(ui.views[screen] === id), on: {click: () => go(screen, id)},
|
||
}, icon(Nav.ICONS[id]))));
|
||
}
|
||
|
||
function agentsScreen() {
|
||
const view = ui.views.agents;
|
||
const stageBox = h("div", {class: "stage"});
|
||
const toolbar = agentsToolbar();
|
||
graphDom = {stage: stageBox, pulses: null, toolbar};
|
||
const body = view === "tree" ? spawnScreen() : view === "hierarchy" ? hierarchyScreen() : h("div", {class: "card stage-card"}, stageBox, view === "3d" ? null : legend());
|
||
return h("div", {class: "graph"}, toolbar.bar, h("div", {class: "card controls"}, viewSwitch("agents"), view === "3d" ? layoutTools("agents") : null), body);
|
||
}
|