@lumea-labs/orchestrator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +21 -0
  2. package/dist/index.d.ts +21 -0
  3. package/dist/index.js +93 -0
  4. package/dist/lib/format.d.ts +3 -0
  5. package/dist/lib/format.js +9 -0
  6. package/dist/orchestrator-document.d.ts +37 -0
  7. package/dist/orchestrator-document.js +122 -0
  8. package/dist/plan-detail.d.ts +102 -0
  9. package/dist/plan-detail.js +385 -0
  10. package/dist/plan-graph.d.ts +39 -0
  11. package/dist/plan-graph.js +597 -0
  12. package/dist/plan-node-detail.d.ts +29 -0
  13. package/dist/plan-node-detail.js +346 -0
  14. package/dist/plan-task-detail.d.ts +76 -0
  15. package/dist/plan-task-detail.js +450 -0
  16. package/dist/plan-types.d.ts +85 -0
  17. package/dist/plan-types.js +51 -0
  18. package/dist/run-kanban-filter-menu.d.ts +24 -0
  19. package/dist/run-kanban-filter-menu.js +152 -0
  20. package/dist/run-kanban.d.ts +61 -0
  21. package/dist/run-kanban.js +234 -0
  22. package/dist/swarm-agent-badge.d.ts +15 -0
  23. package/dist/swarm-agent-badge.js +39 -0
  24. package/dist/swarm-run-activity.d.ts +39 -0
  25. package/dist/swarm-run-activity.js +289 -0
  26. package/dist/swarm-run-card.d.ts +22 -0
  27. package/dist/swarm-run-card.js +91 -0
  28. package/dist/swarm-run-detail.d.ts +45 -0
  29. package/dist/swarm-run-detail.js +559 -0
  30. package/dist/swarm-run-list.d.ts +22 -0
  31. package/dist/swarm-run-list.js +75 -0
  32. package/dist/swarm-run-row.d.ts +22 -0
  33. package/dist/swarm-run-row.js +125 -0
  34. package/dist/swarm-skeletons.d.ts +28 -0
  35. package/dist/swarm-skeletons.js +78 -0
  36. package/dist/swarm-status-bar.d.ts +15 -0
  37. package/dist/swarm-status-bar.js +79 -0
  38. package/dist/swarm-status-pill.d.ts +12 -0
  39. package/dist/swarm-status-pill.js +86 -0
  40. package/dist/swarm-timeline.d.ts +21 -0
  41. package/dist/swarm-timeline.js +414 -0
  42. package/dist/task-workspace-sidebar.d.ts +71 -0
  43. package/dist/task-workspace-sidebar.js +352 -0
  44. package/dist/types.d.ts +285 -0
  45. package/dist/types.js +44 -0
  46. package/package.json +41 -0
@@ -0,0 +1,597 @@
1
+ "use client";
2
+ import "@xyflow/react/dist/style.css";
3
+ import { useEffect, useMemo } from "react";
4
+ import {
5
+ ReactFlow,
6
+ Background,
7
+ BackgroundVariant,
8
+ Controls,
9
+ Handle,
10
+ Position,
11
+ ReactFlowProvider,
12
+ useReactFlow
13
+ } from "@xyflow/react";
14
+ import {
15
+ CheckCircle2,
16
+ Circle,
17
+ Clock,
18
+ Flag,
19
+ FlagTriangleRight,
20
+ Hourglass,
21
+ Pause,
22
+ RotateCcw,
23
+ Scale,
24
+ ShieldCheck,
25
+ XCircle
26
+ } from "lucide-react";
27
+ import {
28
+ defaultPlanGraphLabels
29
+ } from "./plan-types";
30
+ const STATUS_COLOR = {
31
+ draft: "#999999",
32
+ pending: "#999999",
33
+ running: "#E2733D",
34
+ review: "#2B44FF",
35
+ done: "#1A7F52",
36
+ failed: "#E63946"
37
+ };
38
+ const STATUS_ICON = {
39
+ draft: Circle,
40
+ pending: Pause,
41
+ running: RotateCcw,
42
+ review: Scale,
43
+ done: CheckCircle2,
44
+ failed: XCircle
45
+ };
46
+ function parseDuration(iso) {
47
+ if (!iso) return "\u2014";
48
+ const m = iso.match(/^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/);
49
+ if (!m) return iso;
50
+ const [, d, h, mi, s] = m;
51
+ const out = [];
52
+ if (d) out.push(`${d}d`);
53
+ if (h) out.push(`${h}h`);
54
+ if (mi) out.push(`${mi}m`);
55
+ if (s) out.push(`${s}s`);
56
+ return out.join(" ") || iso;
57
+ }
58
+ function joinClasses(...parts) {
59
+ return parts.filter(Boolean).join(" ");
60
+ }
61
+ function HandlesTB() {
62
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
63
+ Handle,
64
+ {
65
+ type: "target",
66
+ position: Position.Top,
67
+ className: "!h-2 !w-2 !border-p-line !bg-p-surface"
68
+ }
69
+ ), /* @__PURE__ */ React.createElement(
70
+ Handle,
71
+ {
72
+ type: "source",
73
+ position: Position.Bottom,
74
+ className: "!h-2 !w-2 !border-p-line !bg-p-surface"
75
+ }
76
+ ));
77
+ }
78
+ function TaskNode({ data, selected }) {
79
+ const { plan, runtime, agent, labels, onOpen } = data;
80
+ const status = runtime?.status ?? "draft";
81
+ const color = STATUS_COLOR[status];
82
+ const Icon = STATUS_ICON[status];
83
+ const assigneeName = agent?.displayName ?? plan.assignTo ?? runtime?.agentName ?? "\u2014";
84
+ const canOpen = !!onOpen;
85
+ return /* @__PURE__ */ React.createElement(
86
+ "div",
87
+ {
88
+ onClick: () => {
89
+ if (!canOpen) return;
90
+ onOpen({ title: plan.title });
91
+ },
92
+ role: canOpen ? "button" : void 0,
93
+ tabIndex: canOpen ? 0 : -1,
94
+ className: joinClasses(
95
+ "group relative w-[240px] rounded-[4px] border bg-p-surface transition-all duration-150",
96
+ canOpen ? "cursor-pointer hover:border-p-ink-3" : "cursor-default",
97
+ selected ? "border-p-ink z-10" : "border-p-line"
98
+ ),
99
+ style: {
100
+ borderLeftWidth: selected ? 4 : 3,
101
+ borderLeftColor: color,
102
+ // Selected: thick double halo in the status hue, gentle scale-up
103
+ // so the node sits forward of its neighbours, status-tinted
104
+ // surface so the fill carries the meaning too.
105
+ boxShadow: selected ? `0 0 0 3px ${color}, 0 0 0 8px ${color}40, 0 22px 48px -18px rgba(15,15,15,0.38)` : "none",
106
+ transform: selected ? "scale(1.02)" : "scale(1)",
107
+ backgroundColor: selected ? `color-mix(in srgb, ${color} 5%, var(--surface))` : "var(--surface)"
108
+ }
109
+ },
110
+ /* @__PURE__ */ React.createElement(HandlesTB, null),
111
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -top-2 left-3 bg-p-surface px-1 font-mono text-[9px] font-bold uppercase tracking-[0.16em] text-p-ink-3" }, labels.task),
112
+ /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-1.5 px-4 py-3" }, /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React.createElement(Icon, { className: "size-3 shrink-0", style: { color } }), /* @__PURE__ */ React.createElement(
113
+ "span",
114
+ {
115
+ className: "font-mono text-[9px] font-bold uppercase tracking-[0.18em]",
116
+ style: { color }
117
+ },
118
+ labels.status[status]
119
+ ), runtime?.phase && runtime.phase !== "execution" && /* @__PURE__ */ React.createElement("span", { className: "ml-auto font-mono text-[9px] uppercase tracking-[0.14em] text-p-ink-3" }, "\xB7 ", runtime.phase)), /* @__PURE__ */ React.createElement(
120
+ "div",
121
+ {
122
+ className: "font-display text-[14.5px] font-bold leading-[1.15] tracking-[-0.01em] text-p-ink",
123
+ style: {
124
+ display: "-webkit-box",
125
+ WebkitLineClamp: 2,
126
+ WebkitBoxOrient: "vertical",
127
+ overflow: "hidden"
128
+ }
129
+ },
130
+ plan.title
131
+ ), /* @__PURE__ */ React.createElement("div", { className: "mt-0.5 flex items-center justify-between gap-2 font-mono text-[10px] uppercase tracking-[0.14em] text-p-ink-3" }, /* @__PURE__ */ React.createElement("span", { className: "truncate" }, assigneeName), /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5 shrink-0" }, runtime?.sideEffects && /* @__PURE__ */ React.createElement(
132
+ "span",
133
+ {
134
+ title: "side effects",
135
+ className: "inline-flex size-1.5 rounded-full bg-[#E63946]"
136
+ }
137
+ ), typeof runtime?.priority === "number" && runtime.priority !== 1 && /* @__PURE__ */ React.createElement("span", { className: "text-p-ink-2" }, runtime.priority, "\xD7"))))
138
+ );
139
+ }
140
+ function CheckpointNode({
141
+ data,
142
+ selected
143
+ }) {
144
+ const { cp, labels, onOpen } = data;
145
+ const canOpen = !!onOpen;
146
+ return /* @__PURE__ */ React.createElement(
147
+ "div",
148
+ {
149
+ onClick: canOpen ? () => onOpen(cp.name) : void 0,
150
+ role: canOpen ? "button" : void 0,
151
+ tabIndex: canOpen ? 0 : -1,
152
+ className: joinClasses(
153
+ "group relative w-[200px] rounded-[4px] border bg-[#FDF8E8] px-4 py-3 transition-all duration-150",
154
+ canOpen ? "cursor-pointer" : "",
155
+ selected ? "border-[#D4A017] z-10" : "border-[#D4A017]/40"
156
+ ),
157
+ style: {
158
+ boxShadow: selected ? "0 0 0 3px #D4A017, 0 0 0 8px #D4A01740, 0 22px 48px -18px rgba(15,15,15,0.38)" : "none",
159
+ transform: selected ? "scale(1.02)" : "scale(1)"
160
+ }
161
+ },
162
+ /* @__PURE__ */ React.createElement(HandlesTB, null),
163
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -top-2 left-3 bg-[#FDF8E8] px-1 font-mono text-[9px] font-bold uppercase tracking-[0.16em] text-[#8A6B0B]" }, labels.checkpoint),
164
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React.createElement(Hourglass, { className: "size-3 shrink-0 text-[#8A6B0B]" }), /* @__PURE__ */ React.createElement("span", { className: "truncate font-display text-[13.5px] font-bold leading-tight text-p-ink" }, cp.name)),
165
+ cp.message && /* @__PURE__ */ React.createElement(
166
+ "p",
167
+ {
168
+ className: "mt-1.5 text-[11.5px] leading-snug text-[#8A6B0B]",
169
+ style: {
170
+ display: "-webkit-box",
171
+ WebkitLineClamp: 2,
172
+ WebkitBoxOrient: "vertical",
173
+ overflow: "hidden"
174
+ }
175
+ },
176
+ cp.message
177
+ )
178
+ );
179
+ }
180
+ function GateNode({ data, selected }) {
181
+ const { gate, labels, onOpen } = data;
182
+ const canOpen = !!onOpen;
183
+ return /* @__PURE__ */ React.createElement(
184
+ "div",
185
+ {
186
+ onClick: canOpen ? () => onOpen(gate.name) : void 0,
187
+ role: canOpen ? "button" : void 0,
188
+ tabIndex: canOpen ? 0 : -1,
189
+ className: joinClasses(
190
+ "group relative w-[220px] rounded-[4px] border bg-p-accent-light px-4 py-3 transition-all duration-150",
191
+ canOpen ? "cursor-pointer" : "",
192
+ selected ? "border-p-accent z-10" : "border-p-accent/40"
193
+ ),
194
+ style: {
195
+ boxShadow: selected ? "0 0 0 3px var(--p-accent), 0 0 0 8px color-mix(in srgb, var(--p-accent) 28%, transparent), 0 22px 48px -18px rgba(15,15,15,0.38)" : "none",
196
+ transform: selected ? "scale(1.02)" : "scale(1)"
197
+ }
198
+ },
199
+ /* @__PURE__ */ React.createElement(HandlesTB, null),
200
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -top-2 left-3 bg-p-accent-light px-1 font-mono text-[9px] font-bold uppercase tracking-[0.16em] text-p-accent" }, labels.qualityGate),
201
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React.createElement(ShieldCheck, { className: "size-3 shrink-0 text-p-accent" }), /* @__PURE__ */ React.createElement("span", { className: "truncate font-display text-[13.5px] font-bold leading-tight text-p-ink" }, gate.name)),
202
+ /* @__PURE__ */ React.createElement("div", { className: "mt-1.5 flex flex-wrap gap-1 font-mono text-[10px] uppercase tracking-[0.14em] text-p-accent" }, typeof gate.minScore === "number" && /* @__PURE__ */ React.createElement("span", null, labels.qualityMinScore, " ", gate.minScore), gate.requireAllPassed && /* @__PURE__ */ React.createElement("span", null, "\xB7 ", labels.qualityRequireAll))
203
+ );
204
+ }
205
+ function DelayNode({ data, selected }) {
206
+ const { delay, labels, onOpen } = data;
207
+ const canOpen = !!onOpen;
208
+ return /* @__PURE__ */ React.createElement(
209
+ "div",
210
+ {
211
+ onClick: canOpen ? () => onOpen(delay.name) : void 0,
212
+ role: canOpen ? "button" : void 0,
213
+ tabIndex: canOpen ? 0 : -1,
214
+ className: joinClasses(
215
+ "group relative w-[180px] rounded-[4px] border bg-[#F3EEFF] px-4 py-3 transition-all duration-150",
216
+ canOpen ? "cursor-pointer" : "",
217
+ selected ? "border-[#7B3FE4] z-10" : "border-[#7B3FE4]/40"
218
+ ),
219
+ style: {
220
+ boxShadow: selected ? "0 0 0 3px #7B3FE4, 0 0 0 8px #7B3FE440, 0 22px 48px -18px rgba(15,15,15,0.38)" : "none",
221
+ transform: selected ? "scale(1.02)" : "scale(1)"
222
+ }
223
+ },
224
+ /* @__PURE__ */ React.createElement(HandlesTB, null),
225
+ /* @__PURE__ */ React.createElement("div", { className: "absolute -top-2 left-3 bg-[#F3EEFF] px-1 font-mono text-[9px] font-bold uppercase tracking-[0.16em] text-[#7B3FE4]" }, labels.delay),
226
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-1.5" }, /* @__PURE__ */ React.createElement(Clock, { className: "size-3 shrink-0 text-[#7B3FE4]" }), /* @__PURE__ */ React.createElement("span", { className: "truncate font-display text-[13.5px] font-bold leading-tight text-p-ink" }, delay.name)),
227
+ /* @__PURE__ */ React.createElement("div", { className: "mt-1.5 font-mono text-[11px] font-bold uppercase tracking-[0.14em] text-[#7B3FE4]" }, parseDuration(delay.duration))
228
+ );
229
+ }
230
+ function SentinelNode({ data }) {
231
+ const { role, label } = data;
232
+ const isStart = role === "start";
233
+ const Icon = isStart ? FlagTriangleRight : Flag;
234
+ return /* @__PURE__ */ React.createElement("div", { className: "relative inline-flex items-center gap-1.5 rounded-full border-2 border-p-ink bg-p-surface px-3 py-1.5" }, isStart ? /* @__PURE__ */ React.createElement(
235
+ Handle,
236
+ {
237
+ type: "source",
238
+ position: Position.Bottom,
239
+ className: "!h-2 !w-2 !border-p-ink !bg-p-ink"
240
+ }
241
+ ) : /* @__PURE__ */ React.createElement(
242
+ Handle,
243
+ {
244
+ type: "target",
245
+ position: Position.Top,
246
+ className: "!h-2 !w-2 !border-p-ink !bg-p-ink"
247
+ }
248
+ ), /* @__PURE__ */ React.createElement(Icon, { className: "size-3 text-p-ink", "aria-hidden": true }), /* @__PURE__ */ React.createElement("span", { className: "font-mono text-[10px] font-bold uppercase tracking-[0.22em] text-p-ink" }, label));
249
+ }
250
+ const nodeTypes = {
251
+ taskNode: TaskNode,
252
+ checkpointNode: CheckpointNode,
253
+ gateNode: GateNode,
254
+ delayNode: DelayNode,
255
+ sentinelNode: SentinelNode
256
+ };
257
+ function layout(nodes, edges, opts) {
258
+ const preds = /* @__PURE__ */ new Map();
259
+ const succs = /* @__PURE__ */ new Map();
260
+ for (const n of nodes) {
261
+ preds.set(n.id, []);
262
+ succs.set(n.id, []);
263
+ }
264
+ for (const e of edges) {
265
+ if (preds.has(e.target)) preds.get(e.target).push(e.source);
266
+ if (succs.has(e.source)) succs.get(e.source).push(e.target);
267
+ }
268
+ const indeg = /* @__PURE__ */ new Map();
269
+ for (const n of nodes) indeg.set(n.id, preds.get(n.id).length);
270
+ const queue = [];
271
+ for (const n of nodes) if (indeg.get(n.id) === 0) queue.push(n.id);
272
+ const order = [];
273
+ const indegCopy = new Map(indeg);
274
+ while (queue.length) {
275
+ const id = queue.shift();
276
+ order.push(id);
277
+ for (const t of succs.get(id) || []) {
278
+ const v = (indegCopy.get(t) ?? 0) - 1;
279
+ indegCopy.set(t, v);
280
+ if (v === 0) queue.push(t);
281
+ }
282
+ }
283
+ for (const n of nodes) if (!order.includes(n.id)) order.push(n.id);
284
+ const layer = /* @__PURE__ */ new Map();
285
+ for (const id of order) {
286
+ const ps = preds.get(id) || [];
287
+ const l = ps.length === 0 ? 0 : Math.max(...ps.map((p) => (layer.get(p) ?? 0) + 1));
288
+ layer.set(id, l);
289
+ }
290
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
291
+ const SPECIAL_TYPES = /* @__PURE__ */ new Set(["checkpointNode", "gateNode", "delayNode"]);
292
+ const isSpecial = (id) => SPECIAL_TYPES.has(nodeById.get(id)?.type ?? "");
293
+ const bump = (id, target) => {
294
+ const current = layer.get(id) ?? 0;
295
+ if (current >= target) return;
296
+ layer.set(id, target);
297
+ for (const s of succs.get(id) || []) bump(s, target + 1);
298
+ };
299
+ const specialsAtLayer = /* @__PURE__ */ new Set();
300
+ for (const id of order) {
301
+ if (!isSpecial(id)) continue;
302
+ let l = layer.get(id) ?? 0;
303
+ while (specialsAtLayer.has(l)) l++;
304
+ if (l !== (layer.get(id) ?? 0)) bump(id, l);
305
+ specialsAtLayer.add(l);
306
+ }
307
+ const byLayer = /* @__PURE__ */ new Map();
308
+ for (const n of nodes) {
309
+ const l = layer.get(n.id) ?? 0;
310
+ if (!byLayer.has(l)) byLayer.set(l, []);
311
+ byLayer.get(l).push(n);
312
+ }
313
+ const rowHeight = /* @__PURE__ */ new Map();
314
+ for (const [l, list] of byLayer) {
315
+ rowHeight.set(l, Math.max(...list.map((n) => n.height)));
316
+ }
317
+ const sortedLayers = [...byLayer.keys()].sort((a, b) => a - b);
318
+ const yOf = /* @__PURE__ */ new Map();
319
+ let y = 0;
320
+ for (const l of sortedLayers) {
321
+ yOf.set(l, y);
322
+ y += (rowHeight.get(l) ?? 0) + opts.colGap;
323
+ }
324
+ const out = [];
325
+ for (const l of sortedLayers) {
326
+ const list = byLayer.get(l);
327
+ const totalW = list.reduce((acc, n) => acc + n.width, 0) + Math.max(0, list.length - 1) * opts.rowGap;
328
+ let x = -totalW / 2;
329
+ for (const n of list) {
330
+ const rh = rowHeight.get(l) ?? n.height;
331
+ const ny = (yOf.get(l) ?? 0) + (rh - n.height) / 2;
332
+ out.push({
333
+ id: n.id,
334
+ type: n.type,
335
+ position: { x, y: ny },
336
+ data: n.data,
337
+ width: n.width,
338
+ height: n.height
339
+ });
340
+ x += n.width + opts.rowGap;
341
+ }
342
+ }
343
+ const outEdges = edges.map((e, i) => ({
344
+ id: `e-${i}-${e.source}-${e.target}`,
345
+ source: e.source,
346
+ target: e.target,
347
+ type: "default",
348
+ animated: e.animated ?? false,
349
+ style: {
350
+ stroke: e.stroke ?? "var(--line)",
351
+ strokeWidth: 1.25,
352
+ strokeDasharray: e.dashed ? "4 4" : void 0
353
+ }
354
+ }));
355
+ return { nodes: out, edges: outEdges };
356
+ }
357
+ function useZoomToNode(targetId, enabled) {
358
+ const rf = useReactFlow();
359
+ useEffect(() => {
360
+ if (!enabled || !targetId) return;
361
+ const node = rf.getNode(targetId);
362
+ if (!node) return;
363
+ const w = node.width ?? 240;
364
+ const h = node.height ?? 110;
365
+ const cx = node.position.x + w / 2;
366
+ const cy = node.position.y + h / 2;
367
+ rf.setCenter(cx, cy, { zoom: 1.25, duration: 400 });
368
+ }, [targetId, enabled, rf]);
369
+ }
370
+ function PlanGraph(props) {
371
+ return /* @__PURE__ */ React.createElement(ReactFlowProvider, null, /* @__PURE__ */ React.createElement(PlanGraphInner, { ...props }));
372
+ }
373
+ function PlanGraphInner({
374
+ tasks,
375
+ runtimeByTitle,
376
+ checkpoints,
377
+ qualityGates,
378
+ delays,
379
+ agents,
380
+ onOpenTask,
381
+ onOpenCheckpoint,
382
+ onOpenGate,
383
+ onOpenDelay,
384
+ selectedNodeId,
385
+ zoomToSelected = false,
386
+ labels,
387
+ className
388
+ }) {
389
+ const L = useMemo(
390
+ () => ({ ...defaultPlanGraphLabels, ...labels }),
391
+ [labels]
392
+ );
393
+ const cps = checkpoints ?? [];
394
+ const qgs = qualityGates ?? [];
395
+ const dls = delays ?? [];
396
+ const { flowNodes, flowEdges } = useMemo(() => {
397
+ const titleSet = /* @__PURE__ */ new Set();
398
+ for (const tk of tasks) titleSet.add(tk.title);
399
+ const resolveTaskId = (ref) => titleSet.has(ref) ? `task:${ref}` : null;
400
+ const internalNodes = [];
401
+ const internalEdges = [];
402
+ for (const tk of tasks) {
403
+ internalNodes.push({
404
+ id: `task:${tk.title}`,
405
+ type: "taskNode",
406
+ width: 240,
407
+ height: 110,
408
+ data: {
409
+ kind: "task",
410
+ plan: tk,
411
+ runtime: runtimeByTitle?.get(tk.title),
412
+ agent: agents?.find((a) => a.name === tk.assignTo),
413
+ labels: L,
414
+ onOpen: onOpenTask
415
+ }
416
+ });
417
+ }
418
+ const gatedPairs = /* @__PURE__ */ new Set();
419
+ const pairKey = (from, to) => `${from}\u2192${to}`;
420
+ const registerGate = (afterTasks, blocksTasks) => {
421
+ for (const a of afterTasks) for (const b of blocksTasks)
422
+ gatedPairs.add(pairKey(a, b));
423
+ };
424
+ for (const cp of cps) registerGate(cp.afterTasks, cp.blocksTasks);
425
+ for (const g of qgs) registerGate(g.afterTasks, g.blocksTasks);
426
+ for (const d of dls) registerGate(d.afterTasks, d.blocksTasks);
427
+ for (const tk of tasks) {
428
+ for (const dep of tk.dependsOn || []) {
429
+ if (gatedPairs.has(pairKey(dep, tk.title))) continue;
430
+ const src = resolveTaskId(dep);
431
+ if (!src) continue;
432
+ const status = runtimeByTitle?.get(tk.title)?.status;
433
+ internalEdges.push({
434
+ source: src,
435
+ target: `task:${tk.title}`,
436
+ stroke: status === "running" ? "var(--p-accent)" : status === "failed" ? "#E63946" : status === "done" ? "var(--green)" : "var(--line)",
437
+ animated: status === "running"
438
+ });
439
+ }
440
+ }
441
+ for (const cp of cps) {
442
+ const id = `cp:${cp.name}`;
443
+ internalNodes.push({
444
+ id,
445
+ type: "checkpointNode",
446
+ width: 200,
447
+ height: 94,
448
+ data: { kind: "checkpoint", cp, labels: L, onOpen: onOpenCheckpoint }
449
+ });
450
+ for (const a of cp.afterTasks) {
451
+ const src = resolveTaskId(a);
452
+ if (src) internalEdges.push({ source: src, target: id });
453
+ }
454
+ for (const b of cp.blocksTasks) {
455
+ const tgt = resolveTaskId(b);
456
+ if (tgt) internalEdges.push({ source: id, target: tgt, dashed: true });
457
+ }
458
+ }
459
+ for (const g of qgs) {
460
+ const id = `qg:${g.name}`;
461
+ internalNodes.push({
462
+ id,
463
+ type: "gateNode",
464
+ width: 220,
465
+ height: 92,
466
+ data: { kind: "gate", gate: g, labels: L, onOpen: onOpenGate }
467
+ });
468
+ for (const a of g.afterTasks) {
469
+ const src = resolveTaskId(a);
470
+ if (src) internalEdges.push({ source: src, target: id });
471
+ }
472
+ for (const b of g.blocksTasks) {
473
+ const tgt = resolveTaskId(b);
474
+ if (tgt) internalEdges.push({ source: id, target: tgt, dashed: true });
475
+ }
476
+ }
477
+ for (const d of dls) {
478
+ const id = `dl:${d.name}`;
479
+ internalNodes.push({
480
+ id,
481
+ type: "delayNode",
482
+ width: 180,
483
+ height: 86,
484
+ data: { kind: "delay", delay: d, labels: L, onOpen: onOpenDelay }
485
+ });
486
+ for (const a of d.afterTasks) {
487
+ const src = resolveTaskId(a);
488
+ if (src) internalEdges.push({ source: src, target: id });
489
+ }
490
+ for (const b of d.blocksTasks) {
491
+ const tgt = resolveTaskId(b);
492
+ if (tgt) internalEdges.push({ source: id, target: tgt, dashed: true });
493
+ }
494
+ }
495
+ const nodeIds = new Set(internalNodes.map((n) => n.id));
496
+ const hasIncoming = /* @__PURE__ */ new Set();
497
+ const hasOutgoing = /* @__PURE__ */ new Set();
498
+ for (const e of internalEdges) {
499
+ hasIncoming.add(e.target);
500
+ hasOutgoing.add(e.source);
501
+ }
502
+ const roots = [...nodeIds].filter((id) => !hasIncoming.has(id));
503
+ const leaves = [...nodeIds].filter((id) => !hasOutgoing.has(id));
504
+ if (internalNodes.length > 0) {
505
+ internalNodes.push({
506
+ id: "sentinel:start",
507
+ type: "sentinelNode",
508
+ width: 84,
509
+ height: 30,
510
+ data: { kind: "sentinel", role: "start", label: L.start }
511
+ });
512
+ internalNodes.push({
513
+ id: "sentinel:end",
514
+ type: "sentinelNode",
515
+ width: 84,
516
+ height: 30,
517
+ data: { kind: "sentinel", role: "end", label: L.end }
518
+ });
519
+ for (const r of roots)
520
+ internalEdges.push({ source: "sentinel:start", target: r });
521
+ for (const l of leaves)
522
+ internalEdges.push({ source: l, target: "sentinel:end" });
523
+ }
524
+ const { nodes, edges } = layout(internalNodes, internalEdges, {
525
+ colGap: 80,
526
+ rowGap: 20
527
+ });
528
+ return { flowNodes: nodes, flowEdges: edges };
529
+ }, [
530
+ tasks,
531
+ cps,
532
+ qgs,
533
+ dls,
534
+ runtimeByTitle,
535
+ agents,
536
+ onOpenTask,
537
+ onOpenCheckpoint,
538
+ onOpenGate,
539
+ onOpenDelay,
540
+ L
541
+ ]);
542
+ const renderedNodes = useMemo(() => {
543
+ if (selectedNodeId === void 0) return flowNodes;
544
+ return flowNodes.map(
545
+ (n) => n.id === selectedNodeId ? { ...n, selected: true } : n.selected ? { ...n, selected: false } : n
546
+ );
547
+ }, [flowNodes, selectedNodeId]);
548
+ useZoomToNode(selectedNodeId ?? null, zoomToSelected);
549
+ if (flowNodes.length === 0) {
550
+ return /* @__PURE__ */ React.createElement(
551
+ "div",
552
+ {
553
+ className: joinClasses(
554
+ "flex flex-col items-center justify-center gap-4 border border-p-line bg-p-bg px-8 py-20 text-center",
555
+ className
556
+ )
557
+ },
558
+ /* @__PURE__ */ React.createElement("span", { className: "inline-flex size-12 items-center justify-center rounded-full border border-p-line bg-p-surface text-p-ink-3" }, /* @__PURE__ */ React.createElement(Flag, { className: "size-5" })),
559
+ /* @__PURE__ */ React.createElement("h3", { className: "max-w-[36ch] font-display text-[22px] italic leading-[1.15] tracking-tight text-p-ink" }, L.emptyTitle),
560
+ /* @__PURE__ */ React.createElement("p", { className: "max-w-[48ch] text-[13.5px] leading-relaxed text-p-ink-2" }, L.emptyBody)
561
+ );
562
+ }
563
+ return /* @__PURE__ */ React.createElement("div", { className: joinClasses("plan-graph relative bg-p-bg", className) }, /* @__PURE__ */ React.createElement(
564
+ ReactFlow,
565
+ {
566
+ nodes: renderedNodes,
567
+ edges: flowEdges,
568
+ nodeTypes,
569
+ fitView: true,
570
+ fitViewOptions: { padding: 0.2, maxZoom: 1 },
571
+ proOptions: { hideAttribution: true },
572
+ nodesDraggable: false,
573
+ nodesConnectable: false,
574
+ elementsSelectable: true,
575
+ style: { width: "100%", height: "100%" }
576
+ },
577
+ /* @__PURE__ */ React.createElement(
578
+ Background,
579
+ {
580
+ variant: BackgroundVariant.Dots,
581
+ gap: 28,
582
+ size: 1,
583
+ color: "var(--line)"
584
+ }
585
+ ),
586
+ /* @__PURE__ */ React.createElement(
587
+ Controls,
588
+ {
589
+ showInteractive: false,
590
+ className: "!bg-p-surface !border !border-p-line !rounded-[4px] !shadow-none [&_button]:!bg-p-surface [&_button]:!border-p-line [&_button]:!text-p-ink-2 [&_button:hover]:!bg-p-warm [&_button:hover]:!text-p-ink"
591
+ }
592
+ )
593
+ ));
594
+ }
595
+ export {
596
+ PlanGraph
597
+ };
@@ -0,0 +1,29 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { PlanTaskRuntime, Plan, PlanGraphLabels, PlanCheckpoint, PlanDelay, PlanQualityGate } from './plan-types.js';
4
+
5
+ interface CommonProps {
6
+ /** When provided, used to overlay runtime status on the after/blocks
7
+ * rows, so consumers see live progress alongside structure. */
8
+ runtimeByTitle?: Map<string, PlanTaskRuntime>;
9
+ plan?: Plan;
10
+ headerTrailing?: ReactNode;
11
+ actionsSlot?: ReactNode;
12
+ onOpenRelated?: (title: string) => void;
13
+ labels?: Partial<PlanGraphLabels>;
14
+ className?: string;
15
+ }
16
+ interface PlanCheckpointDetailProps extends CommonProps {
17
+ checkpoint: PlanCheckpoint;
18
+ }
19
+ interface PlanQualityGateDetailProps extends CommonProps {
20
+ gate: PlanQualityGate;
21
+ }
22
+ interface PlanDelayDetailProps extends CommonProps {
23
+ delay: PlanDelay;
24
+ }
25
+ declare function PlanCheckpointDetail({ checkpoint, runtimeByTitle, plan, headerTrailing, actionsSlot, onOpenRelated, labels, className, }: PlanCheckpointDetailProps): react.JSX.Element;
26
+ declare function PlanQualityGateDetail({ gate, runtimeByTitle, plan, headerTrailing, actionsSlot, onOpenRelated, labels, className, }: PlanQualityGateDetailProps): react.JSX.Element;
27
+ declare function PlanDelayDetail({ delay, runtimeByTitle, plan, headerTrailing, actionsSlot, onOpenRelated, labels, className, }: PlanDelayDetailProps): react.JSX.Element;
28
+
29
+ export { PlanCheckpointDetail, type PlanCheckpointDetailProps, PlanDelayDetail, type PlanDelayDetailProps, PlanQualityGateDetail, type PlanQualityGateDetailProps };