@tangle-network/sandbox-ui 0.138.2 → 0.139.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/dist/{node-output-udnwhShN.js → AgentGraph-Beld8p6x.js} +1021 -204
- package/dist/AgentGraph-DL-QLjkw.d.ts +132 -0
- package/dist/AgentGraphCanvas-BHihTTsl.js +1203 -0
- package/dist/{WorkflowGraph-DXuWXMw9.js → WorkflowGraph-CbIFufIK.js} +4 -56
- package/dist/{chat-BYTVLZQW.js → chat-4OZD23RA.js} +1 -1
- package/dist/chat.js +1 -1
- package/dist/elk-loader-Dl9S7jQk.js +12 -0
- package/dist/globals.css +109 -5
- package/dist/{index-C5h5T0nb.d.ts → index-D6gtWRHq.d.ts} +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/node-output-DlySTwxr.js +178 -0
- package/dist/{run-CliTxkfw.js → run-CC-DESx8.js} +79 -15
- package/dist/run.d.ts +1 -1
- package/dist/run.js +1 -1
- package/dist/styles.css +109 -5
- package/dist/workflows.d.ts +3 -2
- package/dist/workflows.js +3 -82
- package/package.json +3 -3
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
import { t as MOTION_CONTROL } from "./motion-Bn7ESHrj.js";
|
|
2
|
+
import { T as statusBorder, U as fmtTokenTotal, V as fmtCost, a as useColorMode, c as NodeMark, d as RESTING_BORDER, f as RESTING_SHADOW, i as motionAllowed, o as KIND_ACCENT, p as STATUS_COLOR, r as SELECTION_OUTLINE, y as edgeColor } from "./AgentGraph-Beld8p6x.js";
|
|
3
|
+
import { t as loadElk } from "./elk-loader-Dl9S7jQk.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { CircleCheck, CirclePause, CircleQuestionMark, CircleStop, CircleX, Hand, Hourglass, LoaderCircle, Users } from "lucide-react";
|
|
7
|
+
import { Background, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Position, ReactFlow, useInternalNode } from "@xyflow/react";
|
|
8
|
+
import "@xyflow/react/dist/style.css";
|
|
9
|
+
// src/workflows/agent-graph-layout.ts
|
|
10
|
+
/** Every agent card's size. Fixed, so a status or turn update never moves a card. */
|
|
11
|
+
const AGENT_CARD = {
|
|
12
|
+
width: 200,
|
|
13
|
+
height: 78
|
|
14
|
+
};
|
|
15
|
+
/** A live card: room for the figure, the thinking line and the last three events. Fixed for the same reason. */
|
|
16
|
+
const LIVE_AGENT_CARD = {
|
|
17
|
+
width: 272,
|
|
18
|
+
height: 152
|
|
19
|
+
};
|
|
20
|
+
const GRID_GAP = 10;
|
|
21
|
+
const FRAME_PAD = 12;
|
|
22
|
+
const MAX_GRID_COLUMNS = 6;
|
|
23
|
+
const LAYER_GAP = 56;
|
|
24
|
+
const SIBLING_GAP = 24;
|
|
25
|
+
function buildAgentTopology(nodes, edges) {
|
|
26
|
+
const ids = /* @__PURE__ */ new Set();
|
|
27
|
+
const agentIds = [];
|
|
28
|
+
for (const n of nodes) {
|
|
29
|
+
if (ids.has(n.id)) continue;
|
|
30
|
+
ids.add(n.id);
|
|
31
|
+
agentIds.push(n.id);
|
|
32
|
+
}
|
|
33
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34
|
+
const spawns = [];
|
|
35
|
+
const children = /* @__PURE__ */ new Map();
|
|
36
|
+
const parentCount = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const e of edges) {
|
|
38
|
+
if (e.kind !== "spawn" || e.from === e.to) continue;
|
|
39
|
+
if (!ids.has(e.from) || !ids.has(e.to)) continue;
|
|
40
|
+
const key = `${e.from}\u0000${e.to}`;
|
|
41
|
+
if (seen.has(key)) continue;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
spawns.push({
|
|
44
|
+
from: e.from,
|
|
45
|
+
to: e.to
|
|
46
|
+
});
|
|
47
|
+
children.set(e.from, [...children.get(e.from) ?? [], e.to]);
|
|
48
|
+
parentCount.set(e.to, (parentCount.get(e.to) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
const groups = [];
|
|
51
|
+
const groupOf = /* @__PURE__ */ new Map();
|
|
52
|
+
for (const parentId of agentIds) {
|
|
53
|
+
const leaves = (children.get(parentId) ?? []).filter((c) => !children.has(c) && parentCount.get(c) === 1);
|
|
54
|
+
if (leaves.length < 4) continue;
|
|
55
|
+
const id = `agent-group:${parentId}`;
|
|
56
|
+
groups.push({
|
|
57
|
+
id,
|
|
58
|
+
parentId,
|
|
59
|
+
memberIds: leaves
|
|
60
|
+
});
|
|
61
|
+
for (const leaf of leaves) groupOf.set(leaf, id);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
key: JSON.stringify([agentIds, spawns.map((s) => [s.from, s.to])]),
|
|
65
|
+
agentIds,
|
|
66
|
+
spawns,
|
|
67
|
+
groups,
|
|
68
|
+
groupOf
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function gridSize(members, columns, card) {
|
|
72
|
+
const cols = Math.max(1, Math.min(columns, members));
|
|
73
|
+
const rows = Math.ceil(members / cols);
|
|
74
|
+
return {
|
|
75
|
+
width: cols * card.width + (cols - 1) * GRID_GAP + 24,
|
|
76
|
+
height: 26 + rows * card.height + (rows - 1) * GRID_GAP + FRAME_PAD
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The boxes the layouter places: each ungrouped agent, and each grid as one block. */
|
|
80
|
+
function layoutUnits(topology, columns, card) {
|
|
81
|
+
const units = [];
|
|
82
|
+
for (const id of topology.agentIds) if (!topology.groupOf.has(id)) units.push({
|
|
83
|
+
id,
|
|
84
|
+
width: card.width,
|
|
85
|
+
height: card.height
|
|
86
|
+
});
|
|
87
|
+
for (const g of topology.groups) units.push({
|
|
88
|
+
id: g.id,
|
|
89
|
+
...gridSize(g.memberIds.length, columns, card)
|
|
90
|
+
});
|
|
91
|
+
const unitOf = (id) => topology.groupOf.get(id) ?? id;
|
|
92
|
+
const seen = /* @__PURE__ */ new Set();
|
|
93
|
+
const links = [];
|
|
94
|
+
for (const s of topology.spawns) {
|
|
95
|
+
const from = unitOf(s.from);
|
|
96
|
+
const to = unitOf(s.to);
|
|
97
|
+
const key = `${from}\u0000${to}`;
|
|
98
|
+
if (from === to || seen.has(key)) continue;
|
|
99
|
+
seen.add(key);
|
|
100
|
+
links.push({
|
|
101
|
+
from,
|
|
102
|
+
to
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
units,
|
|
107
|
+
links
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function placeWithElk(units, links, direction) {
|
|
111
|
+
const elk = await loadElk();
|
|
112
|
+
if (!elk) return null;
|
|
113
|
+
const graph = {
|
|
114
|
+
id: "root",
|
|
115
|
+
layoutOptions: {
|
|
116
|
+
"elk.algorithm": "layered",
|
|
117
|
+
"elk.direction": direction === "LR" ? "RIGHT" : "DOWN",
|
|
118
|
+
"elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES",
|
|
119
|
+
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
|
|
120
|
+
"elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED",
|
|
121
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": String(LAYER_GAP),
|
|
122
|
+
"elk.spacing.nodeNode": String(SIBLING_GAP),
|
|
123
|
+
"elk.spacing.componentComponent": String(48),
|
|
124
|
+
"elk.separateConnectedComponents": "true",
|
|
125
|
+
"elk.padding": "[top=0,left=0,bottom=0,right=0]"
|
|
126
|
+
},
|
|
127
|
+
children: units.map((u) => ({
|
|
128
|
+
id: u.id,
|
|
129
|
+
width: u.width,
|
|
130
|
+
height: u.height
|
|
131
|
+
})),
|
|
132
|
+
edges: links.map((l, i) => ({
|
|
133
|
+
id: `e${i}`,
|
|
134
|
+
sources: [l.from],
|
|
135
|
+
targets: [l.to]
|
|
136
|
+
}))
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
const out = await elk.layout(graph);
|
|
140
|
+
const at = /* @__PURE__ */ new Map();
|
|
141
|
+
for (const c of out.children ?? []) at.set(c.id, {
|
|
142
|
+
x: c.x ?? 0,
|
|
143
|
+
y: c.y ?? 0
|
|
144
|
+
});
|
|
145
|
+
return at;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Without ELK: one row (TB) or column (LR) per spawn depth, each centred. */
|
|
151
|
+
function placeByDepth(units, links, direction) {
|
|
152
|
+
const parents = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const l of links) parents.set(l.to, [...parents.get(l.to) ?? [], l.from]);
|
|
154
|
+
const depth = /* @__PURE__ */ new Map();
|
|
155
|
+
const depthOf = (id, path) => {
|
|
156
|
+
const known = depth.get(id);
|
|
157
|
+
if (known !== void 0) return known;
|
|
158
|
+
if (path.has(id)) return 0;
|
|
159
|
+
path.add(id);
|
|
160
|
+
const d = Math.max(-1, ...(parents.get(id) ?? []).map((p) => depthOf(p, path))) + 1;
|
|
161
|
+
path.delete(id);
|
|
162
|
+
depth.set(id, d);
|
|
163
|
+
return d;
|
|
164
|
+
};
|
|
165
|
+
const layers = [];
|
|
166
|
+
for (const u of units) (layers[depthOf(u.id, /* @__PURE__ */ new Set())] ??= []).push(u);
|
|
167
|
+
const along = (u) => direction === "TB" ? u.width : u.height;
|
|
168
|
+
const across = (u) => direction === "TB" ? u.height : u.width;
|
|
169
|
+
const extents = layers.map((l) => l.reduce((sum, u) => sum + along(u), 0) + (l.length - 1) * SIBLING_GAP);
|
|
170
|
+
const widest = Math.max(...extents);
|
|
171
|
+
const at = /* @__PURE__ */ new Map();
|
|
172
|
+
let offset = 0;
|
|
173
|
+
layers.forEach((layer, i) => {
|
|
174
|
+
let cursor = (widest - extents[i]) / 2;
|
|
175
|
+
for (const u of layer) {
|
|
176
|
+
at.set(u.id, direction === "TB" ? {
|
|
177
|
+
x: cursor,
|
|
178
|
+
y: offset
|
|
179
|
+
} : {
|
|
180
|
+
x: offset,
|
|
181
|
+
y: cursor
|
|
182
|
+
});
|
|
183
|
+
cursor += along(u) + SIBLING_GAP;
|
|
184
|
+
}
|
|
185
|
+
offset += Math.max(...layer.map(across)) + LAYER_GAP;
|
|
186
|
+
});
|
|
187
|
+
return at;
|
|
188
|
+
}
|
|
189
|
+
/** Lay the run out with every grid `columns` wide. */
|
|
190
|
+
async function layoutAgents(topology, direction, columns, card = AGENT_CARD) {
|
|
191
|
+
const { units, links } = layoutUnits(topology, columns, card);
|
|
192
|
+
const placed = await placeWithElk(units, links, direction) ?? placeByDepth(units, links, direction);
|
|
193
|
+
const positions = /* @__PURE__ */ new Map();
|
|
194
|
+
const frames = [];
|
|
195
|
+
let width = 0;
|
|
196
|
+
let height = 0;
|
|
197
|
+
for (const u of units) {
|
|
198
|
+
const p = placed.get(u.id) ?? {
|
|
199
|
+
x: 0,
|
|
200
|
+
y: 0
|
|
201
|
+
};
|
|
202
|
+
width = Math.max(width, p.x + u.width);
|
|
203
|
+
height = Math.max(height, p.y + u.height);
|
|
204
|
+
positions.set(u.id, p);
|
|
205
|
+
}
|
|
206
|
+
for (const g of topology.groups) {
|
|
207
|
+
const origin = positions.get(g.id) ?? {
|
|
208
|
+
x: 0,
|
|
209
|
+
y: 0
|
|
210
|
+
};
|
|
211
|
+
positions.delete(g.id);
|
|
212
|
+
const size = gridSize(g.memberIds.length, columns, card);
|
|
213
|
+
frames.push({
|
|
214
|
+
group: g,
|
|
215
|
+
...origin,
|
|
216
|
+
...size
|
|
217
|
+
});
|
|
218
|
+
const cols = Math.max(1, Math.min(columns, g.memberIds.length));
|
|
219
|
+
g.memberIds.forEach((id, i) => {
|
|
220
|
+
positions.set(id, {
|
|
221
|
+
x: origin.x + FRAME_PAD + i % cols * (card.width + GRID_GAP),
|
|
222
|
+
y: origin.y + 26 + Math.floor(i / cols) * (card.height + GRID_GAP)
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
columns,
|
|
228
|
+
positions,
|
|
229
|
+
frames,
|
|
230
|
+
width,
|
|
231
|
+
height
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** The grid widths worth trying for this run: one per column count a grid can use. */
|
|
235
|
+
function candidateColumns(topology) {
|
|
236
|
+
const largest = Math.max(0, ...topology.groups.map((g) => g.memberIds.length));
|
|
237
|
+
const top = Math.min(MAX_GRID_COLUMNS, largest);
|
|
238
|
+
return top <= 1 ? [1] : Array.from({ length: top }, (_, i) => i + 1);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* The layout that shows the run largest in this canvas: the highest zoom at
|
|
242
|
+
* which it fits whole, capped at the cards' designed size. Layouts that all
|
|
243
|
+
* reach that cap tie, and the tie goes to the one whose shape is closest to the
|
|
244
|
+
* canvas's, so a wide canvas gets a wide grid and a phone a tall one.
|
|
245
|
+
*/
|
|
246
|
+
function pickAgentLayout(layouts, canvas, padding) {
|
|
247
|
+
let best = null;
|
|
248
|
+
let bestZoom = -1;
|
|
249
|
+
let bestShape = Number.POSITIVE_INFINITY;
|
|
250
|
+
const aspect = canvas.width / Math.max(1, canvas.height);
|
|
251
|
+
for (const l of layouts) {
|
|
252
|
+
const zoom = Math.min(1, (canvas.width - 2 * padding) / Math.max(1, l.width), (canvas.height - 2 * padding) / Math.max(1, l.height));
|
|
253
|
+
const shape = Math.abs(Math.log(l.width / Math.max(1, l.height) / aspect));
|
|
254
|
+
if (zoom > bestZoom + .02 || Math.abs(zoom - bestZoom) <= .02 && shape < bestShape) {
|
|
255
|
+
best = l;
|
|
256
|
+
bestZoom = zoom;
|
|
257
|
+
bestShape = shape;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return best;
|
|
261
|
+
}
|
|
262
|
+
// src/workflows/agent-graph-parts.tsx
|
|
263
|
+
/**
|
|
264
|
+
* What `AgentGraph` draws: an agent's card, the frame around a grid of leaf
|
|
265
|
+
* workers, and the curved edge a message or delegation travels along.
|
|
266
|
+
*
|
|
267
|
+
* The visual vocabulary is the workflow graph's own (`node-ui.tsx`): the same
|
|
268
|
+
* model brand mark, status colours, resting elevation and selection ring, so an
|
|
269
|
+
* agent run and a workflow run read as one system.
|
|
270
|
+
*/
|
|
271
|
+
const MUTED = "hsl(var(--muted-foreground))";
|
|
272
|
+
/** One word, one glyph and one colour per agent status — never colour alone.
|
|
273
|
+
* `flow` is the workflow status whose border, glow and edge colour it shares. */
|
|
274
|
+
const AGENT_STATUS = {
|
|
275
|
+
running: {
|
|
276
|
+
label: "Running",
|
|
277
|
+
icon: LoaderCircle,
|
|
278
|
+
color: STATUS_COLOR.running,
|
|
279
|
+
flow: "running"
|
|
280
|
+
},
|
|
281
|
+
waiting: {
|
|
282
|
+
label: "Waiting",
|
|
283
|
+
icon: Hourglass,
|
|
284
|
+
color: STATUS_COLOR.waiting,
|
|
285
|
+
flow: "waiting"
|
|
286
|
+
},
|
|
287
|
+
"needs-you": {
|
|
288
|
+
label: "Needs you",
|
|
289
|
+
icon: Hand,
|
|
290
|
+
color: STATUS_COLOR.waiting,
|
|
291
|
+
flow: "waiting"
|
|
292
|
+
},
|
|
293
|
+
succeeded: {
|
|
294
|
+
label: "Done",
|
|
295
|
+
icon: CircleCheck,
|
|
296
|
+
color: STATUS_COLOR.succeeded,
|
|
297
|
+
flow: "succeeded"
|
|
298
|
+
},
|
|
299
|
+
failed: {
|
|
300
|
+
label: "Failed",
|
|
301
|
+
icon: CircleX,
|
|
302
|
+
color: STATUS_COLOR.failed,
|
|
303
|
+
flow: "failed"
|
|
304
|
+
},
|
|
305
|
+
stopped: {
|
|
306
|
+
label: "Stopped",
|
|
307
|
+
icon: CircleStop,
|
|
308
|
+
color: MUTED,
|
|
309
|
+
flow: "succeeded"
|
|
310
|
+
},
|
|
311
|
+
idle: {
|
|
312
|
+
label: "Idle",
|
|
313
|
+
icon: CirclePause,
|
|
314
|
+
color: MUTED,
|
|
315
|
+
flow: "queued"
|
|
316
|
+
},
|
|
317
|
+
unknown: {
|
|
318
|
+
label: "Unknown",
|
|
319
|
+
icon: CircleQuestionMark,
|
|
320
|
+
color: MUTED,
|
|
321
|
+
flow: "queued"
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
/** A status this table has no entry for (a newer host) reads as unknown, not as a crash. */
|
|
325
|
+
function agentStatus(status) {
|
|
326
|
+
return AGENT_STATUS[status] ?? AGENT_STATUS.unknown;
|
|
327
|
+
}
|
|
328
|
+
function StatusGlyph({ status, size }) {
|
|
329
|
+
const s = agentStatus(status);
|
|
330
|
+
const Icon = s.icon;
|
|
331
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
332
|
+
"aria-hidden": true,
|
|
333
|
+
size,
|
|
334
|
+
strokeWidth: 2.25,
|
|
335
|
+
className: status === "running" ? "shrink-0 motion-safe:animate-spin [animation-duration:1.6s]" : "shrink-0",
|
|
336
|
+
style: { color: s.color }
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function cardBorder(status) {
|
|
340
|
+
if (status === "unknown") return {
|
|
341
|
+
borderColor: "hsl(var(--muted-foreground) / 0.45)",
|
|
342
|
+
borderStyle: "dashed"
|
|
343
|
+
};
|
|
344
|
+
if (status === "idle") return { borderColor: "hsl(var(--border))" };
|
|
345
|
+
if (status === "stopped") return {
|
|
346
|
+
borderColor: RESTING_BORDER,
|
|
347
|
+
boxShadow: RESTING_SHADOW
|
|
348
|
+
};
|
|
349
|
+
return statusBorder(agentStatus(status).flow);
|
|
350
|
+
}
|
|
351
|
+
/** The words a reader without the picture receives for one agent. */
|
|
352
|
+
function agentSummary(agent) {
|
|
353
|
+
const cost = fmtCost(agent.costUsd ?? void 0);
|
|
354
|
+
const tokens = fmtTokenTotal(agent.tokens);
|
|
355
|
+
return [
|
|
356
|
+
agent.label,
|
|
357
|
+
agent.role,
|
|
358
|
+
agent.harness,
|
|
359
|
+
agent.model,
|
|
360
|
+
agentStatus(agent.status).label,
|
|
361
|
+
agent.turn,
|
|
362
|
+
cost,
|
|
363
|
+
tokens
|
|
364
|
+
].filter(Boolean).join(", ");
|
|
365
|
+
}
|
|
366
|
+
/** Set on each card's React Flow wrapper — the element that takes keyboard
|
|
367
|
+
* focus — so the card inside can draw the focus ring. */
|
|
368
|
+
const AGENT_NODE_CLASS = "group/agent !outline-none";
|
|
369
|
+
const HANDLE_CLASS = "!h-1 !w-1 !min-w-0 !border-0 !bg-transparent opacity-0";
|
|
370
|
+
function Handles({ direction, source = true }) {
|
|
371
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Handle, {
|
|
372
|
+
type: "target",
|
|
373
|
+
position: direction === "LR" ? Position.Left : Position.Top,
|
|
374
|
+
isConnectable: false,
|
|
375
|
+
className: HANDLE_CLASS
|
|
376
|
+
}), source && /* @__PURE__ */ jsx(Handle, {
|
|
377
|
+
type: "source",
|
|
378
|
+
position: direction === "LR" ? Position.Right : Position.Bottom,
|
|
379
|
+
isConnectable: false,
|
|
380
|
+
className: HANDLE_CLASS
|
|
381
|
+
})] });
|
|
382
|
+
}
|
|
383
|
+
function AgentCard({ data }) {
|
|
384
|
+
const { agent, selected, direction } = data;
|
|
385
|
+
const s = agentStatus(agent.status);
|
|
386
|
+
const meta = [agent.role, agent.harness].filter(Boolean).join(" · ");
|
|
387
|
+
const cost = fmtCost(agent.costUsd ?? void 0);
|
|
388
|
+
const tokens = fmtTokenTotal(agent.tokens);
|
|
389
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
390
|
+
"data-status": agent.status,
|
|
391
|
+
"data-selected": selected || void 0,
|
|
392
|
+
title: agentSummary(agent),
|
|
393
|
+
className: `relative flex h-full w-full cursor-pointer flex-col justify-between rounded-xl border bg-card px-3 pt-2.5 pb-2 text-left hover:-translate-y-px motion-reduce:hover:translate-y-0 after:pointer-events-none after:absolute after:inset-0 after:rounded-xl after:opacity-0 after:shadow-[0_10px_28px_-10px_hsl(var(--foreground)/0.45)] after:transition-opacity after:duration-150 after:ease-out hover:after:opacity-100 motion-reduce:after:transition-none group-focus-visible/agent:outline-2 group-focus-visible/agent:outline-offset-4 group-focus-visible/agent:outline-ring ${MOTION_CONTROL}`,
|
|
394
|
+
style: {
|
|
395
|
+
boxShadow: RESTING_SHADOW,
|
|
396
|
+
...cardBorder(agent.status),
|
|
397
|
+
...selected ? SELECTION_OUTLINE : {}
|
|
398
|
+
},
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ jsx(Handles, { direction }),
|
|
401
|
+
/* @__PURE__ */ jsxs("div", {
|
|
402
|
+
className: "flex min-w-0 items-center gap-2.5",
|
|
403
|
+
children: [
|
|
404
|
+
/* @__PURE__ */ jsx(NodeMark, {
|
|
405
|
+
kind: "agent.run",
|
|
406
|
+
model: agent.model ?? void 0,
|
|
407
|
+
accent: KIND_ACCENT["agent.run"],
|
|
408
|
+
tile: 30
|
|
409
|
+
}),
|
|
410
|
+
/* @__PURE__ */ jsxs("div", {
|
|
411
|
+
className: "min-w-0 flex-1",
|
|
412
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
413
|
+
className: "truncate font-semibold text-[13px] text-foreground leading-[18px] tracking-[-0.01em]",
|
|
414
|
+
children: agent.label
|
|
415
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
416
|
+
className: "truncate text-[11px] text-muted-foreground leading-4",
|
|
417
|
+
children: meta || "\xA0"
|
|
418
|
+
})]
|
|
419
|
+
}),
|
|
420
|
+
/* @__PURE__ */ jsx("div", {
|
|
421
|
+
className: "flex shrink-0 flex-col items-end gap-0.5 self-start",
|
|
422
|
+
children: /* @__PURE__ */ jsx(StatusGlyph, {
|
|
423
|
+
status: agent.status,
|
|
424
|
+
size: 15
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
]
|
|
428
|
+
}),
|
|
429
|
+
/* @__PURE__ */ jsxs("div", {
|
|
430
|
+
className: "flex min-w-0 items-center gap-1.5 text-[11px] leading-4",
|
|
431
|
+
children: [
|
|
432
|
+
/* @__PURE__ */ jsx("span", {
|
|
433
|
+
className: "shrink-0 font-medium",
|
|
434
|
+
style: { color: s.color === MUTED ? void 0 : s.color },
|
|
435
|
+
children: s.label
|
|
436
|
+
}),
|
|
437
|
+
/* @__PURE__ */ jsx("span", {
|
|
438
|
+
className: "min-w-0 flex-1 truncate text-muted-foreground",
|
|
439
|
+
children: agent.turn ? agent.turn : ""
|
|
440
|
+
}),
|
|
441
|
+
(cost ?? tokens) && /* @__PURE__ */ jsx("span", {
|
|
442
|
+
className: "shrink-0 text-muted-foreground tabular-nums",
|
|
443
|
+
children: cost ?? tokens
|
|
444
|
+
})
|
|
445
|
+
]
|
|
446
|
+
})
|
|
447
|
+
]
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/** The statuses a grid caption counts: the ones a reader looks for. */
|
|
451
|
+
const CAPTION_STATUSES = [
|
|
452
|
+
"running",
|
|
453
|
+
"waiting",
|
|
454
|
+
"failed"
|
|
455
|
+
];
|
|
456
|
+
function AgentFrameBox({ data }) {
|
|
457
|
+
const { frame, statuses, direction } = data;
|
|
458
|
+
const counts = CAPTION_STATUSES.map((status) => ({
|
|
459
|
+
status,
|
|
460
|
+
n: statuses.filter((s) => s === status).length
|
|
461
|
+
})).filter((c) => c.n > 0);
|
|
462
|
+
const words = [`${frame.group.memberIds.length} agents`, ...counts.map((c) => `${c.n} ${agentStatus(c.status).label.toLowerCase()}`)].join(", ");
|
|
463
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
464
|
+
className: "relative h-full w-full rounded-2xl border",
|
|
465
|
+
style: {
|
|
466
|
+
background: "color-mix(in srgb, hsl(var(--muted-foreground)) 7%, transparent)",
|
|
467
|
+
borderColor: "hsl(var(--foreground) / 0.08)"
|
|
468
|
+
},
|
|
469
|
+
children: [/* @__PURE__ */ jsx(Handles, {
|
|
470
|
+
direction,
|
|
471
|
+
source: false
|
|
472
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
473
|
+
className: "flex h-[26px] items-center gap-2.5 px-3 text-[11px] text-muted-foreground leading-4",
|
|
474
|
+
children: [
|
|
475
|
+
/* @__PURE__ */ jsx("span", {
|
|
476
|
+
className: "sr-only",
|
|
477
|
+
children: words
|
|
478
|
+
}),
|
|
479
|
+
/* @__PURE__ */ jsxs("span", {
|
|
480
|
+
"aria-hidden": true,
|
|
481
|
+
className: "flex items-center gap-1.5 font-medium",
|
|
482
|
+
children: [
|
|
483
|
+
/* @__PURE__ */ jsx(Users, {
|
|
484
|
+
size: 12,
|
|
485
|
+
strokeWidth: 2
|
|
486
|
+
}),
|
|
487
|
+
frame.group.memberIds.length,
|
|
488
|
+
" agents"
|
|
489
|
+
]
|
|
490
|
+
}),
|
|
491
|
+
counts.map((c) => /* @__PURE__ */ jsxs("span", {
|
|
492
|
+
"aria-hidden": true,
|
|
493
|
+
className: "flex items-center gap-1 tabular-nums",
|
|
494
|
+
children: [/* @__PURE__ */ jsx(StatusGlyph, {
|
|
495
|
+
status: c.status,
|
|
496
|
+
size: 11
|
|
497
|
+
}), c.n]
|
|
498
|
+
}, c.status))
|
|
499
|
+
]
|
|
500
|
+
})]
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/** The secondary edges' colours: a message is quiet, a delegation is the
|
|
504
|
+
* primary accent's wash. Both stay well under a spawn edge's weight. */
|
|
505
|
+
const MESSAGE_EDGE_COLOR = {
|
|
506
|
+
message: "color-mix(in srgb, hsl(var(--muted-foreground)) 70%, transparent)",
|
|
507
|
+
delegation: "color-mix(in srgb, hsl(var(--primary)) 75%, transparent)"
|
|
508
|
+
};
|
|
509
|
+
function boxOf(node) {
|
|
510
|
+
return {
|
|
511
|
+
x: node.internals.positionAbsolute.x,
|
|
512
|
+
y: node.internals.positionAbsolute.y,
|
|
513
|
+
w: node.measured.width ?? node.width ?? 0,
|
|
514
|
+
h: node.measured.height ?? node.height ?? 0
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
/** Where the line from a box's centre toward `toward` leaves the box, `gap`px out. */
|
|
518
|
+
function exitPoint(b, toward, gap) {
|
|
519
|
+
const cx = b.x + b.w / 2;
|
|
520
|
+
const cy = b.y + b.h / 2;
|
|
521
|
+
const dx = toward.x - cx;
|
|
522
|
+
const dy = toward.y - cy;
|
|
523
|
+
const len = Math.hypot(dx, dy);
|
|
524
|
+
if (len === 0) return {
|
|
525
|
+
x: cx,
|
|
526
|
+
y: cy
|
|
527
|
+
};
|
|
528
|
+
const scale = Math.min(dx === 0 ? Number.POSITIVE_INFINITY : b.w / 2 / Math.abs(dx), dy === 0 ? Number.POSITIVE_INFINITY : b.h / 2 / Math.abs(dy));
|
|
529
|
+
return {
|
|
530
|
+
x: cx + dx * scale + dx / len * gap,
|
|
531
|
+
y: cy + dy * scale + dy / len * gap
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* A curve from one card's border to another's, bowed to the right of its
|
|
536
|
+
* direction of travel: two agents messaging each other both ways draw two
|
|
537
|
+
* separate arcs instead of one line over another, and a curve never hides
|
|
538
|
+
* under the straight spawn edges of the tree.
|
|
539
|
+
*/
|
|
540
|
+
function messageCurve(a, b) {
|
|
541
|
+
const ac = {
|
|
542
|
+
x: a.x + a.w / 2,
|
|
543
|
+
y: a.y + a.h / 2
|
|
544
|
+
};
|
|
545
|
+
const bc = {
|
|
546
|
+
x: b.x + b.w / 2,
|
|
547
|
+
y: b.y + b.h / 2
|
|
548
|
+
};
|
|
549
|
+
const dx = bc.x - ac.x;
|
|
550
|
+
const dy = bc.y - ac.y;
|
|
551
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
552
|
+
const bend = Math.min(90, Math.max(24, len * .22));
|
|
553
|
+
const control = {
|
|
554
|
+
x: (ac.x + bc.x) / 2 - dy / len * bend,
|
|
555
|
+
y: (ac.y + bc.y) / 2 + dx / len * bend
|
|
556
|
+
};
|
|
557
|
+
const start = exitPoint(a, control, 2);
|
|
558
|
+
const end = exitPoint(b, control, 4);
|
|
559
|
+
return {
|
|
560
|
+
path: `M${start.x},${start.y} Q${control.x},${control.y} ${end.x},${end.y}`,
|
|
561
|
+
mid: {
|
|
562
|
+
x: .25 * start.x + .5 * control.x + .25 * end.x,
|
|
563
|
+
y: .25 * start.y + .5 * control.y + .25 * end.y
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function AgentMessageEdgeView({ id, source, target, data, markerEnd }) {
|
|
568
|
+
const from = useInternalNode(source);
|
|
569
|
+
const to = useInternalNode(target);
|
|
570
|
+
if (!from || !to || !data) return null;
|
|
571
|
+
const { path, mid } = messageCurve(boxOf(from), boxOf(to));
|
|
572
|
+
const color = MESSAGE_EDGE_COLOR[data.kind] ?? MESSAGE_EDGE_COLOR.message;
|
|
573
|
+
const count = data.count ?? 1;
|
|
574
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(BaseEdge, {
|
|
575
|
+
id,
|
|
576
|
+
path,
|
|
577
|
+
markerEnd,
|
|
578
|
+
interactionWidth: 10,
|
|
579
|
+
style: {
|
|
580
|
+
stroke: color,
|
|
581
|
+
strokeWidth: 1.25,
|
|
582
|
+
strokeDasharray: "4 4",
|
|
583
|
+
fill: "none"
|
|
584
|
+
}
|
|
585
|
+
}), count > 1 && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs("div", {
|
|
586
|
+
title: data.label ? `${data.label} ×${count}` : `${count} ${data.kind}s`,
|
|
587
|
+
className: "nodrag nopan pointer-events-auto absolute rounded-full border bg-card px-1.5 font-medium text-[10px] text-muted-foreground tabular-nums leading-4 shadow-sm",
|
|
588
|
+
style: {
|
|
589
|
+
transform: `translate(-50%, -50%) translate(${mid.x}px, ${mid.y}px)`,
|
|
590
|
+
borderColor: color
|
|
591
|
+
},
|
|
592
|
+
children: ["×", count]
|
|
593
|
+
}) })] });
|
|
594
|
+
}
|
|
595
|
+
// src/workflows/agent-live-card.tsx
|
|
596
|
+
const SETTLED = /* @__PURE__ */ new Set([
|
|
597
|
+
"succeeded",
|
|
598
|
+
"failed",
|
|
599
|
+
"stopped"
|
|
600
|
+
]);
|
|
601
|
+
const FIGURES = {
|
|
602
|
+
thinking: [
|
|
603
|
+
[
|
|
604
|
+
" o ",
|
|
605
|
+
" /|\\ ",
|
|
606
|
+
" / \\ "
|
|
607
|
+
],
|
|
608
|
+
[
|
|
609
|
+
" o . ",
|
|
610
|
+
" /|\\ ",
|
|
611
|
+
" / \\ "
|
|
612
|
+
],
|
|
613
|
+
[
|
|
614
|
+
" o .. ",
|
|
615
|
+
" /|\\ ",
|
|
616
|
+
" / \\ "
|
|
617
|
+
],
|
|
618
|
+
[
|
|
619
|
+
" o ...",
|
|
620
|
+
" /|\\ ",
|
|
621
|
+
" / \\ "
|
|
622
|
+
]
|
|
623
|
+
],
|
|
624
|
+
reading: [
|
|
625
|
+
[
|
|
626
|
+
" o ",
|
|
627
|
+
" /|\\[-]",
|
|
628
|
+
" / \\ "
|
|
629
|
+
],
|
|
630
|
+
[
|
|
631
|
+
" o ",
|
|
632
|
+
" /|\\[=]",
|
|
633
|
+
" / \\ "
|
|
634
|
+
],
|
|
635
|
+
[
|
|
636
|
+
" o ",
|
|
637
|
+
" /|\\[#]",
|
|
638
|
+
" / \\ "
|
|
639
|
+
],
|
|
640
|
+
[
|
|
641
|
+
" o ",
|
|
642
|
+
" /|\\[=]",
|
|
643
|
+
" / \\ "
|
|
644
|
+
]
|
|
645
|
+
],
|
|
646
|
+
building: [
|
|
647
|
+
[
|
|
648
|
+
" o T ",
|
|
649
|
+
" /|\\/ ",
|
|
650
|
+
" / \\## "
|
|
651
|
+
],
|
|
652
|
+
[
|
|
653
|
+
" o ",
|
|
654
|
+
" /|\\-T ",
|
|
655
|
+
" / \\## "
|
|
656
|
+
],
|
|
657
|
+
[
|
|
658
|
+
" o T ",
|
|
659
|
+
" /|\\/ ",
|
|
660
|
+
" / \\###"
|
|
661
|
+
],
|
|
662
|
+
[
|
|
663
|
+
" o ",
|
|
664
|
+
" /|\\-T ",
|
|
665
|
+
" / \\###"
|
|
666
|
+
]
|
|
667
|
+
],
|
|
668
|
+
proving: [
|
|
669
|
+
[
|
|
670
|
+
" o |- ",
|
|
671
|
+
" /|\\ ",
|
|
672
|
+
" / \\ "
|
|
673
|
+
],
|
|
674
|
+
[
|
|
675
|
+
" o |= ",
|
|
676
|
+
" /|\\ ",
|
|
677
|
+
" / \\ "
|
|
678
|
+
],
|
|
679
|
+
[
|
|
680
|
+
" o |=-",
|
|
681
|
+
" /|\\ ",
|
|
682
|
+
" / \\ "
|
|
683
|
+
],
|
|
684
|
+
[
|
|
685
|
+
" o QED",
|
|
686
|
+
" /|\\ ",
|
|
687
|
+
" / \\ "
|
|
688
|
+
]
|
|
689
|
+
],
|
|
690
|
+
writing: [
|
|
691
|
+
[
|
|
692
|
+
" o ",
|
|
693
|
+
" /|\\_/ ",
|
|
694
|
+
" / \\. "
|
|
695
|
+
],
|
|
696
|
+
[
|
|
697
|
+
" o ",
|
|
698
|
+
" /|\\_/ ",
|
|
699
|
+
" / \\.. "
|
|
700
|
+
],
|
|
701
|
+
[
|
|
702
|
+
" o ",
|
|
703
|
+
" /|\\_/ ",
|
|
704
|
+
" / \\..."
|
|
705
|
+
],
|
|
706
|
+
[
|
|
707
|
+
" o ",
|
|
708
|
+
" /|\\_/ ",
|
|
709
|
+
" / \\ "
|
|
710
|
+
]
|
|
711
|
+
],
|
|
712
|
+
waiting: [
|
|
713
|
+
[
|
|
714
|
+
" o z ",
|
|
715
|
+
" /|\\ ",
|
|
716
|
+
" / \\ "
|
|
717
|
+
],
|
|
718
|
+
[
|
|
719
|
+
" o zZ",
|
|
720
|
+
" /|\\ ",
|
|
721
|
+
" / \\ "
|
|
722
|
+
],
|
|
723
|
+
[
|
|
724
|
+
" o zZz",
|
|
725
|
+
" /|\\ ",
|
|
726
|
+
" / \\ "
|
|
727
|
+
],
|
|
728
|
+
[
|
|
729
|
+
" o ",
|
|
730
|
+
" /|\\ ",
|
|
731
|
+
" / \\ "
|
|
732
|
+
]
|
|
733
|
+
],
|
|
734
|
+
restarting: [
|
|
735
|
+
[
|
|
736
|
+
" o | ",
|
|
737
|
+
" /|\\ ",
|
|
738
|
+
" / \\ "
|
|
739
|
+
],
|
|
740
|
+
[
|
|
741
|
+
" o / ",
|
|
742
|
+
" /|\\ ",
|
|
743
|
+
" / \\ "
|
|
744
|
+
],
|
|
745
|
+
[
|
|
746
|
+
" o - ",
|
|
747
|
+
" /|\\ ",
|
|
748
|
+
" / \\ "
|
|
749
|
+
],
|
|
750
|
+
[
|
|
751
|
+
" o \\ ",
|
|
752
|
+
" /|\\ ",
|
|
753
|
+
" / \\ "
|
|
754
|
+
]
|
|
755
|
+
]
|
|
756
|
+
};
|
|
757
|
+
/** The activity a status implies when the host sends none. */
|
|
758
|
+
function activityOf(agent) {
|
|
759
|
+
if (agent.activity && FIGURES[agent.activity]) return agent.activity;
|
|
760
|
+
if (agent.status === "waiting" || agent.status === "needs-you" || agent.status === "idle") return "waiting";
|
|
761
|
+
return "thinking";
|
|
762
|
+
}
|
|
763
|
+
/** Every frame is stacked in one block and a stepped animation slides it: no timer per card, and reduced motion keeps
|
|
764
|
+
* the first frame. */
|
|
765
|
+
function Figure({ activity }) {
|
|
766
|
+
const frames = FIGURES[activity];
|
|
767
|
+
return /* @__PURE__ */ jsx("div", {
|
|
768
|
+
className: "ag-live-figure",
|
|
769
|
+
"aria-hidden": true,
|
|
770
|
+
"data-activity": activity,
|
|
771
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
772
|
+
style: { ["--ag-frames"]: frames.length },
|
|
773
|
+
children: frames.map((frame) => frame.join("\n")).join("\n")
|
|
774
|
+
})
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function aliveFor(startedAt, now) {
|
|
778
|
+
if (startedAt == null || !Number.isFinite(startedAt)) return void 0;
|
|
779
|
+
const s = Math.max(0, Math.round((now - startedAt) / 1e3));
|
|
780
|
+
if (s < 60) return `${s}s`;
|
|
781
|
+
const m = Math.floor(s / 60);
|
|
782
|
+
if (m < 60) return `${m}m`;
|
|
783
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
784
|
+
}
|
|
785
|
+
/** Time alive ticks once a second while the host follows now; a replay passes its own clock. */
|
|
786
|
+
function useNow(fixed, live) {
|
|
787
|
+
const [now, setNow] = useState(() => fixed ?? Date.now());
|
|
788
|
+
useEffect(() => {
|
|
789
|
+
if (fixed !== void 0 || !live) return;
|
|
790
|
+
const timer = setInterval(() => setNow(Date.now()), 1e3);
|
|
791
|
+
return () => clearInterval(timer);
|
|
792
|
+
}, [fixed, live]);
|
|
793
|
+
return fixed ?? now;
|
|
794
|
+
}
|
|
795
|
+
function LiveAgentCard({ data }) {
|
|
796
|
+
const { agent, selected, direction } = data;
|
|
797
|
+
const settled = SETTLED.has(agent.status);
|
|
798
|
+
const now = useNow(data.now, !settled);
|
|
799
|
+
const s = agentStatus(agent.status);
|
|
800
|
+
const alive = aliveFor(agent.startedAt, now);
|
|
801
|
+
if (settled) return /* @__PURE__ */ jsxs("div", {
|
|
802
|
+
className: "ag-live-slot",
|
|
803
|
+
"data-status": agent.status,
|
|
804
|
+
title: agentSummary(agent),
|
|
805
|
+
children: [/* @__PURE__ */ jsx(Handles, { direction }), /* @__PURE__ */ jsxs("div", {
|
|
806
|
+
className: "ag-live-chip",
|
|
807
|
+
"data-selected": selected || void 0,
|
|
808
|
+
style: selected ? SELECTION_OUTLINE : void 0,
|
|
809
|
+
children: [
|
|
810
|
+
/* @__PURE__ */ jsx(StatusGlyph, {
|
|
811
|
+
status: agent.status,
|
|
812
|
+
size: 13
|
|
813
|
+
}),
|
|
814
|
+
/* @__PURE__ */ jsx("span", {
|
|
815
|
+
className: "ag-live-chip-name",
|
|
816
|
+
children: agent.label
|
|
817
|
+
}),
|
|
818
|
+
/* @__PURE__ */ jsx("span", {
|
|
819
|
+
className: "ag-live-chip-result",
|
|
820
|
+
children: agent.result ?? s.label
|
|
821
|
+
})
|
|
822
|
+
]
|
|
823
|
+
})]
|
|
824
|
+
});
|
|
825
|
+
const meta = [agent.model, agent.effort].filter(Boolean).join(" · ");
|
|
826
|
+
const events = (agent.events ?? []).slice(-3);
|
|
827
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
828
|
+
className: "ag-live-card",
|
|
829
|
+
"data-status": agent.status,
|
|
830
|
+
"data-selected": selected || void 0,
|
|
831
|
+
title: agentSummary(agent),
|
|
832
|
+
style: {
|
|
833
|
+
boxShadow: RESTING_SHADOW,
|
|
834
|
+
...cardBorder(agent.status),
|
|
835
|
+
...selected ? SELECTION_OUTLINE : {}
|
|
836
|
+
},
|
|
837
|
+
children: [
|
|
838
|
+
/* @__PURE__ */ jsx(Handles, { direction }),
|
|
839
|
+
/* @__PURE__ */ jsxs("div", {
|
|
840
|
+
className: "ag-live-head",
|
|
841
|
+
children: [
|
|
842
|
+
/* @__PURE__ */ jsx(NodeMark, {
|
|
843
|
+
kind: "agent.run",
|
|
844
|
+
model: agent.model ?? void 0,
|
|
845
|
+
accent: KIND_ACCENT["agent.run"],
|
|
846
|
+
tile: 24
|
|
847
|
+
}),
|
|
848
|
+
/* @__PURE__ */ jsxs("div", {
|
|
849
|
+
className: "ag-live-names",
|
|
850
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
851
|
+
className: "ag-live-name",
|
|
852
|
+
children: agent.label
|
|
853
|
+
}), meta && /* @__PURE__ */ jsx("span", {
|
|
854
|
+
className: "ag-live-meta",
|
|
855
|
+
children: meta
|
|
856
|
+
})]
|
|
857
|
+
}),
|
|
858
|
+
/* @__PURE__ */ jsxs("div", {
|
|
859
|
+
className: "ag-live-stats",
|
|
860
|
+
children: [alive && /* @__PURE__ */ jsx("span", {
|
|
861
|
+
title: "Time alive",
|
|
862
|
+
children: alive
|
|
863
|
+
}), agent.commits != null && /* @__PURE__ */ jsxs("span", {
|
|
864
|
+
title: "Commits shipped",
|
|
865
|
+
children: [
|
|
866
|
+
agent.commits,
|
|
867
|
+
" ",
|
|
868
|
+
agent.commits === 1 ? "commit" : "commits"
|
|
869
|
+
]
|
|
870
|
+
})]
|
|
871
|
+
})
|
|
872
|
+
]
|
|
873
|
+
}),
|
|
874
|
+
/* @__PURE__ */ jsxs("div", {
|
|
875
|
+
className: "ag-live-body",
|
|
876
|
+
children: [/* @__PURE__ */ jsx(Figure, { activity: activityOf(agent) }), /* @__PURE__ */ jsxs("div", {
|
|
877
|
+
className: "ag-live-text",
|
|
878
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
879
|
+
className: "ag-live-thinking",
|
|
880
|
+
children: agent.turn || /* @__PURE__ */ jsx("span", {
|
|
881
|
+
className: "ag-live-quiet",
|
|
882
|
+
children: s.label
|
|
883
|
+
})
|
|
884
|
+
}, agent.turn ?? ""), /* @__PURE__ */ jsx("ol", {
|
|
885
|
+
className: "ag-live-events",
|
|
886
|
+
"aria-label": "Latest events",
|
|
887
|
+
children: events.map((event) => /* @__PURE__ */ jsx("li", { children: event.label }, event.id))
|
|
888
|
+
})]
|
|
889
|
+
})]
|
|
890
|
+
})
|
|
891
|
+
]
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
// src/workflows/AgentGraphCanvas.tsx
|
|
895
|
+
/**
|
|
896
|
+
* The topology of a multi-agent run: who spawned whom as a tree, and who
|
|
897
|
+
* messaged or delegated to whom as curved secondary edges over it. Reached
|
|
898
|
+
* through the lazy `AgentGraph` wrapper, which defers `@xyflow/react` (and
|
|
899
|
+
* `elkjs`) to render time.
|
|
900
|
+
*/
|
|
901
|
+
const NODE_TYPES = {
|
|
902
|
+
agent: AgentCard,
|
|
903
|
+
liveAgent: LiveAgentCard,
|
|
904
|
+
agentFrame: AgentFrameBox
|
|
905
|
+
};
|
|
906
|
+
const EDGE_TYPES = { agentMessage: AgentMessageEdgeView };
|
|
907
|
+
/** The fit's margin, in screen pixels. */
|
|
908
|
+
const FIT_PADDING = 16;
|
|
909
|
+
const MINIMAP_MIN_NODES = 40;
|
|
910
|
+
/** The canvas's size, once it has one. */
|
|
911
|
+
function useCanvasSize() {
|
|
912
|
+
const [size, setSize] = useState(null);
|
|
913
|
+
const observer = useRef(null);
|
|
914
|
+
return [size, useCallback((el) => {
|
|
915
|
+
observer.current?.disconnect();
|
|
916
|
+
observer.current = null;
|
|
917
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
918
|
+
const read = () => {
|
|
919
|
+
const r = el.getBoundingClientRect();
|
|
920
|
+
if (r.width === 0 || r.height === 0) return;
|
|
921
|
+
setSize((prev) => prev && Math.abs(prev.width - r.width) < 1 && Math.abs(prev.height - r.height) < 1 ? prev : {
|
|
922
|
+
width: r.width,
|
|
923
|
+
height: r.height
|
|
924
|
+
});
|
|
925
|
+
};
|
|
926
|
+
observer.current = new ResizeObserver(read);
|
|
927
|
+
observer.current.observe(el);
|
|
928
|
+
read();
|
|
929
|
+
}, [])];
|
|
930
|
+
}
|
|
931
|
+
function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB", className, emptyState, live = false, now }) {
|
|
932
|
+
const card = live ? LIVE_AGENT_CARD : AGENT_CARD;
|
|
933
|
+
const wrapperRef = useRef(null);
|
|
934
|
+
const [canvas, measureCanvas] = useCanvasSize();
|
|
935
|
+
const colorMode = useColorMode(wrapperRef, nodes.length > 0);
|
|
936
|
+
const nextTopology = buildAgentTopology(nodes, edges);
|
|
937
|
+
const topologyRef = useRef(nextTopology);
|
|
938
|
+
if (topologyRef.current.key !== nextTopology.key) topologyRef.current = nextTopology;
|
|
939
|
+
const topology = topologyRef.current;
|
|
940
|
+
const [layouts, setLayouts] = useState(null);
|
|
941
|
+
const layoutKey = `${direction}:${live ? "live" : "cards"}:${topology.key}`;
|
|
942
|
+
useEffect(() => {
|
|
943
|
+
let live = true;
|
|
944
|
+
Promise.all(candidateColumns(topology).map((c) => layoutAgents(topology, direction, c, card))).then((all) => {
|
|
945
|
+
if (live) setLayouts({
|
|
946
|
+
key: layoutKey,
|
|
947
|
+
all
|
|
948
|
+
});
|
|
949
|
+
});
|
|
950
|
+
return () => {
|
|
951
|
+
live = false;
|
|
952
|
+
};
|
|
953
|
+
}, [
|
|
954
|
+
topology,
|
|
955
|
+
direction,
|
|
956
|
+
layoutKey,
|
|
957
|
+
card
|
|
958
|
+
]);
|
|
959
|
+
const layout = useMemo(() => layouts && layouts.key === layoutKey && canvas ? pickAgentLayout(layouts.all, canvas, FIT_PADDING) : null, [
|
|
960
|
+
layouts,
|
|
961
|
+
layoutKey,
|
|
962
|
+
canvas
|
|
963
|
+
]);
|
|
964
|
+
const byId = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
965
|
+
const flowNodes = useMemo(() => {
|
|
966
|
+
if (!layout) return [];
|
|
967
|
+
const frames = layout.frames.map((frame) => ({
|
|
968
|
+
id: frame.group.id,
|
|
969
|
+
type: "agentFrame",
|
|
970
|
+
position: {
|
|
971
|
+
x: frame.x,
|
|
972
|
+
y: frame.y
|
|
973
|
+
},
|
|
974
|
+
width: frame.width,
|
|
975
|
+
height: frame.height,
|
|
976
|
+
zIndex: -1,
|
|
977
|
+
selectable: false,
|
|
978
|
+
focusable: false,
|
|
979
|
+
draggable: false,
|
|
980
|
+
data: {
|
|
981
|
+
frame,
|
|
982
|
+
direction,
|
|
983
|
+
statuses: frame.group.memberIds.map((id) => byId.get(id)?.status ?? "unknown")
|
|
984
|
+
}
|
|
985
|
+
}));
|
|
986
|
+
const cards = [];
|
|
987
|
+
for (const id of topology.agentIds) {
|
|
988
|
+
const agent = byId.get(id);
|
|
989
|
+
const position = layout.positions.get(id);
|
|
990
|
+
if (!agent || !position) continue;
|
|
991
|
+
const selected = selectedId === id;
|
|
992
|
+
cards.push({
|
|
993
|
+
id,
|
|
994
|
+
type: live ? "liveAgent" : "agent",
|
|
995
|
+
position,
|
|
996
|
+
width: card.width,
|
|
997
|
+
height: card.height,
|
|
998
|
+
className: AGENT_NODE_CLASS,
|
|
999
|
+
draggable: false,
|
|
1000
|
+
ariaRole: "button",
|
|
1001
|
+
ariaLabel: agentSummary(agent),
|
|
1002
|
+
domAttributes: { "aria-pressed": selected },
|
|
1003
|
+
data: {
|
|
1004
|
+
agent,
|
|
1005
|
+
selected,
|
|
1006
|
+
direction,
|
|
1007
|
+
now
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return [...frames, ...cards];
|
|
1012
|
+
}, [
|
|
1013
|
+
layout,
|
|
1014
|
+
topology,
|
|
1015
|
+
byId,
|
|
1016
|
+
selectedId,
|
|
1017
|
+
direction,
|
|
1018
|
+
live,
|
|
1019
|
+
card,
|
|
1020
|
+
now
|
|
1021
|
+
]);
|
|
1022
|
+
const flowEdges = useMemo(() => {
|
|
1023
|
+
if (!layout) return [];
|
|
1024
|
+
const out = [];
|
|
1025
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1026
|
+
for (const s of topology.spawns) {
|
|
1027
|
+
const target = topology.groupOf.get(s.to) ?? s.to;
|
|
1028
|
+
const id = `spawn:${s.from}:${target}`;
|
|
1029
|
+
if (seen.has(id)) continue;
|
|
1030
|
+
seen.add(id);
|
|
1031
|
+
const group = topology.groups.find((g) => g.id === target);
|
|
1032
|
+
const statuses = group ? group.memberIds.map((m) => byId.get(m)?.status) : [byId.get(s.to)?.status];
|
|
1033
|
+
const flow = [
|
|
1034
|
+
"failed",
|
|
1035
|
+
"waiting",
|
|
1036
|
+
"running"
|
|
1037
|
+
].find((st) => statuses.includes(st)) ?? agentStatus(statuses[0] ?? "unknown").flow;
|
|
1038
|
+
out.push({
|
|
1039
|
+
id,
|
|
1040
|
+
source: s.from,
|
|
1041
|
+
target,
|
|
1042
|
+
type: "smoothstep",
|
|
1043
|
+
pathOptions: { borderRadius: 14 },
|
|
1044
|
+
selectable: false,
|
|
1045
|
+
focusable: false,
|
|
1046
|
+
style: {
|
|
1047
|
+
stroke: edgeColor(flow),
|
|
1048
|
+
strokeWidth: 1.5
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
const ids = new Set(topology.agentIds);
|
|
1053
|
+
const messages = /* @__PURE__ */ new Map();
|
|
1054
|
+
for (const e of edges) {
|
|
1055
|
+
if (e.kind === "spawn" || e.from === e.to || !ids.has(e.from) || !ids.has(e.to)) continue;
|
|
1056
|
+
const id = `${e.kind}:${e.from}:${e.to}`;
|
|
1057
|
+
const prior = messages.get(id);
|
|
1058
|
+
const count = (prior?.data?.count ?? 0) + Math.max(1, e.count ?? 1);
|
|
1059
|
+
const color = MESSAGE_EDGE_COLOR[e.kind] ?? MESSAGE_EDGE_COLOR.message;
|
|
1060
|
+
messages.set(id, {
|
|
1061
|
+
id,
|
|
1062
|
+
source: e.from,
|
|
1063
|
+
target: e.to,
|
|
1064
|
+
type: "agentMessage",
|
|
1065
|
+
selectable: false,
|
|
1066
|
+
focusable: false,
|
|
1067
|
+
zIndex: 0,
|
|
1068
|
+
markerEnd: {
|
|
1069
|
+
type: MarkerType.ArrowClosed,
|
|
1070
|
+
width: 12,
|
|
1071
|
+
height: 12,
|
|
1072
|
+
color
|
|
1073
|
+
},
|
|
1074
|
+
ariaLabel: `${e.kind === "delegation" ? "Delegation" : "Message"} from ${byId.get(e.from)?.label ?? e.from} to ${byId.get(e.to)?.label ?? e.to}${count > 1 ? `, ${count} times` : ""}`,
|
|
1075
|
+
data: {
|
|
1076
|
+
kind: e.kind,
|
|
1077
|
+
count,
|
|
1078
|
+
label: e.label ?? prior?.data?.label
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
return [...out, ...messages.values()];
|
|
1083
|
+
}, [
|
|
1084
|
+
layout,
|
|
1085
|
+
topology,
|
|
1086
|
+
edges,
|
|
1087
|
+
byId
|
|
1088
|
+
]);
|
|
1089
|
+
const rfRef = useRef(null);
|
|
1090
|
+
const [ready, setReady] = useState(false);
|
|
1091
|
+
const [shown, setShown] = useState(false);
|
|
1092
|
+
const readerMoved = useRef(false);
|
|
1093
|
+
const framed = useRef(null);
|
|
1094
|
+
const layoutIdentity = layout ? `${layoutKey}:${layout.columns}` : null;
|
|
1095
|
+
useEffect(() => {
|
|
1096
|
+
const inst = rfRef.current;
|
|
1097
|
+
if (!ready || !inst || !layoutIdentity || !canvas) return;
|
|
1098
|
+
const relayout = framed.current !== layoutIdentity;
|
|
1099
|
+
if (!relayout && readerMoved.current) return;
|
|
1100
|
+
const animate = framed.current !== null && relayout && motionAllowed();
|
|
1101
|
+
framed.current = layoutIdentity;
|
|
1102
|
+
readerMoved.current = false;
|
|
1103
|
+
const frame = requestAnimationFrame(() => {
|
|
1104
|
+
inst.fitView({
|
|
1105
|
+
padding: `${FIT_PADDING}px`,
|
|
1106
|
+
maxZoom: 1,
|
|
1107
|
+
minZoom: .2,
|
|
1108
|
+
duration: animate ? 220 : 0
|
|
1109
|
+
});
|
|
1110
|
+
setShown(true);
|
|
1111
|
+
});
|
|
1112
|
+
return () => cancelAnimationFrame(frame);
|
|
1113
|
+
}, [
|
|
1114
|
+
ready,
|
|
1115
|
+
layoutIdentity,
|
|
1116
|
+
canvas
|
|
1117
|
+
]);
|
|
1118
|
+
const select = useCallback((id) => {
|
|
1119
|
+
if (byId.has(id)) onSelect?.(id);
|
|
1120
|
+
}, [byId, onSelect]);
|
|
1121
|
+
const handleNodeClick = useCallback((_event, node) => select(node.id), [select]);
|
|
1122
|
+
const handleKeyDown = useCallback((event) => {
|
|
1123
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1124
|
+
if (!(event.target instanceof HTMLElement)) return;
|
|
1125
|
+
if (!event.target.classList.contains("react-flow__node")) return;
|
|
1126
|
+
const id = event.target.getAttribute("data-id");
|
|
1127
|
+
if (!id || !byId.has(id)) return;
|
|
1128
|
+
event.preventDefault();
|
|
1129
|
+
event.stopPropagation();
|
|
1130
|
+
select(id);
|
|
1131
|
+
}, [byId, select]);
|
|
1132
|
+
if (nodes.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1133
|
+
className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-muted-foreground text-xs ${className ?? ""}`,
|
|
1134
|
+
children: emptyState ?? "No agents yet"
|
|
1135
|
+
});
|
|
1136
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1137
|
+
ref: (el) => {
|
|
1138
|
+
wrapperRef.current = el;
|
|
1139
|
+
measureCanvas(el);
|
|
1140
|
+
},
|
|
1141
|
+
className: `agent-graph${live ? " ag-live" : ""} relative transition-opacity duration-150 ease-out motion-reduce:transition-none ${layout && shown ? "opacity-100" : "opacity-0"} ${className ?? ""}`,
|
|
1142
|
+
"data-layout": layout && shown ? "ready" : "pending",
|
|
1143
|
+
"data-columns": layout?.columns,
|
|
1144
|
+
children: [live && /* @__PURE__ */ jsx("style", { children: "\n.ag-live { --ag-fg: hsl(var(--foreground)); --ag-muted: hsl(var(--muted-foreground)); --ag-surface: hsl(var(--card)); --ag-border: hsl(var(--border)); }\n.ag-live .ag-live-card { position: relative; display: flex; flex-direction: column; gap: 8px; width: 100%; height: 100%; padding: 12px; border: 1px solid var(--ag-border);\n border-radius: 12px; background: var(--ag-surface); color: var(--ag-fg); cursor: pointer; overflow: hidden; animation: ag-live-grow 360ms cubic-bezier(.2,.8,.2,1) both; transform-origin: 50% 0; }\n.ag-live .ag-live-head { display: flex; align-items: center; gap: 8px; min-width: 0; }\n.ag-live .ag-live-names { display: flex; flex-direction: column; min-width: 0; flex: 1; }\n.ag-live .ag-live-name { font-size: 13px; line-height: 16px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.ag-live .ag-live-meta, .ag-live .ag-live-stats { font-size: 11px; line-height: 16px; color: var(--ag-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.ag-live .ag-live-stats { display: flex; flex-direction: column; align-items: flex-end; font-variant-numeric: tabular-nums; flex: none; }\n.ag-live .ag-live-body { display: flex; gap: 8px; min-height: 0; flex: 1; }\n.ag-live .ag-live-figure { flex: none; width: 60px; height: 48px; overflow: hidden; border-radius: 8px; background: color-mix(in oklab, var(--ag-fg) 5%, transparent); }\n.ag-live .ag-live-figure pre { margin: 0; padding: 0 4px; font: 11px/16px var(--font-mono, ui-monospace, monospace); color: var(--ag-fg); white-space: pre;\n animation: ag-live-frames calc(var(--ag-frames) * 280ms) steps(var(--ag-frames)) infinite; }\n.ag-live .ag-live-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }\n.ag-live .ag-live-thinking { margin: 0; font-size: 12px; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; animation: ag-live-in 220ms ease-out both; }\n.ag-live .ag-live-quiet { color: var(--ag-muted); }\n.ag-live .ag-live-events { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 0; }\n.ag-live .ag-live-events li { font: 11px/16px var(--font-mono, ui-monospace, monospace); color: var(--ag-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; animation: ag-live-in 220ms ease-out both; }\n.ag-live .ag-live-events li:last-child { color: var(--ag-fg); }\n.ag-live .ag-live-slot { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }\n.ag-live .ag-live-chip { display: inline-flex; align-items: center; gap: 8px; max-width: 100%; height: 32px; padding: 0 12px; border: 1px solid var(--ag-border); border-radius: 16px;\n background: var(--ag-surface); color: var(--ag-muted); font-size: 12px; opacity: .72; cursor: pointer; animation: ag-live-settle 320ms cubic-bezier(.2,.8,.2,1) both; }\n.ag-live .ag-live-chip-name { color: var(--ag-fg); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.ag-live .ag-live-chip-result { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n@keyframes ag-live-frames { to { transform: translateY(calc(var(--ag-frames) * -48px)); } }\n@keyframes ag-live-grow { from { opacity: 0; transform: scale(.7) translateY(-12px); } }\n@keyframes ag-live-in { from { opacity: 0; transform: translateY(4px); } }\n@keyframes ag-live-settle { from { opacity: 1; transform: scale(1.12); } }\n@media (prefers-reduced-motion: reduce) { .ag-live * { animation: none !important; } }\n" }), /* @__PURE__ */ jsxs(ReactFlow, {
|
|
1145
|
+
nodes: flowNodes,
|
|
1146
|
+
edges: flowEdges,
|
|
1147
|
+
nodeTypes: NODE_TYPES,
|
|
1148
|
+
edgeTypes: EDGE_TYPES,
|
|
1149
|
+
colorMode,
|
|
1150
|
+
onInit: (inst) => {
|
|
1151
|
+
rfRef.current = inst;
|
|
1152
|
+
setReady(true);
|
|
1153
|
+
},
|
|
1154
|
+
onMoveStart: (event) => {
|
|
1155
|
+
if (event) readerMoved.current = true;
|
|
1156
|
+
},
|
|
1157
|
+
onNodeClick: handleNodeClick,
|
|
1158
|
+
onKeyDown: handleKeyDown,
|
|
1159
|
+
proOptions: { hideAttribution: true },
|
|
1160
|
+
nodesDraggable: false,
|
|
1161
|
+
nodesConnectable: false,
|
|
1162
|
+
elementsSelectable: false,
|
|
1163
|
+
nodesFocusable: true,
|
|
1164
|
+
edgesFocusable: false,
|
|
1165
|
+
deleteKeyCode: null,
|
|
1166
|
+
panOnScroll: true,
|
|
1167
|
+
zoomOnScroll: false,
|
|
1168
|
+
zoomOnPinch: true,
|
|
1169
|
+
zoomOnDoubleClick: false,
|
|
1170
|
+
panOnDrag: true,
|
|
1171
|
+
minZoom: .2,
|
|
1172
|
+
maxZoom: 1.75,
|
|
1173
|
+
"aria-label": "Agent topology",
|
|
1174
|
+
children: [
|
|
1175
|
+
/* @__PURE__ */ jsx(Background, {
|
|
1176
|
+
gap: 18,
|
|
1177
|
+
size: 1.4,
|
|
1178
|
+
color: "hsl(var(--muted-foreground) / 0.38)"
|
|
1179
|
+
}),
|
|
1180
|
+
nodes.length > MINIMAP_MIN_NODES && /* @__PURE__ */ jsx(MiniMap, {
|
|
1181
|
+
pannable: true,
|
|
1182
|
+
zoomable: true,
|
|
1183
|
+
position: "bottom-left",
|
|
1184
|
+
style: {
|
|
1185
|
+
width: 168,
|
|
1186
|
+
height: 104
|
|
1187
|
+
},
|
|
1188
|
+
className: "!hidden sm:!block",
|
|
1189
|
+
ariaLabel: "Agent topology overview"
|
|
1190
|
+
}),
|
|
1191
|
+
/* @__PURE__ */ jsx(Controls, {
|
|
1192
|
+
showInteractive: false,
|
|
1193
|
+
position: "bottom-right",
|
|
1194
|
+
fitViewOptions: {
|
|
1195
|
+
padding: `${FIT_PADDING}px`,
|
|
1196
|
+
maxZoom: 1
|
|
1197
|
+
}
|
|
1198
|
+
})
|
|
1199
|
+
]
|
|
1200
|
+
})]
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
export { AgentGraphCanvas };
|