feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
// Auto-layout the pipeline graph with dagre, so adding a node in the backend
|
|
|
|
|
// graph needs no manual coordinates here — the map redraws itself.
|
|
|
|
|
import dagre from "@dagrejs/dagre";
|
|
|
|
|
import type { Edge, Node } from "reactflow";
|
|
|
|
|
import type { Topology, TopoNode } from "./api";
|
|
|
|
|
|
|
|
|
|
export const NODE_W = 184;
|
|
|
|
|
export const NODE_H = 72;
|
|
|
|
|
|
|
|
|
|
export interface RFData {
|
|
|
|
|
meta: TopoNode;
|
|
|
|
|
state: "idle" | "active" | "awaiting_human" | "parked";
|
|
|
|
|
count: number;
|
|
|
|
|
dimmed: boolean;
|
|
|
|
|
onPath: boolean;
|
2026-06-23 21:14:04 -04:00
|
|
|
// Loop-back targets this node can send work back to (e.g. review -> ["plan"]).
|
|
|
|
|
// Drives the small ↺ affordance on the node instead of an always-on back-arc.
|
|
|
|
|
loopBackTo: string[];
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
}
|
|
|
|
|
|
2026-06-23 21:14:04 -04:00
|
|
|
const FORWARD = new Set(["spine", "branch"]);
|
|
|
|
|
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
// Compute laid-out React Flow nodes + edges from the topology and live state.
|
|
|
|
|
// `onPath` marks the nodes a selected task has visited (highlighted); `dimmed`
|
|
|
|
|
// fades nodes outside the active task's path when one is selected.
|
2026-06-23 21:14:04 -04:00
|
|
|
//
|
|
|
|
|
// Loop-back edges (review->plan, verify->build) are NOT drawn by default — they
|
|
|
|
|
// overlapped the forward edge and read as ugly circles. Instead the source node
|
|
|
|
|
// shows a ↺ chip (loopBackTo), and an individual loop-back arc is drawn only when
|
|
|
|
|
// the selected task actually traversed it (its id is in `loopedEdges`).
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
export function layout(
|
|
|
|
|
topo: Topology,
|
|
|
|
|
liveNodes: Record<string, { state: RFData["state"]; count: number }>,
|
|
|
|
|
pathNodeIds: Set<string> | null,
|
2026-06-23 21:14:04 -04:00
|
|
|
loopedEdges: Set<string> | null = null,
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
): { nodes: Node<RFData>[]; edges: Edge[] } {
|
|
|
|
|
const g = new dagre.graphlib.Graph();
|
|
|
|
|
g.setGraph({ rankdir: "LR", nodesep: 36, ranksep: 72, marginx: 24, marginy: 24 });
|
|
|
|
|
g.setDefaultEdgeLabel(() => ({}));
|
|
|
|
|
|
2026-06-23 21:14:04 -04:00
|
|
|
// Lay out using ONLY forward edges, so the backward loop-backs don't distort
|
|
|
|
|
// the left-to-right ranking.
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
for (const n of topo.nodes) g.setNode(n.id, { width: NODE_W, height: NODE_H });
|
2026-06-23 21:14:04 -04:00
|
|
|
for (const e of topo.edges) {
|
|
|
|
|
if (FORWARD.has(e.kind)) g.setEdge(e.from, e.to);
|
|
|
|
|
}
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
dagre.layout(g);
|
|
|
|
|
|
2026-06-23 21:14:04 -04:00
|
|
|
// node id -> loop-back targets, from the topology's loopback edges.
|
|
|
|
|
const loopBackTo: Record<string, string[]> = {};
|
|
|
|
|
for (const e of topo.edges) {
|
|
|
|
|
if (e.kind === "loopback") (loopBackTo[e.from] ??= []).push(e.to);
|
|
|
|
|
}
|
|
|
|
|
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
const nodes: Node<RFData>[] = topo.nodes.map((meta) => {
|
|
|
|
|
const pos = g.node(meta.id);
|
|
|
|
|
const live = liveNodes[meta.id];
|
|
|
|
|
const onPath = pathNodeIds ? pathNodeIds.has(meta.id) : false;
|
|
|
|
|
return {
|
|
|
|
|
id: meta.id,
|
|
|
|
|
type: "pipeline",
|
|
|
|
|
position: { x: pos.x - NODE_W / 2, y: pos.y - NODE_H / 2 },
|
|
|
|
|
data: {
|
|
|
|
|
meta,
|
|
|
|
|
state: live?.state ?? "idle",
|
|
|
|
|
count: live?.count ?? 0,
|
|
|
|
|
dimmed: pathNodeIds != null && !onPath,
|
|
|
|
|
onPath,
|
2026-06-23 21:14:04 -04:00
|
|
|
loopBackTo: loopBackTo[meta.id] ?? [],
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-23 21:14:04 -04:00
|
|
|
const edges: Edge[] = [];
|
|
|
|
|
for (const e of topo.edges) {
|
|
|
|
|
const id = `${e.from}->${e.to}`;
|
|
|
|
|
if (e.kind === "loopback") {
|
|
|
|
|
// Only draw a loop-back arc the selected task actually traversed.
|
|
|
|
|
if (!loopedEdges || !loopedEdges.has(id)) continue;
|
|
|
|
|
edges.push({
|
|
|
|
|
id,
|
|
|
|
|
source: e.from,
|
|
|
|
|
target: e.to,
|
|
|
|
|
animated: true,
|
|
|
|
|
style: { stroke: "#7a5cff", strokeWidth: 2, strokeDasharray: "5 4" },
|
|
|
|
|
label: e.to === "plan" ? "↺ revised" : "↺ retried",
|
|
|
|
|
labelStyle: { fill: "#9b8bff", fontWeight: 700 },
|
|
|
|
|
type: "smoothstep",
|
|
|
|
|
zIndex: 5,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
edges.push({
|
|
|
|
|
id,
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
source: e.from,
|
|
|
|
|
target: e.to,
|
|
|
|
|
animated: false,
|
|
|
|
|
style: {
|
2026-06-23 21:14:04 -04:00
|
|
|
stroke: e.kind === "branch" ? "#3f7fd8" : "#39414f",
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
strokeWidth: 1.5,
|
|
|
|
|
},
|
|
|
|
|
type: "smoothstep",
|
2026-06-23 21:14:04 -04:00
|
|
|
});
|
|
|
|
|
}
|
feat(agent-team): React/Vite status-dashboard SPA
New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no
CDN). Dark, three-pane layout: summary top bar, filterable task list, center
auto-laid pipeline map, and a right drawer showing a selected task's history
through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan).
Map nodes color by live state, loop-backs render dashed, clicking a node filters
the list, selecting a task highlights its path. Polls /api/state every 4s;
topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
2026-06-23 17:15:23 -04:00
|
|
|
|
|
|
|
|
return { nodes, edges };
|
|
|
|
|
}
|