The header radio button opens the radio spot above the main area of the current tab. The Send button, the toolbar radio button and the Radio tab are gone, the mayday clears inside the spot without a Cancel mayday label, and the radio station probe waits for a visible row. 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 🏆 Score: 88 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
10 KiB
JavaScript
190 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));
|
||
});
|
||
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);
|
||
}
|