@tangle-network/sandbox-ui 2.0.11 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{node-ui-D8dbX86X.js → AgentGraph-CqLe8WCH.js} +43 -123
  2. package/dist/AgentGraph-P7l-jZwo.d.ts +4 -0
  3. package/dist/{WorkflowGraph-BtJnA5iY.js → WorkflowGraph-s3RZ-ANZ.js} +6 -7
  4. package/dist/{dashboard-FMrtIe9S.js → dashboard-D3JhmRhe.js} +4 -4
  5. package/dist/dashboard.js +1 -1
  6. package/dist/globals.css +1550 -87
  7. package/dist/{index-BK8KKLNY.d.ts → index-CEtc2U5v.d.ts} +2 -2
  8. package/dist/{index-BKchjsHk.d.ts → index-q-I_XQu4.d.ts} +6 -16
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +6 -7
  11. package/dist/motion-2Ine0j_s.js +1 -0
  12. package/dist/primitives-znfPK9NP.js +23 -0
  13. package/dist/primitives.d.ts +1 -1
  14. package/dist/primitives.js +1 -1
  15. package/dist/{record-D-evY-c_.js → record-Dt9gqJee.js} +67 -24
  16. package/dist/run/record.js +1 -1
  17. package/dist/{run-CJyFiBG9.js → run-DBTbnQrC.js} +7 -6
  18. package/dist/run.d.ts +1 -1
  19. package/dist/run.js +2 -3
  20. package/dist/styles.css +1550 -87
  21. package/dist/tailwind.css +1800 -79
  22. package/dist/{workbench-BVnZNhP2.js → workbench-D9Bm4vMq.js} +1 -1
  23. package/dist/workbench.js +1 -1
  24. package/dist/workflows.d.ts +4 -59
  25. package/dist/workflows.js +62 -2
  26. package/dist/{workspace-CdcRUl-H.js → workspace-CJnBqCqj.js} +7 -7
  27. package/dist/workspace.js +1 -1
  28. package/package.json +5 -6
  29. package/dist/AgentGraph-CFlI9qta.js +0 -1380
  30. package/dist/AgentGraph-DzdpPFEI.d.ts +0 -132
  31. package/dist/AgentGraphCanvas-CmxReSqa.js +0 -1403
  32. package/dist/canvas-theme-aqWjGbfu.js +0 -55
  33. package/dist/latest-events-v_vXRyd7.js +0 -169
  34. package/dist/motion-Bn7ESHrj.js +0 -6
  35. package/dist/primitives-CCdx5Hh_.js +0 -23
@@ -1,1380 +0,0 @@
1
- import { i as modelBrandFor, t as ModelBrandStack } from "./model-brand-bTcMjQtB.js";
2
- import { t as ProviderIcon } from "./provider-logo-CPKRPHcS.js";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
- import { Component, Suspense, lazy, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
- import { Bot, User, Wrench, X } from "lucide-react";
6
- import { RunStatusIcon, runStatus, statusMeta } from "@tangle-network/ui/run";
7
- // src/workflows/run-ui-state.ts
8
- const listeners = /* @__PURE__ */ new Set();
9
- let clock = Date.now();
10
- let timer;
11
- function tick() {
12
- clock = Date.now();
13
- for (const listener of listeners) listener();
14
- timer = setTimeout(tick, 1e3 - Date.now() % 1e3);
15
- }
16
- function subscribe(listener) {
17
- listeners.add(listener);
18
- if (listeners.size === 1) tick();
19
- return () => {
20
- listeners.delete(listener);
21
- if (!listeners.size) clearTimeout(timer);
22
- };
23
- }
24
- const noSubscribe = () => () => {};
25
- function useRunNow(fixed) {
26
- const now = useSyncExternalStore(fixed === void 0 ? subscribe : noSubscribe, () => fixed ?? clock, () => fixed ?? 0);
27
- return fixed ?? now;
28
- }
29
- const motionQuery = () => typeof matchMedia === "undefined" ? null : matchMedia("(prefers-reduced-motion: reduce)");
30
- const subscribeMotion = (listener) => {
31
- const query = motionQuery();
32
- query?.addEventListener("change", listener);
33
- return () => query?.removeEventListener("change", listener);
34
- };
35
- function useMotionAllowed() {
36
- return useSyncExternalStore(subscribeMotion, () => motionQuery()?.matches === false, () => false);
37
- }
38
- function tokenMilliseconds(token, element) {
39
- if (typeof document === "undefined") return 0;
40
- const value = getComputedStyle(element ?? document.documentElement).getPropertyValue(token).trim();
41
- return (parseFloat(value) || 0) * (value.endsWith("ms") ? 1 : 1e3);
42
- }
43
- const ambientStyle = { animation: "tangle-breathe var(--motion-ambient) infinite" };
44
- /** The document timeline is the page's epoch, including marks mounted later. */
45
- function syncAmbient(event) {
46
- for (const animation of event.currentTarget.getAnimations()) if (animation.animationName === "tangle-breathe") animation.startTime = 0;
47
- }
48
- function activityTiming(agent) {
49
- const times = Number.isFinite(agent.lastActivityAt) && Number.isFinite(agent.medianEventGapMs) && agent.medianEventGapMs >= 0 ? [] : (agent.events ?? []).flatMap((e) => e.at != null && Number.isFinite(e.at) ? [e.at] : []).sort((a, b) => a - b);
50
- const last = Number.isFinite(agent.lastActivityAt) ? agent.lastActivityAt : times.at(-1);
51
- const gaps = times.slice(1).map((time, i) => time - times[i]).sort((a, b) => a - b);
52
- const middle = Math.floor(gaps.length / 2);
53
- return {
54
- lastActivityAt: last,
55
- medianEventGapMs: Number.isFinite(agent.medianEventGapMs) && agent.medianEventGapMs >= 0 ? agent.medianEventGapMs : gaps.length ? gaps.length % 2 ? gaps[middle] : (gaps[middle - 1] + gaps[middle]) / 2 : 0
56
- };
57
- }
58
- function quietActivity(agent, now) {
59
- if (agent.status !== "running") return null;
60
- const { lastActivityAt, medianEventGapMs } = activityTiming(agent);
61
- if (lastActivityAt == null || !Number.isFinite(lastActivityAt)) return null;
62
- const quietMs = Math.max(0, now - lastActivityAt);
63
- return quietMs > Math.max(3e5, 3 * medianEventGapMs) ? {
64
- quietMs,
65
- label: `Quiet ${Math.floor(quietMs / 6e4)}m`
66
- } : null;
67
- }
68
- /** Attention is finite and state changes work without animation completion. */
69
- function useAttention(ref, status) {
70
- const allowed = useMotionAllowed();
71
- useEffect(() => {
72
- const element = ref.current;
73
- if (!element || !allowed || status !== "needs-you" && status !== "failed") return;
74
- const token = statusMeta(status).token;
75
- const duration = tokenMilliseconds("--duration-ambient", element) * (status === "needs-you" ? .45 : .3);
76
- element.dataset.attn = status;
77
- const shadow = getComputedStyle(element).boxShadow;
78
- const resting = shadow === "none" ? "" : `, ${shadow}`;
79
- const animation = element.animate([{ boxShadow: `0 0 0 0 color-mix(in srgb, var(${token}) 40%, transparent)${resting}` }, { boxShadow: `0 0 0 6px transparent${resting}` }], {
80
- duration,
81
- iterations: status === "needs-you" ? 2 : 1,
82
- easing: getComputedStyle(element).getPropertyValue("--ease-exit").trim() || "ease-out"
83
- });
84
- const end = setTimeout(() => {
85
- delete element.dataset.attn;
86
- }, duration * (status === "needs-you" ? 2 : 1));
87
- return () => {
88
- animation.cancel();
89
- clearTimeout(end);
90
- delete element.dataset.attn;
91
- };
92
- }, [
93
- ref,
94
- status,
95
- allowed
96
- ]);
97
- }
98
- // src/workflows/agent-hub-css.ts
99
- /** Hub styles consume Brand's status, elevation, radius and motion roles. */
100
- const AGENT_HUB_CSS = `
101
- .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);
102
- color: var(--ag-fg); font-size: 12px; line-height: 16px; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
103
- .ag-root * { box-sizing: border-box; }
104
- .ag-head, .ag-title, .ag-chip, .ag-legend li, .ag-pop-head, .ag-pop-foot { display: flex; align-items: center; gap: 8px; }
105
- .ag-head { flex-wrap: wrap; flex-shrink: 0; min-height: 40px; }
106
- .ag-title, .ag-pop-name { font-weight: 600; font-size: 14px; line-height: 20px; }
107
- .ag-title-mark { width: 6px; height: 6px; border-radius: var(--radius-pill); background: var(--ag-accent); }
108
- .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); }
109
- .ag-chip { cursor: pointer; }
110
- .ag-chip-needs-you, .ag-pill-needs-you { color: var(--surface-warning-text); }
111
- .ag-chip-failed, .ag-pill-failed { color: var(--surface-danger-text); }
112
- .ag-pill-succeeded { color: var(--surface-success-text); }
113
- .ag-stats { margin-left: auto; display: flex; gap: 12px; flex-wrap: wrap; color: var(--ag-muted); font-variant-numeric: tabular-nums; }
114
- .ag-stats b, .ag-pop-call b, .ag-pop-foot b { color: var(--ag-fg); font-weight: 500; }
115
- .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; }
116
- .ag-canvas { position: absolute; inset: 0 auto auto 0; transform-origin: 0 0; }
117
- .ag-edges { position: absolute; inset: 0; overflow: visible; }
118
- .ag-edge { fill: none; stroke: color-mix(in srgb, var(--ag-muted) 80%, transparent); stroke-width: 1.25; }
119
- .ag-edge-live, .ag-flow { fill: none; stroke-linecap: round; }
120
- .ag-edge-live { stroke-width: 1.25; }
121
- .ag-flow { stroke-width: 2; }
122
- .ag-root g, .ag-node { transition: opacity var(--motion-control); }
123
- .ag-dimmed .ag-tile { opacity: .45; } .ag-edges .ag-dimmed { opacity: .32; }
124
- .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; }
125
- .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); }
126
- .ag-node:hover .ag-tile { box-shadow: var(--shadow-dropdown); }
127
- .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; }
128
- .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; }
129
- .ag-node[style*="--ag-tone"] .ag-tile { border: 1.5px solid var(--ag-tone); }
130
- .ag-node-you .ag-tile { background: var(--ag-fg); color: var(--ag-surface); }
131
- .ag-name, .ag-sub { max-width: 176px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
132
- .ag-name { margin-top: 6px; font-size: 14px; font-weight: 600; line-height: 20px; }
133
- .ag-sub { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; white-space: normal; }
134
- .ag-node-tool .ag-name { max-width: 132px; }
135
- .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; }
136
- .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); }
137
- .ag-root [data-activity="quiet"] .ag-running-dot { animation: none !important; }
138
- .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; }
139
- .ag-legend { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; color: var(--ag-muted); }
140
- .ag-legend-line { width: 14px; height: 2px; border-radius: var(--radius-pill); }
141
- .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: agent-arrive var(--motion-arrive); }
142
- .ag-tile-mini { border-radius: var(--radius-control); box-shadow: none; }
143
- .ag-tile-micro { border: 0; border-radius: var(--radius-tag); background: none; box-shadow: none; }
144
- .ag-pop-title { display: flex; flex-direction: column; min-width: 0; flex: 1; }
145
- .ag-pop-sub, .ag-pop-call { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ag-muted); }
146
- .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; }
147
- .ag-close:hover { background: var(--bg-hover); color: var(--ag-fg); }
148
- .ag-pop-summary { overflow-wrap: anywhere; margin: 8px 0 0; font-size: 14px; line-height: 20px; }
149
- .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; }
150
- .ag-pop-calls li { display: flex; align-items: center; gap: 6px; min-width: 0; }
151
- .ag-pop-dir, .ag-pop-time { color: var(--ag-muted); flex-shrink: 0; }
152
- .ag-pop-call { flex: 1; min-width: 0; }
153
- .ag-pop-time { font-variant-numeric: tabular-nums; display: inline-flex; }
154
- .ag-pop-foot { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--ag-border); color: var(--ag-muted); }
155
- .ag-mobile { list-style: none; margin: 0; padding: 8px; display: grid; gap: 12px; }
156
- .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; }
157
- .ag-mobile .ag-tile { grid-row: span 2; }
158
- .ag-mobile .ag-name, .ag-mobile .ag-sub { max-width: 100%; width: 100%; margin: 0; }
159
- .ag-mobile .ag-sub { display: block; white-space: nowrap; }
160
- .ag-tools { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 0 44px; }
161
- .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; }
162
- .ag-available { padding: 8px; border-top: 1px solid var(--ag-border); color: var(--ag-muted); } .ag-available .ag-tools { margin: 8px 0 0; }
163
- .ag-list-mode .ag-popover { overflow: auto; position: relative; left: auto !important; top: auto !important; width: auto; margin: 8px; }
164
- @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; } }
165
- `;
166
- const HUB_METRICS = {
167
- cellX: 184,
168
- cellY: 140,
169
- tile: 48,
170
- hubTile: 60,
171
- label: 34
172
- };
173
- const key = (col, row) => `${col},${row}`;
174
- /** Ring cells around the hub, nearest first: the diagonals, then the sides, then ring two by angle. */
175
- function ringCells(count) {
176
- const cells = [
177
- [-1, -1],
178
- [1, -1],
179
- [-1, 1],
180
- [1, 1],
181
- [-1, 0],
182
- [1, 0],
183
- [0, 1],
184
- [0, -1]
185
- ];
186
- for (let ring = 2; cells.length < count; ring++) {
187
- const outer = [];
188
- 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]);
189
- outer.sort((a, b) => Math.atan2(a[1], a[0]) - Math.atan2(b[1], b[0]));
190
- cells.push(...outer);
191
- }
192
- return cells.slice(0, count);
193
- }
194
- /** The orchestrator: the agent nobody spawned, else the first. */
195
- function hubAgentId(nodes, edges) {
196
- const spawned = new Set(edges.filter((e) => e.kind === "spawn").map((e) => e.to));
197
- return (nodes.find((n) => !spawned.has(n.id)) ?? nodes[0])?.id;
198
- }
199
- /**
200
- * Place every node on the grid. Agents take ring cells in order; each tool takes
201
- * the first free cell outward from the first agent that called it (beside, then
202
- * above or below, then diagonal), so it sits next to its user.
203
- */
204
- function layoutHub(input) {
205
- const { nodes, edges, tools, calls, you } = input;
206
- const hub = hubAgentId(nodes, edges);
207
- const placements = [];
208
- const taken = /* @__PURE__ */ new Set();
209
- const place = (id, kind, col, row) => {
210
- placements.push({
211
- id,
212
- kind,
213
- col,
214
- row
215
- });
216
- taken.add(key(col, row));
217
- };
218
- if (hub) place(hub, "hub", 0, 0);
219
- const agents = nodes.filter((n) => n.id !== hub);
220
- ringCells(agents.length).forEach(([c, r], i) => place(agents[i].id, "agent", c, r));
221
- const ring = Math.max(1, ...placements.map((p) => Math.max(Math.abs(p.col), Math.abs(p.row))));
222
- if (you) {
223
- const col = -Math.min(ring, 1);
224
- let row = -ring - 1;
225
- while (taken.has(key(col, row))) row--;
226
- place("you", "you", col, row);
227
- }
228
- const cell = new Map(placements.map((p) => [p.id, p]));
229
- const userOf = /* @__PURE__ */ new Map();
230
- for (const call of calls) {
231
- const toolId = tools.some((t) => t.id === call.to) ? call.to : tools.some((t) => t.id === call.from) ? call.from : void 0;
232
- const agentId = toolId === call.to ? call.from : call.to;
233
- if (toolId && cell.has(agentId) && !userOf.has(toolId)) userOf.set(toolId, agentId);
234
- }
235
- for (const tool of tools) {
236
- const owner = cell.get(userOf.get(tool.id) ?? hub ?? "");
237
- const ac = owner?.col ?? 0, ar = owner?.row ?? 0;
238
- const sx = Math.sign(ac) || 1, sy = Math.sign(ar) || 1;
239
- const options = ac === 0 ? [
240
- [0, sy],
241
- [sx, sy],
242
- [-sx, sy],
243
- [sx, 0]
244
- ] : ar === 0 ? [
245
- [sx, 0],
246
- [sx, -1],
247
- [sx, 1],
248
- [0, -1]
249
- ] : [
250
- [sx, 0],
251
- [0, sy],
252
- [sx, sy],
253
- [-sx, sy],
254
- [sx, -sy]
255
- ];
256
- let spot;
257
- for (let step = 1; !spot && step < 6; step++) for (const [dx, dy] of options) {
258
- const c = ac + dx * step, r = ar + dy * step;
259
- if (!taken.has(key(c, r))) {
260
- spot = [c, r];
261
- break;
262
- }
263
- }
264
- place(tool.id, "tool", spot[0], spot[1]);
265
- cell.set(tool.id, placements.at(-1));
266
- }
267
- const links = [];
268
- const seen = /* @__PURE__ */ new Set();
269
- const link = (from, to) => {
270
- const k = [from, to].sort().join("|");
271
- if (from === to || seen.has(k) || !taken.size) return;
272
- seen.add(k);
273
- links.push({
274
- from,
275
- to
276
- });
277
- };
278
- if (you && hub) link("you", hub);
279
- for (const agent of agents) {
280
- const parent = edges.find((e) => e.kind === "spawn" && e.to === agent.id)?.from;
281
- link(parent && cell.has(parent) ? parent : hub, agent.id);
282
- }
283
- for (const call of calls) if (cell.has(call.from) && cell.has(call.to)) link(call.from, call.to);
284
- return {
285
- placements,
286
- links,
287
- minCol: Math.min(...placements.map((p) => p.col)),
288
- maxCol: Math.max(...placements.map((p) => p.col)),
289
- minRow: Math.min(...placements.map((p) => p.row)),
290
- maxRow: Math.max(...placements.map((p) => p.row))
291
- };
292
- }
293
- /**
294
- * A short rounded Manhattan route around tile and label bounds. Departure lanes
295
- * separate parallel calls; the path length keeps finite trips at a consistent speed.
296
- */
297
- function orthogonalPath(a, b, verticalFirst, lane = 0, radius = 10, obstacles = []) {
298
- 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);
299
- const xs = [.../* @__PURE__ */ new Set([
300
- a.x + lane,
301
- (a.x + b.x) / 2,
302
- ...obstacles.flatMap((rect) => [rect.left - 8, rect.right + 8])
303
- ])];
304
- const ys = [.../* @__PURE__ */ new Set([
305
- a.y + lane,
306
- (a.y + b.y) / 2,
307
- ...obstacles.flatMap((rect) => [rect.top - 8, rect.bottom + 8])
308
- ])];
309
- const candidates = [[
310
- a,
311
- {
312
- x: a.x,
313
- y: b.y
314
- },
315
- b
316
- ], [
317
- a,
318
- {
319
- x: b.x,
320
- y: a.y
321
- },
322
- b
323
- ]];
324
- for (const x of xs) candidates.push([
325
- a,
326
- {
327
- x,
328
- y: a.y
329
- },
330
- {
331
- x,
332
- y: b.y
333
- },
334
- b
335
- ]);
336
- for (const y of ys) candidates.push([
337
- a,
338
- {
339
- x: a.x,
340
- y
341
- },
342
- {
343
- x: b.x,
344
- y
345
- },
346
- b
347
- ]);
348
- for (const x of xs) for (const y of ys) candidates.push([
349
- a,
350
- {
351
- x: a.x,
352
- y
353
- },
354
- {
355
- x,
356
- y
357
- },
358
- {
359
- x,
360
- y: b.y
361
- },
362
- b
363
- ], [
364
- a,
365
- {
366
- x,
367
- y: a.y
368
- },
369
- {
370
- x,
371
- y
372
- },
373
- {
374
- x: b.x,
375
- y
376
- },
377
- b
378
- ]);
379
- let points = candidates[verticalFirst ? 0 : 1], best = Infinity;
380
- for (const candidate of candidates) {
381
- const path = candidate.filter((p, i) => !i || p.x !== candidate[i - 1].x || p.y !== candidate[i - 1].y);
382
- if (!path.slice(1).every((point, i) => clear(path[i], point))) continue;
383
- 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);
384
- const trunk = path.find((point, i) => i && point.y !== path[i - 1].y)?.x ?? a.x;
385
- const score = distance + path.length + (lane ? Math.abs(trunk - a.x - lane) : 0);
386
- if (score < best) {
387
- best = score;
388
- points = path;
389
- }
390
- }
391
- 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));
392
- let d = `M${a.x},${a.y}`, length = 0;
393
- for (let i = 1; i < points.length; i++) {
394
- const previous = points[i - 1], point = points[i], next = points[i + 1];
395
- const incoming = Math.hypot(point.x - previous.x, point.y - previous.y);
396
- length += incoming;
397
- if (!next) {
398
- d += ` L${point.x},${point.y}`;
399
- continue;
400
- }
401
- const outgoing = Math.hypot(next.x - point.x, next.y - point.y), r = Math.min(radius, incoming / 2, outgoing / 2);
402
- 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}`;
403
- length -= 2 * r - Math.PI * r / 2;
404
- }
405
- return {
406
- d,
407
- length
408
- };
409
- }
410
- // src/workflows/format.ts
411
- /**
412
- * Display formatters for the workflow graph's live run-state chips. Pure and
413
- * dependency-free so they can be unit-tested without rendering the graph.
414
- */
415
- /** Human-readable duration: `850ms`, `4.2s`, `1m30s`. Undefined for an absent,
416
- * non-finite, or negative input (e.g. clock skew) so the caller renders no
417
- * chip rather than a confusing `-5ms`. */
418
- function fmtDuration(ms) {
419
- if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
420
- if (ms < 1e3) return `${Math.round(ms)}ms`;
421
- const s = ms / 1e3;
422
- if (s < 60) return `${s.toFixed(1)}s`;
423
- const totalSeconds = Math.round(s);
424
- return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
425
- }
426
- /** Human-readable USD cost: `$0`, `$0.0032` (sub-cent, 4dp), `$1.20`. Undefined
427
- * for an absent, non-finite, or negative input — a cost is a billing amount, so
428
- * a negative value is bad data, not a refund to render. */
429
- function fmtCost(usd) {
430
- if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
431
- if (usd === 0) return "$0";
432
- return usd < .01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
433
- }
434
- /** Bound host-supplied preview/error text to a fixed length (with an ellipsis)
435
- * before it reaches the DOM, so an oversized payload can't bloat the node tree —
436
- * the card only ever shows a short preview, and CSS clamping is visual only.
437
- * Truncation is code-point aware: it never slices through a surrogate pair (which
438
- * would leave a lone surrogate that renders as the replacement character). */
439
- function clampPreview(text, max = 200) {
440
- if (text.length <= max) return text;
441
- const lastCode = text.charCodeAt(max - 1);
442
- const end = lastCode >= 55296 && lastCode <= 56319 ? max - 1 : max;
443
- return `${text.slice(0, end)}…`;
444
- }
445
- /** Human-readable token usage: `1200/340 tok`. Undefined when neither side is a
446
- * finite, non-negative count, so the caller renders no chip; an invalid side
447
- * (negative / NaN / absent) shows as 0 rather than leaking `-5/NaN tok`. */
448
- function fmtTokens(input, output) {
449
- const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
450
- const i = valid(input);
451
- const o = valid(output);
452
- if (i === void 0 && o === void 0) return void 0;
453
- return `${i ?? 0}/${o ?? 0} tok`;
454
- }
455
- /** A token total in a few characters: `840 tok`, `12.4k tok`, `1.2M tok`.
456
- * Undefined for an absent, non-finite or negative count. */
457
- function fmtTokenTotal(tokens) {
458
- if (tokens == null || !Number.isFinite(tokens) || tokens < 0) return void 0;
459
- if (tokens < 1e3) return `${Math.round(tokens)} tok`;
460
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(tokens < 1e4 ? 1 : 0)}k tok`;
461
- return `${(tokens / 1e6).toFixed(1)}M tok`;
462
- }
463
- function fmtEventTime(at, now, frozen = false) {
464
- if (at == null || !Number.isFinite(at)) return "—";
465
- if (frozen) return new Date(at).toLocaleTimeString([], {
466
- hour: "2-digit",
467
- minute: "2-digit",
468
- hourCycle: "h23"
469
- });
470
- const seconds = Math.max(0, Math.floor((now - at) / 1e3));
471
- return seconds < 5 ? "now" : seconds < 60 ? `${seconds}s` : seconds < 3600 ? `${Math.floor(seconds / 60)}m` : `${Math.floor(seconds / 3600)}h`;
472
- }
473
- // src/workflows/agent-hub-parts.tsx
474
- /** Hub nodes and call details share the run status vocabulary. */
475
- /** One colour per call kind, shared by edges, the legend and popover rows. */
476
- const CALL_COLOR = {
477
- call: "var(--brand-primary)",
478
- answer: `var(${statusMeta("done").token})`,
479
- "needs-you": `var(${statusMeta("needs-you").token})`,
480
- failed: `var(${statusMeta("failed").token})`
481
- };
482
- const CALL_LABEL = {
483
- call: "Call",
484
- answer: "Answer",
485
- "needs-you": statusMeta("needs-you").label,
486
- failed: statusMeta("failed").label
487
- };
488
- function NodeIcon({ entity, size }) {
489
- if (entity.kind === "you") return /* @__PURE__ */ jsx(User, {
490
- size: size * .42,
491
- strokeWidth: 2.2,
492
- "aria-hidden": true
493
- });
494
- if (entity.kind === "tool") {
495
- const provider = entity.tool?.provider;
496
- if (!provider) return /* @__PURE__ */ jsx(Wrench, {
497
- size: size * .4,
498
- "aria-hidden": true
499
- });
500
- return /* @__PURE__ */ jsx(ProviderIcon, {
501
- id: provider,
502
- displayName: entity.label,
503
- size: Math.round(size * .46),
504
- className: "ag-model-mark"
505
- });
506
- }
507
- const brand = entity.agent?.model ? modelBrandFor(entity.agent.model) : null;
508
- return brand ? /* @__PURE__ */ jsx("span", {
509
- className: "ag-model-mark",
510
- children: /* @__PURE__ */ jsx(ModelBrandStack, {
511
- identity: brand,
512
- size: "sm"
513
- })
514
- }) : /* @__PURE__ */ jsx(Bot, {
515
- size: 16,
516
- "aria-hidden": true
517
- });
518
- }
519
- /** Running has one quiet opacity signal; all other states use the shared glyph. */
520
- function StatusBadge({ status, quiet }) {
521
- if (!status) return null;
522
- return /* @__PURE__ */ jsx("span", {
523
- className: "ag-badge",
524
- children: runStatus(status) === "running" ? /* @__PURE__ */ jsx("span", {
525
- className: "ag-running-dot",
526
- "data-motion": "essential",
527
- "aria-label": "Running",
528
- style: quiet ? {
529
- animation: "none",
530
- background: "var(--ag-muted)"
531
- } : ambientStyle,
532
- onAnimationStart: syncAmbient
533
- }) : /* @__PURE__ */ jsx(RunStatusIcon, { status })
534
- });
535
- }
536
- function HubNode({ entity, x, y, tile, dimmed, selected, buttonRef, onOpen, onHover, onKeyDown }) {
537
- const attentionRef = useRef(null);
538
- useAttention(attentionRef, entity.status ?? "unknown");
539
- const tone = entity.status === "needs-you" || entity.status === "failed" ? `var(${statusMeta(entity.status).token})` : void 0;
540
- 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);
541
- return /* @__PURE__ */ jsxs("button", {
542
- ref: (el) => {
543
- attentionRef.current = el;
544
- if (typeof buttonRef === "function") buttonRef(el);
545
- else if (buttonRef) buttonRef.current = el;
546
- },
547
- type: "button",
548
- "data-agent-graph-node": entity.id,
549
- "data-kind": entity.kind,
550
- "data-status": entity.status,
551
- "data-activity": entity.quiet ? "quiet" : void 0,
552
- "aria-label": `${entity.label}${entity.sub ? `, ${entity.sub}` : ""}${entity.status ? `, ${entity.status === "needs-you" ? "needs you" : entity.status}` : ""}`,
553
- "aria-expanded": selected,
554
- className: `ag-node ag-node-${entity.kind}${dimmed ? " ag-dimmed" : ""}${selected ? " ag-selected" : ""}`,
555
- style: {
556
- left: x,
557
- top: y,
558
- "--ag-tile": `${tile}px`,
559
- ...tone ? { "--ag-tone": tone } : {}
560
- },
561
- onClick: onOpen,
562
- onPointerEnter: () => onHover(true),
563
- onPointerLeave: () => onHover(false),
564
- onFocus: () => onHover(true),
565
- onBlur: () => onHover(false),
566
- onKeyDown,
567
- children: [
568
- /* @__PURE__ */ jsxs("span", {
569
- className: "ag-tile",
570
- children: [/* @__PURE__ */ jsx(NodeIcon, {
571
- entity,
572
- size: tile
573
- }), /* @__PURE__ */ jsx(StatusBadge, {
574
- status: entity.status,
575
- quiet: entity.quiet
576
- })]
577
- }),
578
- /* @__PURE__ */ jsx("span", {
579
- className: "ag-name",
580
- children: entity.label
581
- }),
582
- sub && /* @__PURE__ */ jsx("span", {
583
- className: "ag-sub",
584
- style: tone ? { color: tone } : void 0,
585
- children: entity.quiet ? /* @__PURE__ */ jsxs(Fragment, { children: [
586
- statusMeta(entity.status ?? "running").label,
587
- " · ",
588
- /* @__PURE__ */ jsx("span", {
589
- style: { color: "var(--surface-warning-text)" },
590
- children: sub
591
- })
592
- ] }) : sub
593
- })
594
- ]
595
- });
596
- }
597
- const fmtMs = (ms) => ms < 1e3 ? `${Math.round(ms / 100) * 100 || Math.round(ms)}ms` : `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
598
- /** A node's recent calls: who it called or was called by, what for, how long, and what failed. */
599
- function HubPopover({ entity, entities, calls, now, left, top, summary, closeRef, onClose }) {
600
- const element = useRef(null);
601
- const [fitted, setFitted] = useState({
602
- left,
603
- top
604
- });
605
- useLayoutEffect(() => {
606
- const dialog = element.current, container = dialog?.parentElement;
607
- if (!dialog || !container) return;
608
- const fit = () => {
609
- const relative = getComputedStyle(dialog).position !== "absolute";
610
- const maxHeight = Math.max(0, container.clientHeight - 16);
611
- const height = Math.min(dialog.scrollHeight + dialog.offsetHeight - dialog.clientHeight, maxHeight);
612
- const next = relative ? {
613
- left,
614
- top
615
- } : {
616
- left: Math.max(container.scrollLeft + 8, Math.min(left, container.scrollLeft + container.clientWidth - dialog.offsetWidth - 8)),
617
- top: Math.max(container.scrollTop + 8, Math.min(top, container.scrollTop + container.clientHeight - height - 8)),
618
- maxHeight
619
- };
620
- setFitted((previous) => previous.left === next.left && previous.top === next.top && previous.maxHeight === next.maxHeight ? previous : next);
621
- };
622
- fit();
623
- const observer = new ResizeObserver(fit);
624
- observer.observe(dialog);
625
- observer.observe(container);
626
- return () => observer.disconnect();
627
- }, [left, top]);
628
- const mine = (calls ?? []).filter((c) => (c.from === entity.id || c.to === entity.id) && c.startedAt <= now).sort((a, b) => b.startedAt - a.startedAt);
629
- const recent = mine.slice(0, 5);
630
- const lastMinute = mine.filter((c) => now - c.startedAt <= 6e4).length;
631
- const status = entity.status ? statusMeta(entity.status).label : void 0;
632
- const cost = entity.agent?.costUsd;
633
- return /* @__PURE__ */ jsxs("div", {
634
- ref: element,
635
- role: "dialog",
636
- "aria-label": `${entity.label} calls`,
637
- className: "ag-popover",
638
- style: fitted,
639
- onKeyDown: (event) => {
640
- if (event.key === "Escape") {
641
- event.stopPropagation();
642
- onClose();
643
- }
644
- },
645
- children: [
646
- /* @__PURE__ */ jsxs("div", {
647
- className: "ag-pop-head",
648
- children: [
649
- /* @__PURE__ */ jsx("span", {
650
- className: "ag-tile ag-tile-mini",
651
- style: { "--ag-tile": "28px" },
652
- children: /* @__PURE__ */ jsx(NodeIcon, {
653
- entity,
654
- size: 28
655
- })
656
- }),
657
- /* @__PURE__ */ jsxs("span", {
658
- className: "ag-pop-title",
659
- children: [/* @__PURE__ */ jsx("span", {
660
- className: "ag-pop-name",
661
- children: entity.label
662
- }), (entity.agent?.model || entity.sub || entity.tool?.detail) && /* @__PURE__ */ jsx("span", {
663
- className: "ag-pop-sub",
664
- children: entity.agent?.model ?? entity.tool?.detail ?? entity.sub
665
- })]
666
- }),
667
- status && /* @__PURE__ */ jsxs("span", {
668
- className: `ag-pill ag-pill-${entity.status}`,
669
- children: [entity.status === "running" && !entity.quiet ? /* @__PURE__ */ jsx("span", {
670
- className: "ag-running-dot",
671
- "data-motion": "essential",
672
- style: ambientStyle,
673
- onAnimationStart: syncAmbient
674
- }) : /* @__PURE__ */ jsx(RunStatusIcon, {
675
- status: entity.quiet ? "waiting" : entity.status,
676
- "aria-hidden": true
677
- }), entity.quiet ?? status]
678
- }),
679
- /* @__PURE__ */ jsx("button", {
680
- ref: closeRef,
681
- type: "button",
682
- className: "ag-close",
683
- "aria-label": "Close",
684
- onClick: onClose,
685
- children: /* @__PURE__ */ jsx(X, { size: 14 })
686
- })
687
- ]
688
- }),
689
- summary && /* @__PURE__ */ jsx("p", {
690
- className: "ag-pop-summary",
691
- children: summary
692
- }),
693
- recent.length > 0 && /* @__PURE__ */ jsx("ul", {
694
- className: "ag-pop-calls",
695
- children: recent.map((c) => {
696
- const outgoing = c.from === entity.id;
697
- const other = entities.get(outgoing ? c.to : c.from);
698
- const running = c.durationMs == null;
699
- return /* @__PURE__ */ jsxs("li", { children: [
700
- /* @__PURE__ */ jsx("span", {
701
- className: "ag-pop-dir",
702
- "aria-label": outgoing ? "to" : "from",
703
- children: outgoing ? "→" : "←"
704
- }),
705
- other && /* @__PURE__ */ jsx("span", {
706
- className: "ag-tile ag-tile-micro",
707
- style: { "--ag-tile": "16px" },
708
- children: /* @__PURE__ */ jsx(NodeIcon, {
709
- entity: other,
710
- size: 16
711
- })
712
- }),
713
- /* @__PURE__ */ jsxs("span", {
714
- className: "ag-pop-call",
715
- children: [/* @__PURE__ */ jsx("b", { children: other?.label ?? (outgoing ? c.to : c.from) }), c.label ? ` · ${c.label}` : ""]
716
- }),
717
- /* @__PURE__ */ jsx("span", {
718
- className: "ag-pop-time",
719
- style: c.kind === "failed" ? { color: CALL_COLOR.failed } : void 0,
720
- children: c.kind === "failed" ? "Failed" : running ? /* @__PURE__ */ jsx("span", {
721
- className: "ag-running-dot",
722
- "data-motion": "essential",
723
- "aria-label": "Running",
724
- style: ambientStyle,
725
- onAnimationStart: syncAmbient
726
- }) : fmtMs(c.durationMs)
727
- })
728
- ] }, c.id);
729
- })
730
- }),
731
- /* @__PURE__ */ jsxs("p", {
732
- className: "ag-pop-foot",
733
- children: [cost != null && /* @__PURE__ */ jsx("b", { children: fmtCost(cost) }), /* @__PURE__ */ jsxs("span", {
734
- title: calls === void 0 ? "Call history not reported" : void 0,
735
- children: [
736
- calls === void 0 ? "—" : lastMinute,
737
- " ",
738
- lastMinute === 1 ? "call" : "calls",
739
- " in the last minute"
740
- ]
741
- })]
742
- })
743
- ]
744
- });
745
- }
746
- // src/workflows/AgentHubCanvas.tsx
747
- /** The hub presents agent and tool calls on a stable canvas or a readable list. */
748
- const PAD_X = 84;
749
- const PAD_Y = 36;
750
- function useWidth(ref) {
751
- const [width, setWidth] = useState(0);
752
- useLayoutEffect(() => {
753
- const el = ref.current;
754
- if (!el) return;
755
- setWidth(el.clientWidth);
756
- const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
757
- observer.observe(el);
758
- return () => observer.disconnect();
759
- }, [ref]);
760
- return width;
761
- }
762
- function CallStroke({ call, d, length, now, forward, animate }) {
763
- const ref = useRef(null);
764
- const routeRef = useRef(null);
765
- const [chevron, setChevron] = useState();
766
- const end = call.durationMs == null ? Infinity : call.startedAt + call.durationMs;
767
- const returning = now >= end;
768
- useLayoutEffect(() => {
769
- const path = routeRef.current;
770
- if (!path) return;
771
- const middle = path.getTotalLength() / 2, point = path.getPointAtLength(middle), before = path.getPointAtLength(middle - 1), after = path.getPointAtLength(middle + 1);
772
- const ambient = tokenMilliseconds("--duration-ambient", path);
773
- setChevron({
774
- x: point.x,
775
- y: point.y,
776
- angle: Math.atan2(after.y - before.y, after.x - before.x) * 180 / Math.PI + (forward === returning ? 180 : 0),
777
- travel: Math.min(ambient * .9, Math.max(ambient * .3, length / 320 * 1e3)),
778
- decay: ambient * 4
779
- });
780
- }, [
781
- d,
782
- length,
783
- forward,
784
- returning
785
- ]);
786
- useEffect(() => {
787
- const el = ref.current;
788
- const duration = chevron?.travel;
789
- if (!el || !animate || call.kind === "failed" || !duration) return;
790
- const age = now - (returning ? end : call.startedAt);
791
- if (age > duration) return;
792
- const reverse = forward === returning;
793
- const animation = el.animate([{
794
- strokeDashoffset: reverse ? -length : 24,
795
- opacity: .35
796
- }, {
797
- strokeDashoffset: reverse ? 24 : -length,
798
- opacity: .35
799
- }], {
800
- duration,
801
- easing: "linear"
802
- });
803
- animation.currentTime = Math.max(0, age);
804
- return () => animation.cancel();
805
- }, [
806
- call.id,
807
- returning,
808
- animate,
809
- length,
810
- forward,
811
- chevron?.travel
812
- ]);
813
- const age = Math.max(0, now - end);
814
- const tint = returning ? Math.max(0, .55 * (1 - age / (chevron?.decay || 1))) : .55;
815
- return /* @__PURE__ */ jsxs("g", {
816
- "data-call-id": call.id,
817
- "data-direction": forward !== returning ? "forward" : "return",
818
- style: { stroke: CALL_COLOR[call.kind] },
819
- children: [
820
- /* @__PURE__ */ jsx("path", {
821
- ref: routeRef,
822
- d,
823
- className: "ag-edge-live",
824
- opacity: tint
825
- }),
826
- chevron && /* @__PURE__ */ jsx("path", {
827
- "data-call-arrow": true,
828
- opacity: returning ? tint / .55 : 1,
829
- d: "M-3,-3 L1,0 L-3,3",
830
- fill: "none",
831
- strokeWidth: 1.5,
832
- strokeLinecap: "round",
833
- strokeLinejoin: "round",
834
- transform: `translate(${chevron.x},${chevron.y}) rotate(${chevron.angle})`
835
- }),
836
- animate && /* @__PURE__ */ jsx("path", {
837
- ref,
838
- d,
839
- className: "ag-flow",
840
- strokeDasharray: `24 ${length}`,
841
- opacity: 0
842
- })
843
- ]
844
- });
845
- }
846
- function AgentHubCanvas(props) {
847
- const { nodes, edges, tools = [], calls = [], you, now: nowProp, title = "Agent graph", selectedId, onSelect, className } = props;
848
- const wrap = useRef(null);
849
- const width = useWidth(wrap);
850
- const now = useRunNow(nowProp);
851
- const [hover, setHover] = useState(null);
852
- const [open, setOpen] = useState(selectedId ?? null);
853
- const buttons = useRef(/* @__PURE__ */ new Map());
854
- const closeRef = useRef(null);
855
- const animate = useMotionAllowed();
856
- const visibleCalls = useMemo(() => calls.filter((c) => c.startedAt <= now), [calls, now]);
857
- const nextLayout = useMemo(() => layoutHub({
858
- nodes,
859
- edges,
860
- tools,
861
- calls,
862
- you: Boolean(you)
863
- }), [
864
- nodes,
865
- edges,
866
- tools,
867
- calls,
868
- you
869
- ]);
870
- const layoutKey = JSON.stringify(nextLayout);
871
- const layoutRef = useRef({
872
- key: layoutKey,
873
- layout: nextLayout
874
- });
875
- if (layoutRef.current.key !== layoutKey) layoutRef.current = {
876
- key: layoutKey,
877
- layout: nextLayout
878
- };
879
- const layout = layoutRef.current.layout;
880
- const callSummaries = useMemo(() => {
881
- const summaries = {
882
- call: /* @__PURE__ */ new Map(),
883
- answer: /* @__PURE__ */ new Map(),
884
- "needs-you": /* @__PURE__ */ new Map()
885
- };
886
- for (const call of visibleCalls) if (call.kind !== "failed") {
887
- const bucket = summaries[call.kind], id = call.kind === "call" ? call.to : call.from;
888
- if (!bucket.has(id) || bucket.get(id).startedAt < call.startedAt) bucket.set(id, call);
889
- }
890
- return summaries;
891
- }, [visibleCalls]);
892
- const span = (m) => ({
893
- w: (layout.maxCol - layout.minCol) * m.cellX + 168,
894
- h: (layout.maxRow - layout.minRow) * m.cellY + 72 + m.hubTile + m.label
895
- });
896
- const metrics = HUB_METRICS;
897
- const { w, h } = span(metrics);
898
- const list = width > 0 && (width < 600 || width / w < .85);
899
- const scale = width ? Math.max(.85, Math.min(1, width / w)) : 1;
900
- const entities = useMemo(() => {
901
- const map = /* @__PURE__ */ new Map();
902
- for (const p of layout.placements) if (p.kind === "you") map.set(p.id, {
903
- id: p.id,
904
- kind: "you",
905
- label: typeof you === "object" && you.label ? you.label : "You"
906
- });
907
- else if (p.kind === "tool") {
908
- const tool = tools.find((t) => t.id === p.id);
909
- map.set(p.id, {
910
- id: p.id,
911
- kind: "tool",
912
- label: tool.label,
913
- tool
914
- });
915
- } else {
916
- const agent = nodes.find((n) => n.id === p.id);
917
- map.set(p.id, {
918
- id: p.id,
919
- kind: p.kind,
920
- label: agent.label,
921
- 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,
922
- status: agent.status,
923
- quiet: quietActivity(agent, now)?.label,
924
- agent
925
- });
926
- }
927
- return map;
928
- }, [
929
- layout,
930
- nodes,
931
- tools,
932
- you,
933
- now,
934
- callSummaries
935
- ]);
936
- const { positions, routes } = useMemo(() => {
937
- const positions = new Map(layout.placements.map((p) => [p.id, {
938
- x: (p.col - layout.minCol) * metrics.cellX + PAD_X,
939
- y: (p.row - layout.minRow) * metrics.cellY + PAD_Y + metrics.hubTile / 2,
940
- tile: p.kind === "hub" ? metrics.hubTile : p.kind === "you" ? metrics.tile - 8 : metrics.tile
941
- }]));
942
- const bounds = layout.placements.map((p) => {
943
- const { x, y, tile } = positions.get(p.id);
944
- return {
945
- id: p.id,
946
- tile: {
947
- left: x - tile / 2 - 4,
948
- right: x + tile / 2 + 4,
949
- top: y - tile / 2 - 4,
950
- bottom: y + tile / 2 + 4
951
- },
952
- label: {
953
- left: x - (p.kind === "tool" || p.kind === "you" ? 70 : 92),
954
- right: x + (p.kind === "tool" || p.kind === "you" ? 70 : 92),
955
- top: y + tile / 2 + 4,
956
- bottom: y + tile / 2 + (p.kind === "tool" || p.kind === "you" ? 30 : 64)
957
- }
958
- };
959
- });
960
- let lanes = 0;
961
- return {
962
- positions,
963
- routes: new Map(layout.links.map((link) => {
964
- const a = positions.get(link.from), b = positions.get(link.to);
965
- const lane = layout.placements.find((p) => p.id === link.from)?.kind === "hub" ? (++lanes % 2 ? -1 : 1) * Math.ceil(lanes / 2) * 12 : 0;
966
- 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]))];
967
- }))
968
- };
969
- }, [layout, metrics]);
970
- const at = (id) => positions.get(id);
971
- const edgeState = (from, to) => {
972
- let latest;
973
- for (const c of visibleCalls) if (c.from === from && c.to === to || c.from === to && c.to === from) {
974
- if (!latest || c.startedAt > latest.startedAt) latest = c;
975
- }
976
- if (!latest) return void 0;
977
- return latest;
978
- };
979
- const focus = hover ?? open;
980
- const related = useMemo(() => {
981
- if (!focus) return null;
982
- const set = /* @__PURE__ */ new Set([focus]);
983
- for (const l of layout.links) {
984
- if (l.from === focus) set.add(l.to);
985
- if (l.to === focus) set.add(l.from);
986
- }
987
- return set;
988
- }, [focus, layout]);
989
- const availableTools = tools.filter((tool) => !visibleCalls.some((call) => call.from === tool.id || call.to === tool.id));
990
- const lastMinute = visibleCalls.filter((c) => now - c.startedAt <= 6e4).length;
991
- const costs = nodes.map((n) => n.costUsd);
992
- const cost = costs.every((c) => c != null && Number.isFinite(c) && c >= 0) ? costs.reduce((s, c) => s + (c ?? 0), 0) : void 0;
993
- const chips = nodes.filter((n) => n.status === "needs-you" || n.status === "failed" || quietActivity(n, now));
994
- const openNode = (id) => {
995
- const next = open === id ? null : id;
996
- setOpen(next);
997
- if (next) onSelect?.(next);
998
- };
999
- useEffect(() => {
1000
- if (open) {
1001
- closeRef.current?.closest("[role=\"dialog\"]")?.scrollIntoView({ block: "nearest" });
1002
- closeRef.current?.focus({ preventScroll: true });
1003
- }
1004
- }, [open]);
1005
- const keydown = (id) => (event) => {
1006
- const dir = {
1007
- ArrowLeft: [-1, 0],
1008
- ArrowRight: [1, 0],
1009
- ArrowUp: [0, -1],
1010
- ArrowDown: [0, 1]
1011
- }[event.key];
1012
- if (event.key === "Escape") {
1013
- setOpen(null);
1014
- buttons.current.get(id)?.focus();
1015
- return;
1016
- }
1017
- if (event.key === "Home") {
1018
- event.preventDefault();
1019
- nodes[0] && buttons.current.get(nodes[0].id)?.focus();
1020
- return;
1021
- }
1022
- if (!dir) return;
1023
- event.preventDefault();
1024
- const from = at(id);
1025
- let best, bestScore = Infinity;
1026
- for (const p of layout.placements) {
1027
- if (p.id === id) continue;
1028
- const to = at(p.id);
1029
- const dx = to.x - from.x, dy = to.y - from.y;
1030
- const along = dx * dir[0] + dy * dir[1];
1031
- if (along <= 0) continue;
1032
- const score = along + Math.abs(dx * dir[1] - dy * dir[0]) * 2;
1033
- if (score < bestScore) {
1034
- bestScore = score;
1035
- best = p.id;
1036
- }
1037
- }
1038
- if (best) buttons.current.get(best)?.focus();
1039
- };
1040
- const openEntity = open ? entities.get(open) : void 0;
1041
- const popover = openEntity && (() => {
1042
- const p = at(openEntity.id);
1043
- const left = (p.x + 140 + 280) * scale < (width || w) ? (p.x + 40) * scale : Math.max(8, (p.x - 40) * scale - 280);
1044
- const top = list ? 8 : Math.max(8, (p.y - metrics.hubTile / 2 - 8) * scale);
1045
- const summary = openEntity.kind === "you" ? typeof you === "object" ? you.summary : void 0 : openEntity.agent?.turn ?? openEntity.sub ?? void 0;
1046
- return /* @__PURE__ */ jsx(HubPopover, {
1047
- entity: openEntity,
1048
- entities,
1049
- calls: props.calls === void 0 ? void 0 : visibleCalls,
1050
- now,
1051
- left,
1052
- top,
1053
- summary,
1054
- closeRef,
1055
- onClose: () => {
1056
- const id = open;
1057
- setOpen(null);
1058
- buttons.current.get(id)?.focus();
1059
- }
1060
- });
1061
- })();
1062
- return /* @__PURE__ */ jsxs("section", {
1063
- className: `ag-root ${className ?? ""}`,
1064
- "aria-label": typeof title === "string" ? title : "Agent graph",
1065
- children: [
1066
- /* @__PURE__ */ jsx("style", { children: AGENT_HUB_CSS }),
1067
- /* @__PURE__ */ jsxs("header", {
1068
- className: "ag-head",
1069
- children: [
1070
- /* @__PURE__ */ jsxs("span", {
1071
- className: "ag-title",
1072
- children: [/* @__PURE__ */ jsx("span", {
1073
- className: "ag-title-mark",
1074
- "aria-hidden": true
1075
- }), title]
1076
- }),
1077
- chips.map((n) => /* @__PURE__ */ jsxs("button", {
1078
- type: "button",
1079
- className: `ag-chip ag-chip-${n.status}`,
1080
- onClick: () => openNode(n.id),
1081
- children: [
1082
- /* @__PURE__ */ jsx(RunStatusIcon, {
1083
- status: quietActivity(n, now) ? "waiting" : n.status,
1084
- "aria-hidden": true
1085
- }),
1086
- n.label,
1087
- " ",
1088
- quietActivity(n, now)?.label ?? statusMeta(n.status).label
1089
- ]
1090
- }, n.id)),
1091
- /* @__PURE__ */ jsxs("span", {
1092
- className: "ag-stats",
1093
- children: [
1094
- /* @__PURE__ */ jsxs("span", { children: [
1095
- /* @__PURE__ */ jsx("b", { children: nodes.length }),
1096
- " ",
1097
- nodes.length === 1 ? "agent" : "agents"
1098
- ] }),
1099
- /* @__PURE__ */ jsxs("span", {
1100
- title: props.tools === void 0 ? "Tool history not reported" : void 0,
1101
- children: [
1102
- /* @__PURE__ */ jsx("b", { children: props.tools?.length ?? "—" }),
1103
- " ",
1104
- tools.length === 1 ? "tool" : "tools"
1105
- ]
1106
- }),
1107
- /* @__PURE__ */ jsxs("span", {
1108
- title: props.calls === void 0 ? "Call history not reported" : void 0,
1109
- children: [/* @__PURE__ */ jsx("b", { children: props.calls === void 0 ? "—" : lastMinute }), " calls/min"]
1110
- }),
1111
- /* @__PURE__ */ jsxs("span", {
1112
- title: cost === void 0 ? "Not every agent recorded its cost" : "Total recorded agent cost",
1113
- children: [/* @__PURE__ */ jsx("b", { children: cost === void 0 ? "—" : fmtCost(cost) }), " cost"]
1114
- })
1115
- ]
1116
- })
1117
- ]
1118
- }),
1119
- /* @__PURE__ */ jsxs("div", {
1120
- ref: wrap,
1121
- className: `ag-canvas-wrap${list ? " ag-list-mode" : ""}`,
1122
- style: list ? void 0 : { height: h * scale },
1123
- onKeyDown: (e) => {
1124
- if (e.key === "Escape") setOpen(null);
1125
- },
1126
- children: [list ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
1127
- className: "ag-mobile",
1128
- children: [...entities.values()].filter((e) => e.agent).sort((a, b) => {
1129
- const rank = (e) => e.status === "needs-you" ? 0 : e.status === "failed" ? 1 : e.quiet ? 2 : e.status === "waiting" ? 3 : e.status === "running" ? 4 : 5;
1130
- return rank(a) - rank(b);
1131
- }).map((entity) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx(HubNode, {
1132
- entity,
1133
- x: 0,
1134
- y: 0,
1135
- tile: 32,
1136
- dimmed: false,
1137
- selected: open === entity.id,
1138
- buttonRef: (el) => {
1139
- if (el) buttons.current.set(entity.id, el);
1140
- else buttons.current.delete(entity.id);
1141
- },
1142
- onOpen: () => openNode(entity.id),
1143
- onHover: () => {},
1144
- onKeyDown: keydown(entity.id)
1145
- }), /* @__PURE__ */ jsx("div", {
1146
- className: "ag-tools",
1147
- 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", {
1148
- type: "button",
1149
- onClick: () => openNode(t.id),
1150
- children: t.label
1151
- }, t.id))
1152
- })] }, entity.id))
1153
- }), availableTools.length > 0 && /* @__PURE__ */ jsxs("div", {
1154
- className: "ag-available",
1155
- children: [/* @__PURE__ */ jsx("span", { children: "Available tools" }), /* @__PURE__ */ jsx("div", {
1156
- className: "ag-tools",
1157
- children: availableTools.map((tool) => /* @__PURE__ */ jsx("button", {
1158
- type: "button",
1159
- onClick: () => openNode(tool.id),
1160
- children: tool.label
1161
- }, tool.id))
1162
- })]
1163
- })] }) : /* @__PURE__ */ jsxs("div", {
1164
- className: "ag-canvas",
1165
- style: {
1166
- width: w,
1167
- height: h,
1168
- transform: `scale(${scale})`
1169
- },
1170
- children: [/* @__PURE__ */ jsx("svg", {
1171
- className: "ag-edges",
1172
- width: w,
1173
- height: h,
1174
- "aria-hidden": true,
1175
- children: layout.links.map((l) => {
1176
- const { d, length } = routes.get(`${l.from}|${l.to}`);
1177
- const live = edgeState(l.from, l.to);
1178
- const touches = !related || related.has(l.from) && related.has(l.to);
1179
- const forward = live ? live.from === l.from : true;
1180
- return /* @__PURE__ */ jsxs("g", {
1181
- className: touches ? void 0 : "ag-dimmed",
1182
- children: [/* @__PURE__ */ jsx("path", {
1183
- d,
1184
- className: "ag-edge"
1185
- }), live && /* @__PURE__ */ jsx(CallStroke, {
1186
- call: live,
1187
- d,
1188
- length,
1189
- now,
1190
- forward,
1191
- animate
1192
- })]
1193
- }, `${l.from}-${l.to}`);
1194
- })
1195
- }), layout.placements.map((p) => {
1196
- const entity = entities.get(p.id);
1197
- const { x, y } = at(p.id);
1198
- const tile = p.kind === "hub" ? metrics.hubTile : p.kind === "you" ? metrics.tile - 8 : metrics.tile;
1199
- return /* @__PURE__ */ jsx(HubNode, {
1200
- entity,
1201
- x,
1202
- y,
1203
- tile,
1204
- dimmed: Boolean(related && !related.has(p.id)),
1205
- selected: open === p.id,
1206
- buttonRef: (el) => {
1207
- if (el) buttons.current.set(p.id, el);
1208
- else buttons.current.delete(p.id);
1209
- },
1210
- onOpen: () => openNode(p.id),
1211
- onHover: (on) => setHover((prev) => on ? p.id : prev === p.id ? null : prev),
1212
- onKeyDown: keydown(p.id)
1213
- }, p.id);
1214
- })]
1215
- }), popover]
1216
- }),
1217
- !list && /* @__PURE__ */ jsx("ul", {
1218
- className: "ag-legend",
1219
- "aria-label": "Edge colours",
1220
- children: [
1221
- "call",
1222
- "answer",
1223
- "needs-you",
1224
- "failed"
1225
- ].map((k) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
1226
- className: "ag-legend-line",
1227
- style: { background: CALL_COLOR[k] }
1228
- }), CALL_LABEL[k]] }, k))
1229
- })
1230
- ]
1231
- });
1232
- }
1233
- // src/workflows/WorkflowGraphLazy.tsx
1234
- /**
1235
- * Lazy + error-isolated boundary for {@link WorkflowGraph}. React Flow (~50KB)
1236
- * is only needed on the workflow detail page and on workflow proposal cards, so
1237
- * it's code-split out of the always-loaded app shell. The Suspense fallback
1238
- * keeps the caller's height so layout doesn't jump; the error boundary degrades
1239
- * to the raw YAML if the chunk fails
1240
- * to load (offline) or React Flow throws, instead of crashing the panel tree.
1241
- *
1242
- * The chunk import auto-retries a transient failure (a flaky network or an
1243
- * HMR/cache miss) before degrading, and the fallback offers a manual retry that
1244
- * re-attempts the import — so a recoverable failure isn't permanently pinned to
1245
- * the YAML view. A chunk that is gone for good (e.g. an old hash after a deploy)
1246
- * still degrades to the fully-legible YAML; the host can recover those globally
1247
- * via a Vite `vite:preloadError` reload guard.
1248
- */
1249
- /** Re-run `factory` up to `retries` times with a short backoff before giving up.
1250
- * Covers a transient dynamic-import rejection (network blip, a chunk briefly
1251
- * unavailable behind a CDN) without bothering the user. Exported for tests. */
1252
- function retryImport(factory, retries = 2, delayMs = 350) {
1253
- return factory().catch((err) => {
1254
- if (retries <= 0) throw err;
1255
- return new Promise((resolve, reject) => {
1256
- setTimeout(() => retryImport(factory, retries - 1, delayMs).then(resolve, reject), delayMs);
1257
- });
1258
- });
1259
- }
1260
- function lazyWorkflowGraph() {
1261
- return lazy(() => retryImport(() => import("./WorkflowGraph-BtJnA5iY.js").then((m) => ({ default: m.WorkflowGraph }))));
1262
- }
1263
- var GraphErrorBoundary = class extends Component {
1264
- state = { hasError: false };
1265
- static getDerivedStateFromError() {
1266
- return { hasError: true };
1267
- }
1268
- render() {
1269
- return this.state.hasError ? this.props.fallback : this.props.children;
1270
- }
1271
- };
1272
- /** Graceful degradation when the graph can't render: the workflow is still
1273
- * fully legible as YAML, with a retry for a recoverable load failure. */
1274
- function GraphFallback({ yaml, className, onRetry }) {
1275
- return /* @__PURE__ */ jsxs("div", {
1276
- className: `overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
1277
- children: [/* @__PURE__ */ jsxs("div", {
1278
- className: "mb-1 flex items-center justify-between gap-2",
1279
- children: [/* @__PURE__ */ jsx("p", {
1280
- className: "text-muted-foreground text-xs",
1281
- children: "Couldn't render the graph — showing the definition."
1282
- }), /* @__PURE__ */ jsx("button", {
1283
- type: "button",
1284
- onClick: onRetry,
1285
- className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-muted-foreground text-xs transition hover:text-foreground",
1286
- children: "Retry"
1287
- })]
1288
- }), /* @__PURE__ */ jsx("pre", {
1289
- className: "text-foreground text-xs",
1290
- children: /* @__PURE__ */ jsx("code", { children: yaml })
1291
- })]
1292
- });
1293
- }
1294
- function WorkflowGraph(props) {
1295
- const [attempt, setAttempt] = useState(0);
1296
- const Impl = useMemo(() => lazyWorkflowGraph(), [attempt]);
1297
- return /* @__PURE__ */ jsx(GraphErrorBoundary, {
1298
- fallback: /* @__PURE__ */ jsx(GraphFallback, {
1299
- ...props,
1300
- onRetry: () => setAttempt((a) => a + 1)
1301
- }),
1302
- children: /* @__PURE__ */ jsx(Suspense, {
1303
- fallback: /* @__PURE__ */ jsx("div", {
1304
- className: `flex items-center justify-center rounded-lg border border-border border-dashed text-text-muted text-xs ${props.className ?? ""}`,
1305
- children: "Loading graph…"
1306
- }),
1307
- children: /* @__PURE__ */ jsx(Impl, { ...props })
1308
- })
1309
- }, `${props.yaml}::${attempt}`);
1310
- }
1311
- // src/workflows/AgentGraph.tsx
1312
- /**
1313
- * `AgentGraph` — the topology of a multi-agent run, for a run page's side pane.
1314
- *
1315
- * Lazy and error-isolated like `WorkflowGraph`: React Flow and ELK load on
1316
- * first render, a transient chunk failure retries, and a graph that cannot
1317
- * render degrades to a plain list of the agents (still selectable) instead of
1318
- * taking the pane down.
1319
- */
1320
- function lazyCanvas() {
1321
- return lazy(() => retryImport(() => import("./AgentGraphCanvas-CmxReSqa.js").then((m) => ({ default: m.AgentGraphCanvas }))));
1322
- }
1323
- /** The agents as a list, for when the canvas cannot load. */
1324
- function AgentListFallback({ nodes, selectedId, onSelect, className, onRetry }) {
1325
- return /* @__PURE__ */ jsxs("div", {
1326
- className: `overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
1327
- children: [/* @__PURE__ */ jsxs("div", {
1328
- className: "mb-1 flex items-center justify-between gap-2",
1329
- children: [/* @__PURE__ */ jsx("p", {
1330
- className: "text-muted-foreground text-xs",
1331
- children: "Couldn't draw the graph — showing the agents."
1332
- }), /* @__PURE__ */ jsx("button", {
1333
- type: "button",
1334
- onClick: onRetry,
1335
- className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-muted-foreground text-xs transition hover:text-foreground",
1336
- children: "Retry"
1337
- })]
1338
- }), /* @__PURE__ */ jsx("ul", {
1339
- className: "space-y-0.5",
1340
- children: nodes.map((n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
1341
- type: "button",
1342
- "aria-pressed": selectedId === n.id,
1343
- onClick: () => onSelect?.(n.id),
1344
- 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",
1345
- children: [/* @__PURE__ */ jsx("span", {
1346
- className: "truncate",
1347
- children: n.label
1348
- }), /* @__PURE__ */ jsx("span", {
1349
- className: "shrink-0 text-muted-foreground",
1350
- children: n.status
1351
- })]
1352
- }) }, n.id))
1353
- })]
1354
- });
1355
- }
1356
- function AgentGraph(props) {
1357
- if (props.layout === "hub") {
1358
- if (props.nodes.length === 0 && props.emptyState) return /* @__PURE__ */ jsx(Fragment, { children: props.emptyState });
1359
- return /* @__PURE__ */ jsx(AgentHubCanvas, { ...props });
1360
- }
1361
- return /* @__PURE__ */ jsx(AgentGraphTree, { ...props });
1362
- }
1363
- function AgentGraphTree(props) {
1364
- const [attempt, setAttempt] = useState(0);
1365
- const Canvas = useMemo(() => lazyCanvas(), [attempt]);
1366
- return /* @__PURE__ */ jsx(GraphErrorBoundary, {
1367
- fallback: /* @__PURE__ */ jsx(AgentListFallback, {
1368
- ...props,
1369
- onRetry: () => setAttempt((a) => a + 1)
1370
- }),
1371
- children: /* @__PURE__ */ jsx(Suspense, {
1372
- fallback: /* @__PURE__ */ jsx("div", {
1373
- className: `flex items-center justify-center rounded-lg border border-border border-dashed text-muted-foreground text-xs ${props.className ?? ""}`,
1374
- children: "Loading graph…"
1375
- }),
1376
- children: /* @__PURE__ */ jsx(Canvas, { ...props })
1377
- })
1378
- }, attempt);
1379
- }
1380
- export { fmtDuration as a, fmtTokens as c, quietActivity as d, syncAmbient as f, useRunNow as g, useMotionAllowed as h, fmtCost as i, activityTiming as l, useAttention as m, WorkflowGraph as n, fmtEventTime as o, tokenMilliseconds as p, clampPreview as r, fmtTokenTotal as s, AgentGraph as t, ambientStyle as u };