@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.
- package/README.md +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +93 -0
- package/dist/lib/format.d.ts +3 -0
- package/dist/lib/format.js +9 -0
- package/dist/orchestrator-document.d.ts +37 -0
- package/dist/orchestrator-document.js +122 -0
- package/dist/plan-detail.d.ts +102 -0
- package/dist/plan-detail.js +385 -0
- package/dist/plan-graph.d.ts +39 -0
- package/dist/plan-graph.js +597 -0
- package/dist/plan-node-detail.d.ts +29 -0
- package/dist/plan-node-detail.js +346 -0
- package/dist/plan-task-detail.d.ts +76 -0
- package/dist/plan-task-detail.js +450 -0
- package/dist/plan-types.d.ts +85 -0
- package/dist/plan-types.js +51 -0
- package/dist/run-kanban-filter-menu.d.ts +24 -0
- package/dist/run-kanban-filter-menu.js +152 -0
- package/dist/run-kanban.d.ts +61 -0
- package/dist/run-kanban.js +234 -0
- package/dist/swarm-agent-badge.d.ts +15 -0
- package/dist/swarm-agent-badge.js +39 -0
- package/dist/swarm-run-activity.d.ts +39 -0
- package/dist/swarm-run-activity.js +289 -0
- package/dist/swarm-run-card.d.ts +22 -0
- package/dist/swarm-run-card.js +91 -0
- package/dist/swarm-run-detail.d.ts +45 -0
- package/dist/swarm-run-detail.js +559 -0
- package/dist/swarm-run-list.d.ts +22 -0
- package/dist/swarm-run-list.js +75 -0
- package/dist/swarm-run-row.d.ts +22 -0
- package/dist/swarm-run-row.js +125 -0
- package/dist/swarm-skeletons.d.ts +28 -0
- package/dist/swarm-skeletons.js +78 -0
- package/dist/swarm-status-bar.d.ts +15 -0
- package/dist/swarm-status-bar.js +79 -0
- package/dist/swarm-status-pill.d.ts +12 -0
- package/dist/swarm-status-pill.js +86 -0
- package/dist/swarm-timeline.d.ts +21 -0
- package/dist/swarm-timeline.js +414 -0
- package/dist/task-workspace-sidebar.d.ts +71 -0
- package/dist/task-workspace-sidebar.js +352 -0
- package/dist/types.d.ts +285 -0
- package/dist/types.js +44 -0
- 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 };
|