@tangle-network/charts 2.0.13 → 2.0.14

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.
@@ -0,0 +1,1191 @@
1
+ import { D as statusMeta, E as runStatus, O as createLucideIcon, T as RunStatusIcon } from "../../flow-waterfall-model-DvrgPLGS.js";
2
+ import { a as LIVE_AGENT_CARD, c as AgentMark, i as GRID_MIN_LEAVES, l as GraphMarksProvider, n as AGENT_CARD, o as buildAgentTopology, r as FRAME_CAPTION, s as layoutAgents, t as LatestEvents, u as ToolMark } from "../../latest-events-iiWwpIck.js";
3
+ import { C as fmtCost, D as fmtTokens, E as fmtTokenTotal, S as clampPreview, T as fmtEventTime, _ as syncAmbient, a as motionAllowed, b as useMotionAllowed, c as humanizeIdentifier, d as loadElk, f as GraphErrorBoundary, g as ambientStyle, h as agentQuietActivity, i as SELECTION_OUTLINE, l as parseActionPath, m as activityTiming, n as RESTING_SHADOW, o as useColorMode, p as retryImport, r as edgeColor, s as actionPathLabel, t as RESTING_BORDER, u as shortModel, v as tokenMilliseconds, w as fmtDuration, x as useRunNow, y as useAttention } from "../../graph-style-fealnLLn.js";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Suspense, lazy, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ //#region node_modules/lucide-react/dist/esm/icons/user.mjs
7
+ /**
8
+ * @license lucide-react v1.63.0 - ISC
9
+ *
10
+ * This source code is licensed under the ISC license.
11
+ * See the LICENSE file in the root directory of this source tree.
12
+ */
13
+ const __iconData$1 = {
14
+ name: "user",
15
+ size: 24,
16
+ node: [["path", {
17
+ d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",
18
+ key: "975kel"
19
+ }], ["circle", {
20
+ cx: "12",
21
+ cy: "7",
22
+ r: "4",
23
+ key: "17ys0d"
24
+ }]]
25
+ };
26
+ __iconData$1.node;
27
+ const User = createLucideIcon(__iconData$1);
28
+ //#endregion
29
+ //#region node_modules/lucide-react/dist/esm/icons/x.mjs
30
+ /**
31
+ * @license lucide-react v1.63.0 - ISC
32
+ *
33
+ * This source code is licensed under the ISC license.
34
+ * See the LICENSE file in the root directory of this source tree.
35
+ */
36
+ const __iconData = {
37
+ name: "x",
38
+ size: 24,
39
+ node: [["path", {
40
+ d: "M18 6 6 18",
41
+ key: "1bl5f8"
42
+ }], ["path", {
43
+ d: "m6 6 12 12",
44
+ key: "d8bk6v"
45
+ }]]
46
+ };
47
+ __iconData.node;
48
+ const X = createLucideIcon(__iconData);
49
+ //#endregion
50
+ //#region src/react/agent-graph/agent-hub-css.ts
51
+ /** Hub styles consume Brand's status, elevation, radius and motion roles. */
52
+ const AGENT_HUB_CSS = `
53
+ .ag-root { --ag-surface: var(--bg-card); --ag-fg: var(--text-primary); --ag-muted: var(--text-muted); --ag-border: var(--border-default); --ag-accent: var(--brand-primary);
54
+ color: var(--ag-fg); font-size: 12px; line-height: 16px; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
55
+ .ag-root * { box-sizing: border-box; }
56
+ .ag-head, .ag-title, .ag-chip, .ag-legend li, .ag-pop-head, .ag-pop-foot { display: flex; align-items: center; gap: 8px; }
57
+ .ag-head { flex-wrap: wrap; flex-shrink: 0; min-height: 40px; }
58
+ .ag-title, .ag-pop-name { font-weight: 600; font-size: 14px; line-height: 20px; }
59
+ .ag-title-mark { width: 6px; height: 6px; border-radius: var(--radius-pill); background: var(--ag-accent); }
60
+ .ag-chip, .ag-pill { display: inline-flex; align-items: center; gap: 4px; padding: 0 8px; border: 1px solid var(--ag-border); border-radius: var(--radius-pill); font-size: 12px; font-weight: 500; min-height: 24px; background: transparent; color: var(--ag-muted); }
61
+ .ag-chip { cursor: pointer; }
62
+ .ag-chip-needs-you, .ag-pill-needs-you { color: var(--surface-warning-text); }
63
+ .ag-chip-failed, .ag-pill-failed { color: var(--surface-danger-text); }
64
+ .ag-pill-succeeded { color: var(--surface-success-text); }
65
+ .ag-stats { margin-left: auto; display: flex; gap: 12px; flex-wrap: wrap; color: var(--ag-muted); font-variant-numeric: tabular-nums; }
66
+ .ag-stats b, .ag-pop-call b, .ag-pop-foot b { color: var(--ag-fg); font-weight: 500; }
67
+ .ag-canvas-wrap { position: relative; min-height: 0; border: 1px solid var(--ag-border); border-radius: var(--radius-container); background: var(--bg-root); overflow: auto; }
68
+ .ag-canvas { position: absolute; inset: 0 auto auto 0; transform-origin: 0 0; }
69
+ .ag-edges { position: absolute; inset: 0; overflow: visible; }
70
+ .ag-edge { fill: none; stroke: color-mix(in srgb, var(--ag-muted) 80%, transparent); stroke-width: 1.25; }
71
+ .ag-edge-live, .ag-flow { fill: none; stroke-linecap: round; }
72
+ .ag-edge-live { stroke-width: 1.25; }
73
+ .ag-flow { stroke-width: 2; }
74
+ .ag-root g, .ag-node { transition: opacity var(--motion-control); }
75
+ .ag-dimmed .ag-tile { opacity: .45; } .ag-edges .ag-dimmed { opacity: .32; }
76
+ .ag-node { position: absolute; display: flex; flex-direction: column; align-items: center; width: 176px; margin-left: -88px; margin-top: calc(var(--ag-tile) / -2); padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; outline: none; }
77
+ .ag-tile { position: relative; display: grid; place-items: center; width: var(--ag-tile); height: var(--ag-tile); flex-shrink: 0; border-radius: var(--radius-card); background: var(--ag-surface); border: 1px solid var(--ag-border); box-shadow: var(--shadow-card); transition: box-shadow var(--motion-control); }
78
+ .ag-node:hover .ag-tile { box-shadow: var(--shadow-dropdown); }
79
+ .ag-model-mark { display: grid; place-items: center; width: 20px; height: 20px; filter: grayscale(1) var(--tw-invert,); color: var(--ag-muted); } .ag-model-mark img, .ag-model-mark svg { max-width: 20px; max-height: 20px; } .ag-node:hover .ag-model-mark { filter: var(--tw-invert, none); } .ag-node:not(:hover) .ag-model-mark svg path { fill: currentColor; }
80
+ .ag-node:focus-visible, .ag-selected .ag-tile, .ag-close:focus-visible, .ag-chip:focus-visible { outline: 2px solid color-mix(in srgb, var(--ag-fg) 70%, transparent); outline-offset: 2px; }
81
+ .ag-node[style*="--ag-tone"] .ag-tile { border: 1.5px solid var(--ag-tone); }
82
+ .ag-node-you .ag-tile { background: var(--ag-fg); color: var(--ag-surface); }
83
+ .ag-name, .ag-sub { max-width: 176px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
84
+ .ag-name { margin-top: 6px; font-size: 14px; font-weight: 600; line-height: 20px; }
85
+ .ag-sub { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; white-space: normal; }
86
+ .ag-node-tool .ag-name { max-width: 132px; }
87
+ .ag-sub, .ag-node-tool .ag-name { font-size: 12px; font-weight: 400; color: var(--ag-muted); } .ag-canvas .ag-sub { font-size: 13px; line-height: 17px; }
88
+ .ag-badge { position: absolute; top: -5px; right: -5px; display: grid; place-items: center; background: var(--ag-surface); border-radius: var(--radius-pill); box-shadow: 0 0 0 2px var(--ag-surface); }
89
+ .ag-root [data-activity="quiet"] .ag-running-dot { animation: none !important; }
90
+ .ag-running-dot { display: inline-block; flex-shrink: 0; width: 6px; height: 6px; border-radius: var(--radius-pill); background: var(--surface-info-text); animation: tangle-breathe var(--motion-ambient) infinite; }
91
+ .ag-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; color: var(--ag-muted); }
92
+ .ag-legend-line { width: 14px; height: 2px; border-radius: var(--radius-pill); }
93
+ .ag-popover { overflow: auto; position: absolute; z-index: 5; width: min(280px, calc(100% - 16px)); padding: 12px; border-radius: var(--radius-card); background: var(--bg-elevated); color: var(--ag-fg); border: 1px solid var(--ag-border); box-shadow: var(--shadow-overlay); animation: tangle-intro-enter var(--motion-arrive); }
94
+ .ag-tile-mini { border-radius: var(--radius-control); box-shadow: none; }
95
+ .ag-tile-micro { border: 0; border-radius: var(--radius-tag); background: none; box-shadow: none; }
96
+ .ag-pop-title { display: flex; flex-direction: column; min-width: 0; flex: 1; }
97
+ .ag-pop-sub, .ag-pop-call { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ag-muted); }
98
+ .ag-close { display: grid; place-items: center; width: 28px; height: 28px; border: 0; border-radius: var(--radius-control); background: none; color: var(--ag-muted); cursor: pointer; }
99
+ .ag-close:hover { background: var(--bg-hover); color: var(--ag-fg); }
100
+ .ag-pop-summary { overflow-wrap: anywhere; margin: 8px 0 0; font-size: 14px; line-height: 20px; }
101
+ .ag-pop-calls { list-style: none; margin: 8px 0 0; padding: 8px 0 0; border-top: 1px solid var(--ag-border); display: grid; gap: 6px; }
102
+ .ag-pop-calls li { display: flex; align-items: center; gap: 6px; min-width: 0; }
103
+ .ag-pop-dir, .ag-pop-time { color: var(--ag-muted); flex-shrink: 0; }
104
+ .ag-pop-call { flex: 1; min-width: 0; }
105
+ .ag-pop-time { font-variant-numeric: tabular-nums; display: inline-flex; }
106
+ .ag-pop-foot { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--ag-border); color: var(--ag-muted); }
107
+ .ag-mobile { list-style: none; margin: 0; padding: 8px; display: grid; gap: 12px; }
108
+ .ag-mobile .ag-node { position: relative; left: auto !important; top: auto !important; margin: 0; width: 100%; min-height: 48px; display: grid; grid-template-columns: 32px minmax(0,1fr); text-align: left; column-gap: 12px; }
109
+ .ag-mobile .ag-tile { grid-row: span 2; }
110
+ .ag-mobile .ag-name, .ag-mobile .ag-sub { max-width: 100%; width: 100%; margin: 0; }
111
+ .ag-mobile .ag-sub { display: block; white-space: nowrap; }
112
+ .ag-tools { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0 44px; }
113
+ .ag-tools button { color: var(--ag-muted); background: transparent; border: 1px solid var(--ag-border); border-radius: var(--radius-pill); min-height: 28px; padding: 0 8px; font: inherit; cursor: pointer; }
114
+ .ag-available { padding: 8px; border-top: 1px solid var(--ag-border); color: var(--ag-muted); } .ag-available .ag-tools { margin: 8px 0 0; }
115
+ .ag-list-mode .ag-popover { overflow: auto; position: relative; left: auto !important; top: auto !important; width: auto; margin: 8px; }
116
+ @media (prefers-reduced-motion: reduce) { .ag-root *, .ag-root *::before { animation: none !important; transition: opacity var(--duration-fast) !important; } .ag-root .ag-running-dot { animation: tangle-breathe var(--motion-ambient) infinite !important; } }
117
+ `;
118
+ //#endregion
119
+ //#region src/react/agent-graph/agent-hub-layout.ts
120
+ /** The id the "You" node takes in `calls`. */
121
+ const YOU_ID = "you";
122
+ const HUB_METRICS = {
123
+ cellX: 184,
124
+ cellY: 140,
125
+ tile: 48,
126
+ hubTile: 60,
127
+ label: 34
128
+ };
129
+ const key = (col, row) => `${col},${row}`;
130
+ /** Ring cells around the hub, nearest first: the diagonals, then the sides, then ring two by angle. */
131
+ function ringCells(count) {
132
+ const cells = [
133
+ [-1, -1],
134
+ [1, -1],
135
+ [-1, 1],
136
+ [1, 1],
137
+ [-1, 0],
138
+ [1, 0],
139
+ [0, 1],
140
+ [0, -1]
141
+ ];
142
+ for (let ring = 2; cells.length < count; ring++) {
143
+ const outer = [];
144
+ for (let c = -ring; c <= ring; c++) for (let r = -ring; r <= ring; r++) if (Math.max(Math.abs(c), Math.abs(r)) === ring) outer.push([c, r]);
145
+ outer.sort((a, b) => Math.atan2(a[1], a[0]) - Math.atan2(b[1], b[0]));
146
+ cells.push(...outer);
147
+ }
148
+ return cells.slice(0, count);
149
+ }
150
+ /** The orchestrator: the agent nobody spawned, else the first. */
151
+ function hubAgentId(nodes, edges) {
152
+ const spawned = new Set(edges.filter((e) => e.kind === "spawn").map((e) => e.to));
153
+ return (nodes.find((n) => !spawned.has(n.id)) ?? nodes[0])?.id;
154
+ }
155
+ /**
156
+ * Place every node on the grid. Agents take ring cells in order; each tool takes
157
+ * the first free cell outward from the first agent that called it (beside, then
158
+ * above or below, then diagonal), so it sits next to its user.
159
+ */
160
+ function layoutHub(input) {
161
+ const { nodes, edges, tools, calls, you } = input;
162
+ const hub = hubAgentId(nodes, edges);
163
+ const placements = [];
164
+ const taken = /* @__PURE__ */ new Set();
165
+ const place = (id, kind, col, row) => {
166
+ placements.push({
167
+ id,
168
+ kind,
169
+ col,
170
+ row
171
+ });
172
+ taken.add(key(col, row));
173
+ };
174
+ if (hub) place(hub, "hub", 0, 0);
175
+ const agents = nodes.filter((n) => n.id !== hub);
176
+ ringCells(agents.length).forEach(([c, r], i) => place(agents[i].id, "agent", c, r));
177
+ const ring = Math.max(1, ...placements.map((p) => Math.max(Math.abs(p.col), Math.abs(p.row))));
178
+ if (you) {
179
+ const col = -Math.min(ring, 1);
180
+ let row = -ring - 1;
181
+ while (taken.has(key(col, row))) row--;
182
+ place("you", "you", col, row);
183
+ }
184
+ const cell = new Map(placements.map((p) => [p.id, p]));
185
+ const userOf = /* @__PURE__ */ new Map();
186
+ for (const call of calls) {
187
+ const toolId = tools.some((t) => t.id === call.to) ? call.to : tools.some((t) => t.id === call.from) ? call.from : void 0;
188
+ const agentId = toolId === call.to ? call.from : call.to;
189
+ if (toolId && cell.has(agentId) && !userOf.has(toolId)) userOf.set(toolId, agentId);
190
+ }
191
+ for (const tool of tools) {
192
+ const owner = cell.get(userOf.get(tool.id) ?? hub ?? "");
193
+ const ac = owner?.col ?? 0, ar = owner?.row ?? 0;
194
+ const sx = Math.sign(ac) || 1, sy = Math.sign(ar) || 1;
195
+ const options = ac === 0 ? [
196
+ [0, sy],
197
+ [sx, sy],
198
+ [-sx, sy],
199
+ [sx, 0]
200
+ ] : ar === 0 ? [
201
+ [sx, 0],
202
+ [sx, -1],
203
+ [sx, 1],
204
+ [0, -1]
205
+ ] : [
206
+ [sx, 0],
207
+ [0, sy],
208
+ [sx, sy],
209
+ [-sx, sy],
210
+ [sx, -sy]
211
+ ];
212
+ let spot;
213
+ for (let step = 1; !spot && step < 6; step++) for (const [dx, dy] of options) {
214
+ const c = ac + dx * step, r = ar + dy * step;
215
+ if (!taken.has(key(c, r))) {
216
+ spot = [c, r];
217
+ break;
218
+ }
219
+ }
220
+ place(tool.id, "tool", spot[0], spot[1]);
221
+ cell.set(tool.id, placements.at(-1));
222
+ }
223
+ const links = [];
224
+ const seen = /* @__PURE__ */ new Set();
225
+ const link = (from, to) => {
226
+ const k = [from, to].sort().join("|");
227
+ if (from === to || seen.has(k) || !taken.size) return;
228
+ seen.add(k);
229
+ links.push({
230
+ from,
231
+ to
232
+ });
233
+ };
234
+ if (you && hub) link("you", hub);
235
+ for (const agent of agents) {
236
+ const parent = edges.find((e) => e.kind === "spawn" && e.to === agent.id)?.from;
237
+ link(parent && cell.has(parent) ? parent : hub, agent.id);
238
+ }
239
+ for (const call of calls) if (cell.has(call.from) && cell.has(call.to)) link(call.from, call.to);
240
+ return {
241
+ placements,
242
+ links,
243
+ minCol: Math.min(...placements.map((p) => p.col)),
244
+ maxCol: Math.max(...placements.map((p) => p.col)),
245
+ minRow: Math.min(...placements.map((p) => p.row)),
246
+ maxRow: Math.max(...placements.map((p) => p.row))
247
+ };
248
+ }
249
+ /**
250
+ * A short rounded Manhattan route around tile and label bounds. Departure lanes
251
+ * separate parallel calls; the path length keeps finite trips at a consistent speed.
252
+ */
253
+ function orthogonalPath(a, b, verticalFirst, lane = 0, radius = 10, obstacles = []) {
254
+ const clear = (p, q) => obstacles.every((rect) => p.x === q.x ? p.x <= rect.left || p.x >= rect.right || Math.max(p.y, q.y) <= rect.top || Math.min(p.y, q.y) >= rect.bottom : p.y <= rect.top || p.y >= rect.bottom || Math.max(p.x, q.x) <= rect.left || Math.min(p.x, q.x) >= rect.right);
255
+ const xs = [.../* @__PURE__ */ new Set([
256
+ a.x + lane,
257
+ (a.x + b.x) / 2,
258
+ ...obstacles.flatMap((rect) => [rect.left - 8, rect.right + 8])
259
+ ])];
260
+ const ys = [.../* @__PURE__ */ new Set([
261
+ a.y + lane,
262
+ (a.y + b.y) / 2,
263
+ ...obstacles.flatMap((rect) => [rect.top - 8, rect.bottom + 8])
264
+ ])];
265
+ const candidates = [[
266
+ a,
267
+ {
268
+ x: a.x,
269
+ y: b.y
270
+ },
271
+ b
272
+ ], [
273
+ a,
274
+ {
275
+ x: b.x,
276
+ y: a.y
277
+ },
278
+ b
279
+ ]];
280
+ for (const x of xs) candidates.push([
281
+ a,
282
+ {
283
+ x,
284
+ y: a.y
285
+ },
286
+ {
287
+ x,
288
+ y: b.y
289
+ },
290
+ b
291
+ ]);
292
+ for (const y of ys) candidates.push([
293
+ a,
294
+ {
295
+ x: a.x,
296
+ y
297
+ },
298
+ {
299
+ x: b.x,
300
+ y
301
+ },
302
+ b
303
+ ]);
304
+ for (const x of xs) for (const y of ys) candidates.push([
305
+ a,
306
+ {
307
+ x: a.x,
308
+ y
309
+ },
310
+ {
311
+ x,
312
+ y
313
+ },
314
+ {
315
+ x,
316
+ y: b.y
317
+ },
318
+ b
319
+ ], [
320
+ a,
321
+ {
322
+ x,
323
+ y: a.y
324
+ },
325
+ {
326
+ x,
327
+ y
328
+ },
329
+ {
330
+ x: b.x,
331
+ y
332
+ },
333
+ b
334
+ ]);
335
+ let points = candidates[verticalFirst ? 0 : 1], best = Infinity;
336
+ for (const candidate of candidates) {
337
+ const path = candidate.filter((p, i) => !i || p.x !== candidate[i - 1].x || p.y !== candidate[i - 1].y);
338
+ if (!path.slice(1).every((point, i) => clear(path[i], point))) continue;
339
+ const distance = path.slice(1).reduce((sum, point, i) => sum + Math.abs(point.x - path[i].x) + Math.abs(point.y - path[i].y), 0);
340
+ const trunk = path.find((point, i) => i && point.y !== path[i - 1].y)?.x ?? a.x;
341
+ const score = distance + path.length + (lane ? Math.abs(trunk - a.x - lane) : 0);
342
+ if (score < best) {
343
+ best = score;
344
+ points = path;
345
+ }
346
+ }
347
+ points = points.filter((point, i) => !i || i === points.length - 1 || !(points[i - 1].x === point.x && point.x === points[i + 1].x || points[i - 1].y === point.y && point.y === points[i + 1].y));
348
+ let d = `M${a.x},${a.y}`, length = 0;
349
+ for (let i = 1; i < points.length; i++) {
350
+ const previous = points[i - 1], point = points[i], next = points[i + 1];
351
+ const incoming = Math.hypot(point.x - previous.x, point.y - previous.y);
352
+ length += incoming;
353
+ if (!next) {
354
+ d += ` L${point.x},${point.y}`;
355
+ continue;
356
+ }
357
+ const outgoing = Math.hypot(next.x - point.x, next.y - point.y), r = Math.min(radius, incoming / 2, outgoing / 2);
358
+ d += ` L${point.x - Math.sign(point.x - previous.x) * r},${point.y - Math.sign(point.y - previous.y) * r} Q${point.x},${point.y} ${point.x + Math.sign(next.x - point.x) * r},${point.y + Math.sign(next.y - point.y) * r}`;
359
+ length -= 2 * r - Math.PI * r / 2;
360
+ }
361
+ return {
362
+ d,
363
+ length
364
+ };
365
+ }
366
+ //#endregion
367
+ //#region src/react/agent-graph/agent-hub-parts.tsx
368
+ /** Hub nodes and call details share the run status vocabulary. */
369
+ /** One colour per call kind, shared by edges, the legend and popover rows. */
370
+ const CALL_COLOR = {
371
+ call: "var(--brand-primary)",
372
+ answer: `var(${statusMeta("done").token})`,
373
+ "needs-you": `var(${statusMeta("needs-you").token})`,
374
+ failed: `var(${statusMeta("failed").token})`
375
+ };
376
+ const CALL_LABEL = {
377
+ call: "Call",
378
+ answer: "Answer",
379
+ "needs-you": statusMeta("needs-you").label,
380
+ failed: statusMeta("failed").label
381
+ };
382
+ function NodeIcon({ entity, size }) {
383
+ if (entity.kind === "you") return /* @__PURE__ */ jsx(User, {
384
+ size: size * .42,
385
+ strokeWidth: 2.2,
386
+ "aria-hidden": true
387
+ });
388
+ if (entity.kind === "tool") return /* @__PURE__ */ jsx(ToolMark, {
389
+ tool: entity.tool,
390
+ label: entity.label,
391
+ size
392
+ });
393
+ return /* @__PURE__ */ jsx(AgentMark, {
394
+ agent: entity.agent,
395
+ layout: "hub",
396
+ size
397
+ });
398
+ }
399
+ /** Running has one quiet opacity signal; all other states use the shared glyph. */
400
+ function StatusBadge({ status, quiet }) {
401
+ if (!status) return null;
402
+ return /* @__PURE__ */ jsx("span", {
403
+ className: "ag-badge",
404
+ children: runStatus(status) === "running" ? /* @__PURE__ */ jsx("span", {
405
+ className: "ag-running-dot",
406
+ "data-motion": "essential",
407
+ "aria-label": "Running",
408
+ style: quiet ? {
409
+ animation: "none",
410
+ background: "var(--ag-muted)"
411
+ } : ambientStyle,
412
+ onAnimationStart: syncAmbient
413
+ }) : /* @__PURE__ */ jsx(RunStatusIcon, { status })
414
+ });
415
+ }
416
+ function HubNode({ entity, x, y, tile, dimmed, selected, buttonRef, onOpen, onHover, onKeyDown }) {
417
+ const attentionRef = useRef(null);
418
+ useAttention(attentionRef, entity.status ?? "unknown");
419
+ const tone = entity.status === "needs-you" || entity.status === "failed" ? `var(${statusMeta(entity.status).token})` : void 0;
420
+ const sub = entity.quiet ?? (entity.status === "needs-you" ? "Needs you · Respond" : entity.status === "waiting" ? entity.agent?.turn || "Waiting on peer" : entity.status === "failed" ? entity.agent?.result ?? entity.agent?.turn ?? "Failure cause not recorded" : entity.sub ? `${statusMeta(entity.status ?? "unknown").label} · ${entity.sub}` : entity.status ? statusMeta(entity.status).label : void 0);
421
+ return /* @__PURE__ */ jsxs("button", {
422
+ ref: (el) => {
423
+ attentionRef.current = el;
424
+ if (typeof buttonRef === "function") buttonRef(el);
425
+ else if (buttonRef) buttonRef.current = el;
426
+ },
427
+ type: "button",
428
+ "data-agent-graph-node": entity.id,
429
+ "data-kind": entity.kind,
430
+ "data-status": entity.status,
431
+ "data-activity": entity.quiet ? "quiet" : void 0,
432
+ "aria-label": `${entity.label}${entity.sub ? `, ${entity.sub}` : ""}${entity.status ? `, ${entity.status === "needs-you" ? "needs you" : entity.status}` : ""}`,
433
+ "aria-expanded": selected,
434
+ className: `ag-node ag-node-${entity.kind}${dimmed ? " ag-dimmed" : ""}${selected ? " ag-selected" : ""}`,
435
+ style: {
436
+ left: x,
437
+ top: y,
438
+ "--ag-tile": `${tile}px`,
439
+ ...tone ? { "--ag-tone": tone } : {}
440
+ },
441
+ onClick: onOpen,
442
+ onPointerEnter: () => onHover(true),
443
+ onPointerLeave: () => onHover(false),
444
+ onFocus: () => onHover(true),
445
+ onBlur: () => onHover(false),
446
+ onKeyDown,
447
+ children: [
448
+ /* @__PURE__ */ jsxs("span", {
449
+ className: "ag-tile",
450
+ children: [/* @__PURE__ */ jsx(NodeIcon, {
451
+ entity,
452
+ size: tile
453
+ }), /* @__PURE__ */ jsx(StatusBadge, {
454
+ status: entity.status,
455
+ quiet: entity.quiet
456
+ })]
457
+ }),
458
+ /* @__PURE__ */ jsx("span", {
459
+ className: "ag-name",
460
+ children: entity.label
461
+ }),
462
+ sub && /* @__PURE__ */ jsx("span", {
463
+ className: "ag-sub",
464
+ style: tone ? { color: tone } : void 0,
465
+ children: entity.quiet ? /* @__PURE__ */ jsxs(Fragment, { children: [
466
+ statusMeta(entity.status ?? "running").label,
467
+ " · ",
468
+ /* @__PURE__ */ jsx("span", {
469
+ style: { color: "var(--surface-warning-text)" },
470
+ children: sub
471
+ })
472
+ ] }) : sub
473
+ })
474
+ ]
475
+ });
476
+ }
477
+ const fmtMs = (ms) => ms < 1e3 ? `${Math.round(ms / 100) * 100 || Math.round(ms)}ms` : `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
478
+ /** A node's recent calls: who it called or was called by, what for, how long, and what failed. */
479
+ function HubPopover({ entity, entities, calls, now, left, top, summary, closeRef, onClose }) {
480
+ const element = useRef(null);
481
+ const [fitted, setFitted] = useState({
482
+ left,
483
+ top
484
+ });
485
+ useLayoutEffect(() => {
486
+ const dialog = element.current, container = dialog?.parentElement;
487
+ if (!dialog || !container) return;
488
+ const fit = () => {
489
+ const relative = getComputedStyle(dialog).position !== "absolute";
490
+ const maxHeight = Math.max(0, container.clientHeight - 16);
491
+ const height = Math.min(dialog.scrollHeight + dialog.offsetHeight - dialog.clientHeight, maxHeight);
492
+ const next = relative ? {
493
+ left,
494
+ top
495
+ } : {
496
+ left: Math.max(container.scrollLeft + 8, Math.min(left, container.scrollLeft + container.clientWidth - dialog.offsetWidth - 8)),
497
+ top: Math.max(container.scrollTop + 8, Math.min(top, container.scrollTop + container.clientHeight - height - 8)),
498
+ maxHeight
499
+ };
500
+ setFitted((previous) => previous.left === next.left && previous.top === next.top && previous.maxHeight === next.maxHeight ? previous : next);
501
+ };
502
+ fit();
503
+ const observer = new ResizeObserver(fit);
504
+ observer.observe(dialog);
505
+ observer.observe(container);
506
+ return () => observer.disconnect();
507
+ }, [left, top]);
508
+ const mine = (calls ?? []).filter((c) => (c.from === entity.id || c.to === entity.id) && c.startedAt <= now).sort((a, b) => b.startedAt - a.startedAt);
509
+ const recent = mine.slice(0, 5);
510
+ const lastMinute = mine.filter((c) => now - c.startedAt <= 6e4).length;
511
+ const status = entity.status ? statusMeta(entity.status).label : void 0;
512
+ const cost = entity.agent?.costUsd;
513
+ return /* @__PURE__ */ jsxs("div", {
514
+ ref: element,
515
+ role: "dialog",
516
+ "aria-label": `${entity.label} calls`,
517
+ className: "ag-popover",
518
+ style: fitted,
519
+ onKeyDown: (event) => {
520
+ if (event.key === "Escape") {
521
+ event.stopPropagation();
522
+ onClose();
523
+ }
524
+ },
525
+ children: [
526
+ /* @__PURE__ */ jsxs("div", {
527
+ className: "ag-pop-head",
528
+ children: [
529
+ /* @__PURE__ */ jsx("span", {
530
+ className: "ag-tile ag-tile-mini",
531
+ style: { "--ag-tile": "28px" },
532
+ children: /* @__PURE__ */ jsx(NodeIcon, {
533
+ entity,
534
+ size: 28
535
+ })
536
+ }),
537
+ /* @__PURE__ */ jsxs("span", {
538
+ className: "ag-pop-title",
539
+ children: [/* @__PURE__ */ jsx("span", {
540
+ className: "ag-pop-name",
541
+ children: entity.label
542
+ }), (entity.agent?.model || entity.sub || entity.tool?.detail) && /* @__PURE__ */ jsx("span", {
543
+ className: "ag-pop-sub",
544
+ children: entity.agent?.model ?? entity.tool?.detail ?? entity.sub
545
+ })]
546
+ }),
547
+ status && /* @__PURE__ */ jsxs("span", {
548
+ className: `ag-pill ag-pill-${entity.status}`,
549
+ children: [entity.status === "running" && !entity.quiet ? /* @__PURE__ */ jsx("span", {
550
+ className: "ag-running-dot",
551
+ "data-motion": "essential",
552
+ style: ambientStyle,
553
+ onAnimationStart: syncAmbient
554
+ }) : /* @__PURE__ */ jsx(RunStatusIcon, {
555
+ status: entity.quiet ? "waiting" : entity.status,
556
+ "aria-hidden": true
557
+ }), entity.quiet ?? status]
558
+ }),
559
+ /* @__PURE__ */ jsx("button", {
560
+ ref: closeRef,
561
+ type: "button",
562
+ className: "ag-close",
563
+ "aria-label": "Close",
564
+ onClick: onClose,
565
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
566
+ })
567
+ ]
568
+ }),
569
+ summary && /* @__PURE__ */ jsx("p", {
570
+ className: "ag-pop-summary",
571
+ children: summary
572
+ }),
573
+ recent.length > 0 && /* @__PURE__ */ jsx("ul", {
574
+ className: "ag-pop-calls",
575
+ children: recent.map((c) => {
576
+ const outgoing = c.from === entity.id;
577
+ const other = entities.get(outgoing ? c.to : c.from);
578
+ const running = c.durationMs == null;
579
+ return /* @__PURE__ */ jsxs("li", { children: [
580
+ /* @__PURE__ */ jsx("span", {
581
+ className: "ag-pop-dir",
582
+ "aria-label": outgoing ? "to" : "from",
583
+ children: outgoing ? "→" : "←"
584
+ }),
585
+ other && /* @__PURE__ */ jsx("span", {
586
+ className: "ag-tile ag-tile-micro",
587
+ style: { "--ag-tile": "16px" },
588
+ children: /* @__PURE__ */ jsx(NodeIcon, {
589
+ entity: other,
590
+ size: 16
591
+ })
592
+ }),
593
+ /* @__PURE__ */ jsxs("span", {
594
+ className: "ag-pop-call",
595
+ children: [/* @__PURE__ */ jsx("b", { children: other?.label ?? (outgoing ? c.to : c.from) }), c.label ? ` · ${c.label}` : ""]
596
+ }),
597
+ /* @__PURE__ */ jsx("span", {
598
+ className: "ag-pop-time",
599
+ style: c.kind === "failed" ? { color: CALL_COLOR.failed } : void 0,
600
+ children: c.kind === "failed" ? "Failed" : running ? /* @__PURE__ */ jsx("span", {
601
+ className: "ag-running-dot",
602
+ "data-motion": "essential",
603
+ "aria-label": "Running",
604
+ style: ambientStyle,
605
+ onAnimationStart: syncAmbient
606
+ }) : fmtMs(c.durationMs)
607
+ })
608
+ ] }, c.id);
609
+ })
610
+ }),
611
+ /* @__PURE__ */ jsxs("p", {
612
+ className: "ag-pop-foot",
613
+ children: [cost != null && /* @__PURE__ */ jsx("b", { children: fmtCost(cost) }), /* @__PURE__ */ jsxs("span", {
614
+ title: calls === void 0 ? "Call history not reported" : void 0,
615
+ children: [
616
+ calls === void 0 ? "—" : lastMinute,
617
+ " ",
618
+ lastMinute === 1 ? "call" : "calls",
619
+ " in the last minute"
620
+ ]
621
+ })]
622
+ })
623
+ ]
624
+ });
625
+ }
626
+ //#endregion
627
+ //#region src/react/agent-graph/AgentHubCanvas.tsx
628
+ /** The hub presents agent and tool calls on a stable canvas or a readable list. */
629
+ const PAD_X = 84;
630
+ const PAD_Y = 36;
631
+ function useWidth(ref) {
632
+ const [width, setWidth] = useState(0);
633
+ useLayoutEffect(() => {
634
+ const el = ref.current;
635
+ if (!el) return;
636
+ setWidth(el.clientWidth);
637
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
638
+ observer.observe(el);
639
+ return () => observer.disconnect();
640
+ }, [ref]);
641
+ return width;
642
+ }
643
+ function CallStroke({ call, d, length, now, forward, animate }) {
644
+ const ref = useRef(null);
645
+ const routeRef = useRef(null);
646
+ const [chevron, setChevron] = useState();
647
+ const end = call.durationMs == null ? Infinity : call.startedAt + call.durationMs;
648
+ const returning = now >= end;
649
+ useLayoutEffect(() => {
650
+ const path = routeRef.current;
651
+ if (!path) return;
652
+ const middle = path.getTotalLength() / 2, point = path.getPointAtLength(middle), before = path.getPointAtLength(middle - 1), after = path.getPointAtLength(middle + 1);
653
+ const ambient = tokenMilliseconds("--duration-ambient", path);
654
+ setChevron({
655
+ x: point.x,
656
+ y: point.y,
657
+ angle: Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI + (forward === returning ? 180 : 0),
658
+ travel: Math.min(ambient * .9, Math.max(ambient * .3, length / 320 * 1e3)),
659
+ decay: ambient * 4
660
+ });
661
+ }, [
662
+ d,
663
+ length,
664
+ forward,
665
+ returning
666
+ ]);
667
+ useEffect(() => {
668
+ const el = ref.current;
669
+ const duration = chevron?.travel;
670
+ if (!el || !animate || call.kind === "failed" || !duration) return;
671
+ const age = now - (returning ? end : call.startedAt);
672
+ if (age > duration) return;
673
+ const reverse = forward === returning;
674
+ const animation = el.animate([{
675
+ strokeDashoffset: reverse ? -length : 24,
676
+ opacity: .35
677
+ }, {
678
+ strokeDashoffset: reverse ? 24 : -length,
679
+ opacity: .35
680
+ }], {
681
+ duration,
682
+ easing: "linear"
683
+ });
684
+ animation.currentTime = Math.max(0, age);
685
+ return () => animation.cancel();
686
+ }, [
687
+ call.id,
688
+ returning,
689
+ animate,
690
+ length,
691
+ forward,
692
+ chevron?.travel
693
+ ]);
694
+ const age = Math.max(0, now - end);
695
+ const tint = returning ? Math.max(0, .55 * (1 - age / (chevron?.decay || 1))) : .55;
696
+ return /* @__PURE__ */ jsxs("g", {
697
+ "data-call-id": call.id,
698
+ "data-direction": forward !== returning ? "forward" : "return",
699
+ style: { stroke: CALL_COLOR[call.kind] },
700
+ children: [
701
+ /* @__PURE__ */ jsx("path", {
702
+ ref: routeRef,
703
+ d,
704
+ className: "ag-edge-live",
705
+ opacity: tint
706
+ }),
707
+ chevron && /* @__PURE__ */ jsx("path", {
708
+ "data-call-arrow": true,
709
+ opacity: returning ? tint / .55 : 1,
710
+ d: "M-3,-3 L1,0 L-3,3",
711
+ fill: "none",
712
+ strokeWidth: 1.5,
713
+ strokeLinecap: "round",
714
+ strokeLinejoin: "round",
715
+ transform: `translate(${chevron.x},${chevron.y}) rotate(${chevron.angle})`
716
+ }),
717
+ animate && /* @__PURE__ */ jsx("path", {
718
+ ref,
719
+ d,
720
+ className: "ag-flow",
721
+ strokeDasharray: `24 ${length}`,
722
+ opacity: 0
723
+ })
724
+ ]
725
+ });
726
+ }
727
+ function AgentHubCanvas(props) {
728
+ const { nodes, edges, tools = [], calls = [], you, now: nowProp, title = "Agent graph", selectedId, onSelect, className } = props;
729
+ const wrap = useRef(null);
730
+ const width = useWidth(wrap);
731
+ const now = useRunNow(nowProp);
732
+ const [hover, setHover] = useState(null);
733
+ const [open, setOpen] = useState(selectedId ?? null);
734
+ const buttons = useRef(/* @__PURE__ */ new Map());
735
+ const closeRef = useRef(null);
736
+ const animate = useMotionAllowed();
737
+ const visibleCalls = useMemo(() => calls.filter((c) => c.startedAt <= now), [calls, now]);
738
+ const nextLayout = useMemo(() => layoutHub({
739
+ nodes,
740
+ edges,
741
+ tools,
742
+ calls,
743
+ you: Boolean(you)
744
+ }), [
745
+ nodes,
746
+ edges,
747
+ tools,
748
+ calls,
749
+ you
750
+ ]);
751
+ const layoutKey = JSON.stringify(nextLayout);
752
+ const layoutRef = useRef({
753
+ key: layoutKey,
754
+ layout: nextLayout
755
+ });
756
+ if (layoutRef.current.key !== layoutKey) layoutRef.current = {
757
+ key: layoutKey,
758
+ layout: nextLayout
759
+ };
760
+ const layout = layoutRef.current.layout;
761
+ const callSummaries = useMemo(() => {
762
+ const summaries = {
763
+ call: /* @__PURE__ */ new Map(),
764
+ answer: /* @__PURE__ */ new Map(),
765
+ "needs-you": /* @__PURE__ */ new Map()
766
+ };
767
+ for (const call of visibleCalls) if (call.kind !== "failed") {
768
+ const bucket = summaries[call.kind], id = call.kind === "call" ? call.to : call.from;
769
+ if (!bucket.has(id) || bucket.get(id).startedAt < call.startedAt) bucket.set(id, call);
770
+ }
771
+ return summaries;
772
+ }, [visibleCalls]);
773
+ const span = (m) => ({
774
+ w: (layout.maxCol - layout.minCol) * m.cellX + 168,
775
+ h: (layout.maxRow - layout.minRow) * m.cellY + 72 + m.hubTile + m.label
776
+ });
777
+ const metrics = HUB_METRICS;
778
+ const { w, h } = span(metrics);
779
+ const list = width > 0 && (width < 600 || width / w < .85);
780
+ const scale = width ? Math.max(.85, Math.min(1, width / w)) : 1;
781
+ const entities = useMemo(() => {
782
+ const map = /* @__PURE__ */ new Map();
783
+ for (const p of layout.placements) if (p.kind === "you") map.set(p.id, {
784
+ id: p.id,
785
+ kind: "you",
786
+ label: typeof you === "object" && you.label ? you.label : "You"
787
+ });
788
+ else if (p.kind === "tool") {
789
+ const tool = tools.find((t) => t.id === p.id);
790
+ map.set(p.id, {
791
+ id: p.id,
792
+ kind: "tool",
793
+ label: tool.label,
794
+ tool
795
+ });
796
+ } else {
797
+ const agent = nodes.find((n) => n.id === p.id);
798
+ map.set(p.id, {
799
+ id: p.id,
800
+ kind: p.kind,
801
+ label: agent.label,
802
+ sub: (agent.status === "succeeded" ? agent.result ?? callSummaries.answer.get(agent.id)?.label : void 0) ?? agent.turn ?? (agent.status === "needs-you" ? callSummaries["needs-you"].get(agent.id)?.label : void 0) ?? callSummaries.call.get(agent.id)?.label ?? void 0,
803
+ status: agent.status,
804
+ quiet: agentQuietActivity(agent, now)?.label,
805
+ agent
806
+ });
807
+ }
808
+ return map;
809
+ }, [
810
+ layout,
811
+ nodes,
812
+ tools,
813
+ you,
814
+ now,
815
+ callSummaries
816
+ ]);
817
+ const { positions, routes } = useMemo(() => {
818
+ const positions = new Map(layout.placements.map((p) => [p.id, {
819
+ x: (p.col - layout.minCol) * metrics.cellX + PAD_X,
820
+ y: (p.row - layout.minRow) * metrics.cellY + PAD_Y + metrics.hubTile / 2,
821
+ tile: p.kind === "hub" ? metrics.hubTile : p.kind === "you" ? metrics.tile - 8 : metrics.tile
822
+ }]));
823
+ const bounds = layout.placements.map((p) => {
824
+ const { x, y, tile } = positions.get(p.id);
825
+ return {
826
+ id: p.id,
827
+ tile: {
828
+ left: x - tile / 2 - 4,
829
+ right: x + tile / 2 + 4,
830
+ top: y - tile / 2 - 4,
831
+ bottom: y + tile / 2 + 4
832
+ },
833
+ label: {
834
+ left: x - (p.kind === "tool" || p.kind === "you" ? 70 : 92),
835
+ right: x + (p.kind === "tool" || p.kind === "you" ? 70 : 92),
836
+ top: y + tile / 2 + 4,
837
+ bottom: y + tile / 2 + (p.kind === "tool" || p.kind === "you" ? 30 : 64)
838
+ }
839
+ };
840
+ });
841
+ let lanes = 0;
842
+ return {
843
+ positions,
844
+ routes: new Map(layout.links.map((link) => {
845
+ const a = positions.get(link.from), b = positions.get(link.to);
846
+ const lane = layout.placements.find((p) => p.id === link.from)?.kind === "hub" ? (++lanes % 2 ? -1 : 1) * Math.ceil(lanes / 2) * 12 : 0;
847
+ return [`${link.from}|${link.to}`, orthogonalPath(a, b, b.y < a.y, lane, 10, bounds.flatMap((rect) => [...rect.id === link.from || rect.id === link.to ? [] : [rect.tile], rect.label]))];
848
+ }))
849
+ };
850
+ }, [layout, metrics]);
851
+ const at = (id) => positions.get(id);
852
+ const edgeState = (from, to) => {
853
+ let latest;
854
+ for (const c of visibleCalls) if (c.from === from && c.to === to || c.from === to && c.to === from) {
855
+ if (!latest || c.startedAt > latest.startedAt) latest = c;
856
+ }
857
+ if (!latest) return void 0;
858
+ return latest;
859
+ };
860
+ const focus = hover ?? open;
861
+ const related = useMemo(() => {
862
+ if (!focus) return null;
863
+ const set = /* @__PURE__ */ new Set([focus]);
864
+ for (const l of layout.links) {
865
+ if (l.from === focus) set.add(l.to);
866
+ if (l.to === focus) set.add(l.from);
867
+ }
868
+ return set;
869
+ }, [focus, layout]);
870
+ const availableTools = tools.filter((tool) => !visibleCalls.some((call) => call.from === tool.id || call.to === tool.id));
871
+ const lastMinute = visibleCalls.filter((c) => now - c.startedAt <= 6e4).length;
872
+ const costs = nodes.map((n) => n.costUsd);
873
+ const cost = costs.every((c) => c != null && Number.isFinite(c) && c >= 0) ? costs.reduce((s, c) => s + (c ?? 0), 0) : void 0;
874
+ const chips = nodes.filter((n) => n.status === "needs-you" || n.status === "failed" || agentQuietActivity(n, now));
875
+ const openNode = (id) => {
876
+ const next = open === id ? null : id;
877
+ setOpen(next);
878
+ if (next) onSelect?.(next);
879
+ };
880
+ useEffect(() => {
881
+ if (open) {
882
+ closeRef.current?.closest("[role=\"dialog\"]")?.scrollIntoView({ block: "nearest" });
883
+ closeRef.current?.focus({ preventScroll: true });
884
+ }
885
+ }, [open]);
886
+ const keydown = (id) => (event) => {
887
+ const dir = {
888
+ ArrowLeft: [-1, 0],
889
+ ArrowRight: [1, 0],
890
+ ArrowUp: [0, -1],
891
+ ArrowDown: [0, 1]
892
+ }[event.key];
893
+ if (event.key === "Escape") {
894
+ setOpen(null);
895
+ buttons.current.get(id)?.focus();
896
+ return;
897
+ }
898
+ if (event.key === "Home") {
899
+ event.preventDefault();
900
+ nodes[0] && buttons.current.get(nodes[0].id)?.focus();
901
+ return;
902
+ }
903
+ if (!dir) return;
904
+ event.preventDefault();
905
+ const from = at(id);
906
+ let best, bestScore = Infinity;
907
+ for (const p of layout.placements) {
908
+ if (p.id === id) continue;
909
+ const to = at(p.id);
910
+ const dx = to.x - from.x, dy = to.y - from.y;
911
+ const along = dx * dir[0] + dy * dir[1];
912
+ if (along <= 0) continue;
913
+ const score = along + Math.abs(dx * dir[1] - dy * dir[0]) * 2;
914
+ if (score < bestScore) {
915
+ bestScore = score;
916
+ best = p.id;
917
+ }
918
+ }
919
+ if (best) buttons.current.get(best)?.focus();
920
+ };
921
+ const openEntity = open ? entities.get(open) : void 0;
922
+ const popover = openEntity && (() => {
923
+ const p = at(openEntity.id);
924
+ const left = (p.x + 140 + 280) * scale < (width || w) ? (p.x + 40) * scale : Math.max(8, (p.x - 40) * scale - 280);
925
+ const top = list ? 8 : Math.max(8, (p.y - metrics.hubTile / 2 - 8) * scale);
926
+ const summary = openEntity.kind === "you" ? typeof you === "object" ? you.summary : void 0 : openEntity.agent?.turn ?? openEntity.sub ?? void 0;
927
+ return /* @__PURE__ */ jsx(HubPopover, {
928
+ entity: openEntity,
929
+ entities,
930
+ calls: props.calls === void 0 ? void 0 : visibleCalls,
931
+ now,
932
+ left,
933
+ top,
934
+ summary,
935
+ closeRef,
936
+ onClose: () => {
937
+ const id = open;
938
+ setOpen(null);
939
+ buttons.current.get(id)?.focus();
940
+ }
941
+ });
942
+ })();
943
+ return /* @__PURE__ */ jsxs("section", {
944
+ className: `ag-root ${className ?? ""}`,
945
+ "aria-label": typeof title === "string" ? title : "Agent graph",
946
+ children: [
947
+ /* @__PURE__ */ jsx("style", { children: AGENT_HUB_CSS }),
948
+ /* @__PURE__ */ jsxs("header", {
949
+ className: "ag-head",
950
+ children: [
951
+ /* @__PURE__ */ jsxs("span", {
952
+ className: "ag-title",
953
+ children: [/* @__PURE__ */ jsx("span", {
954
+ className: "ag-title-mark",
955
+ "aria-hidden": true
956
+ }), title]
957
+ }),
958
+ chips.map((n) => /* @__PURE__ */ jsxs("button", {
959
+ type: "button",
960
+ className: `ag-chip ag-chip-${n.status}`,
961
+ onClick: () => openNode(n.id),
962
+ children: [
963
+ /* @__PURE__ */ jsx(RunStatusIcon, {
964
+ status: agentQuietActivity(n, now) ? "waiting" : n.status,
965
+ "aria-hidden": true
966
+ }),
967
+ n.label,
968
+ " ",
969
+ agentQuietActivity(n, now)?.label ?? statusMeta(n.status).label
970
+ ]
971
+ }, n.id)),
972
+ /* @__PURE__ */ jsxs("span", {
973
+ className: "ag-stats",
974
+ children: [
975
+ /* @__PURE__ */ jsxs("span", { children: [
976
+ /* @__PURE__ */ jsx("b", { children: nodes.length }),
977
+ " ",
978
+ nodes.length === 1 ? "agent" : "agents"
979
+ ] }),
980
+ /* @__PURE__ */ jsxs("span", {
981
+ title: props.tools === void 0 ? "Tool history not reported" : void 0,
982
+ children: [
983
+ /* @__PURE__ */ jsx("b", { children: props.tools?.length ?? "—" }),
984
+ " ",
985
+ tools.length === 1 ? "tool" : "tools"
986
+ ]
987
+ }),
988
+ /* @__PURE__ */ jsxs("span", {
989
+ title: props.calls === void 0 ? "Call history not reported" : void 0,
990
+ children: [/* @__PURE__ */ jsx("b", { children: props.calls === void 0 ? "—" : lastMinute }), " calls/min"]
991
+ }),
992
+ /* @__PURE__ */ jsxs("span", {
993
+ title: cost === void 0 ? "Not every agent recorded its cost" : "Total recorded agent cost",
994
+ children: [/* @__PURE__ */ jsx("b", { children: cost === void 0 ? "—" : fmtCost(cost) }), " cost"]
995
+ })
996
+ ]
997
+ })
998
+ ]
999
+ }),
1000
+ /* @__PURE__ */ jsxs("div", {
1001
+ ref: wrap,
1002
+ className: `ag-canvas-wrap${list ? " ag-list-mode" : ""}`,
1003
+ style: list ? void 0 : { height: h * scale },
1004
+ onKeyDown: (e) => {
1005
+ if (e.key === "Escape") setOpen(null);
1006
+ },
1007
+ children: [list ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1008
+ className: "ag-mobile",
1009
+ children: [...entities.values()].filter((e) => e.agent).sort((a, b) => {
1010
+ const rank = (e) => e.status === "needs-you" ? 0 : e.status === "failed" ? 1 : e.quiet ? 2 : e.status === "waiting" ? 3 : e.status === "running" ? 4 : 5;
1011
+ return rank(a) - rank(b);
1012
+ }).map((entity) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(HubNode, {
1013
+ entity,
1014
+ x: 0,
1015
+ y: 0,
1016
+ tile: 32,
1017
+ dimmed: false,
1018
+ selected: open === entity.id,
1019
+ buttonRef: (el) => {
1020
+ if (el) buttons.current.set(entity.id, el);
1021
+ else buttons.current.delete(entity.id);
1022
+ },
1023
+ onOpen: () => openNode(entity.id),
1024
+ onHover: () => {},
1025
+ onKeyDown: keydown(entity.id)
1026
+ }), /* @__PURE__ */ jsx("div", {
1027
+ className: "ag-tools",
1028
+ children: tools.filter((t) => visibleCalls.some((c) => c.from === entity.id && c.to === t.id || c.to === entity.id && c.from === t.id)).map((t) => /* @__PURE__ */ jsx("button", {
1029
+ type: "button",
1030
+ onClick: () => openNode(t.id),
1031
+ children: t.label
1032
+ }, t.id))
1033
+ })] }, entity.id))
1034
+ }), availableTools.length > 0 && /* @__PURE__ */ jsxs("div", {
1035
+ className: "ag-available",
1036
+ children: [/* @__PURE__ */ jsx("span", { children: "Available tools" }), /* @__PURE__ */ jsx("div", {
1037
+ className: "ag-tools",
1038
+ children: availableTools.map((tool) => /* @__PURE__ */ jsx("button", {
1039
+ type: "button",
1040
+ onClick: () => openNode(tool.id),
1041
+ children: tool.label
1042
+ }, tool.id))
1043
+ })]
1044
+ })] }) : /* @__PURE__ */ jsxs("div", {
1045
+ className: "ag-canvas",
1046
+ style: {
1047
+ width: w,
1048
+ height: h,
1049
+ transform: `scale(${scale})`
1050
+ },
1051
+ children: [/* @__PURE__ */ jsx("svg", {
1052
+ className: "ag-edges",
1053
+ width: w,
1054
+ height: h,
1055
+ "aria-hidden": true,
1056
+ children: layout.links.map((l) => {
1057
+ const { d, length } = routes.get(`${l.from}|${l.to}`);
1058
+ const live = edgeState(l.from, l.to);
1059
+ const touches = !related || related.has(l.from) && related.has(l.to);
1060
+ const forward = live ? live.from === l.from : true;
1061
+ return /* @__PURE__ */ jsxs("g", {
1062
+ className: touches ? void 0 : "ag-dimmed",
1063
+ children: [/* @__PURE__ */ jsx("path", {
1064
+ d,
1065
+ className: "ag-edge"
1066
+ }), live && /* @__PURE__ */ jsx(CallStroke, {
1067
+ call: live,
1068
+ d,
1069
+ length,
1070
+ now,
1071
+ forward,
1072
+ animate
1073
+ })]
1074
+ }, `${l.from}-${l.to}`);
1075
+ })
1076
+ }), layout.placements.map((p) => {
1077
+ const entity = entities.get(p.id);
1078
+ const { x, y } = at(p.id);
1079
+ const tile = p.kind === "hub" ? metrics.hubTile : p.kind === "you" ? metrics.tile - 8 : metrics.tile;
1080
+ return /* @__PURE__ */ jsx(HubNode, {
1081
+ entity,
1082
+ x,
1083
+ y,
1084
+ tile,
1085
+ dimmed: Boolean(related && !related.has(p.id)),
1086
+ selected: open === p.id,
1087
+ buttonRef: (el) => {
1088
+ if (el) buttons.current.set(p.id, el);
1089
+ else buttons.current.delete(p.id);
1090
+ },
1091
+ onOpen: () => openNode(p.id),
1092
+ onHover: (on) => setHover((prev) => on ? p.id : prev === p.id ? null : prev),
1093
+ onKeyDown: keydown(p.id)
1094
+ }, p.id);
1095
+ })]
1096
+ }), popover]
1097
+ }),
1098
+ !list && /* @__PURE__ */ jsx("ul", {
1099
+ className: "ag-legend",
1100
+ "aria-label": "Edge colours",
1101
+ children: [
1102
+ "call",
1103
+ "answer",
1104
+ "needs-you",
1105
+ "failed"
1106
+ ].map((k) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
1107
+ className: "ag-legend-line",
1108
+ style: { background: CALL_COLOR[k] }
1109
+ }), CALL_LABEL[k]] }, k))
1110
+ })
1111
+ ]
1112
+ });
1113
+ }
1114
+ //#endregion
1115
+ //#region src/react/agent-graph/AgentGraph.tsx
1116
+ /**
1117
+ * `AgentGraph` — the topology of a multi-agent run, for a run page's side pane.
1118
+ *
1119
+ * Lazy and error-isolated like `WorkflowGraph`: React Flow and ELK load on
1120
+ * first render, a transient chunk failure retries, and a graph that cannot
1121
+ * render degrades to a plain list of the agents (still selectable) instead of
1122
+ * taking the pane down.
1123
+ */
1124
+ function lazyCanvas() {
1125
+ return lazy(() => retryImport(() => import("../../AgentGraphCanvas-B74GN1o6.js").then((m) => ({ default: m.AgentGraphCanvas }))));
1126
+ }
1127
+ /** The agents as a list, for when the canvas cannot load. */
1128
+ function AgentListFallback({ nodes, selectedId, onSelect, className, onRetry }) {
1129
+ return /* @__PURE__ */ jsxs("div", {
1130
+ className: `agent-graph overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
1131
+ children: [/* @__PURE__ */ jsxs("div", {
1132
+ className: "mb-1 flex items-center justify-between gap-2",
1133
+ children: [/* @__PURE__ */ jsx("p", {
1134
+ className: "text-muted-foreground text-xs",
1135
+ children: "Couldn't draw the graph — showing the agents."
1136
+ }), /* @__PURE__ */ jsx("button", {
1137
+ type: "button",
1138
+ onClick: onRetry,
1139
+ className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-muted-foreground text-xs transition hover:text-foreground",
1140
+ children: "Retry"
1141
+ })]
1142
+ }), /* @__PURE__ */ jsx("ul", {
1143
+ className: "space-y-0.5",
1144
+ children: nodes.map((n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
1145
+ type: "button",
1146
+ "aria-pressed": selectedId === n.id,
1147
+ onClick: () => onSelect?.(n.id),
1148
+ className: "flex w-full items-center justify-between gap-2 rounded px-2 py-1 text-left text-foreground text-xs hover:bg-muted aria-pressed:bg-muted",
1149
+ children: [/* @__PURE__ */ jsx("span", {
1150
+ className: "truncate",
1151
+ children: n.label
1152
+ }), /* @__PURE__ */ jsx("span", {
1153
+ className: "shrink-0 text-muted-foreground",
1154
+ children: n.status
1155
+ })]
1156
+ }) }, n.id))
1157
+ })]
1158
+ });
1159
+ }
1160
+ function AgentGraphBody(props) {
1161
+ if (props.layout === "hub") {
1162
+ if (props.nodes.length === 0 && props.emptyState) return /* @__PURE__ */ jsx(Fragment, { children: props.emptyState });
1163
+ return /* @__PURE__ */ jsx(AgentHubCanvas, { ...props });
1164
+ }
1165
+ return /* @__PURE__ */ jsx(AgentGraphTree, { ...props });
1166
+ }
1167
+ function AgentGraphTree(props) {
1168
+ const [attempt, setAttempt] = useState(0);
1169
+ const Canvas = useMemo(() => lazyCanvas(), [attempt]);
1170
+ return /* @__PURE__ */ jsx(GraphErrorBoundary, {
1171
+ fallback: /* @__PURE__ */ jsx(AgentListFallback, {
1172
+ ...props,
1173
+ onRetry: () => setAttempt((a) => a + 1)
1174
+ }),
1175
+ children: /* @__PURE__ */ jsx(Suspense, {
1176
+ fallback: /* @__PURE__ */ jsx("div", {
1177
+ className: `agent-graph flex items-center justify-center rounded-lg border border-border border-dashed text-muted-foreground text-xs ${props.className ?? ""}`,
1178
+ children: "Loading graph…"
1179
+ }),
1180
+ children: /* @__PURE__ */ jsx(Canvas, { ...props })
1181
+ })
1182
+ }, attempt);
1183
+ }
1184
+ function AgentGraph(props) {
1185
+ return /* @__PURE__ */ jsx(GraphMarksProvider, {
1186
+ value: props,
1187
+ children: /* @__PURE__ */ jsx(AgentGraphBody, { ...props })
1188
+ });
1189
+ }
1190
+ //#endregion
1191
+ export { AGENT_CARD, AgentGraph, FRAME_CAPTION, GRID_MIN_LEAVES, GraphErrorBoundary, HUB_METRICS, LIVE_AGENT_CARD, LatestEvents, RESTING_BORDER, RESTING_SHADOW, SELECTION_OUTLINE, YOU_ID, actionPathLabel, activityTiming, agentQuietActivity, ambientStyle, buildAgentTopology, clampPreview, edgeColor, fmtCost, fmtDuration, fmtEventTime, fmtTokenTotal, fmtTokens, hubAgentId, humanizeIdentifier, layoutAgents, layoutHub, loadElk, motionAllowed, orthogonalPath, parseActionPath, retryImport, shortModel, syncAmbient, tokenMilliseconds, useAttention, useColorMode, useMotionAllowed, useRunNow };