@tangle-network/sandbox-ui 0.138.1 → 0.139.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,302 @@
1
1
  import { i as modelBrandFor, t as ModelBrandStack } from "./model-brand-bTcMjQtB.js";
2
2
  import { t as ProviderIcon } from "./provider-logo-CPKRPHcS.js";
3
3
  import { StatusPill } from "@tangle-network/ui/primitives";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
- import { AlertTriangle, Bell, Box, Cable, Camera, Check, Circle, Clock, Code, GitBranch, Repeat, ScanSearch, Sparkles, UserCheck, Webhook, Zap } from "lucide-react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Component, Suspense, lazy, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { AlertTriangle, Bell, Bot, Box, Cable, Camera, Check, Circle, Clock, Clock3, Code, GitBranch, LoaderCircle, Repeat, ScanSearch, Sparkles, User, UserCheck, Webhook, Wrench, X, Zap } from "lucide-react";
6
7
  import { parse } from "yaml";
8
+ // src/workflows/agent-hub-css.ts
9
+ /**
10
+ * The hub layout's styles, scoped under `.ag-root` and shipped with the component
11
+ * so a host without the package's Tailwind build still draws it. Colours are the
12
+ * package's theme tokens, so light, dark and named themes follow the host.
13
+ * Four type sizes (11, 12, 13, 14 px); spacing on a 4 px grid.
14
+ */
15
+ const AGENT_HUB_CSS = `
16
+ .ag-root { --ag-surface: hsl(var(--card)); --ag-fg: hsl(var(--foreground)); --ag-muted: hsl(var(--muted-foreground)); --ag-border: hsl(var(--border));
17
+ --ag-line: color-mix(in oklab, var(--ag-fg) 14%, transparent); --ag-accent: hsl(var(--primary));
18
+ color: var(--ag-fg); font-size: 13px; line-height: 18px; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
19
+ .ag-root * { box-sizing: border-box; }
20
+ .ag-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 24px; }
21
+ .ag-title { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; }
22
+ .ag-title-mark { width: 14px; height: 14px; border-radius: 4px; background: conic-gradient(from 45deg, var(--ag-accent), color-mix(in oklab, var(--ag-accent) 40%, transparent), var(--ag-accent)); -webkit-mask: radial-gradient(circle at 50% 50%, transparent 2.5px, #000 3px); mask: radial-gradient(circle at 50% 50%, transparent 2.5px, #000 3px); }
23
+ .ag-chip { display: inline-flex; align-items: center; gap: 4px; height: 22px; padding: 0 8px; border-radius: 6px; font-size: 12px; font-weight: 500; }
24
+ .ag-chip-needs-you { color: var(--agent-graph-needs-you, hsl(38 92% 42%)); background: color-mix(in oklab, var(--agent-graph-needs-you, hsl(38 92% 50%)) 14%, transparent); }
25
+ .ag-chip-failed { color: var(--agent-graph-failed, hsl(0 72% 50%)); background: color-mix(in oklab, var(--agent-graph-failed, hsl(0 84% 60%)) 13%, transparent); }
26
+ .ag-stats { margin-left: auto; display: inline-flex; gap: 16px; color: var(--ag-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
27
+ .ag-stats b { color: var(--ag-fg); font-weight: 500; }
28
+ .ag-canvas-wrap { position: relative; border: 1px solid var(--ag-border); border-radius: 12px; background: var(--ag-surface); overflow: hidden; }
29
+ .ag-canvas { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
30
+ .ag-edges { position: absolute; inset: 0; overflow: visible; }
31
+ .ag-edge { fill: none; stroke: var(--ag-line); stroke-width: 1.25; }
32
+ .ag-edge-live { fill: none; stroke-width: 1.75; stroke-linecap: round; }
33
+ .ag-flow { stroke-dasharray: 28 var(--ag-len); animation: ag-travel 1.15s cubic-bezier(.4,0,.2,1) infinite; }
34
+ @keyframes ag-travel { from { stroke-dashoffset: 28px; } to { stroke-dashoffset: calc(-1 * var(--ag-len)); } }
35
+ .ag-root g, .ag-node { transition: opacity 180ms cubic-bezier(.2,.8,.2,1); }
36
+ .ag-dimmed { opacity: .22; }
37
+ .ag-node { position: absolute; display: flex; flex-direction: column; align-items: center; width: 132px; margin-left: -66px; margin-top: calc(var(--ag-tile) / -2);
38
+ padding: 0; border: 0; background: none; color: inherit; font: inherit; cursor: pointer; outline: none; }
39
+ .ag-tile { position: relative; display: grid; place-items: center; width: var(--ag-tile); height: var(--ag-tile); border-radius: calc(var(--ag-tile) * .26);
40
+ background: var(--ag-surface); border: 1px solid var(--ag-border); box-shadow: 0 1px 2px rgb(0 0 0 / .06), 0 4px 12px -6px rgb(0 0 0 / .18);
41
+ transition: transform 160ms cubic-bezier(.2,.8,.2,1), box-shadow 160ms; }
42
+ .ag-node:hover .ag-tile { transform: translateY(-1px); box-shadow: 0 2px 4px rgb(0 0 0 / .08), 0 8px 18px -8px rgb(0 0 0 / .28); }
43
+ .ag-node:focus-visible .ag-tile, .ag-selected .ag-tile { outline: 2px solid var(--ag-accent); outline-offset: 2px; }
44
+ .ag-node[style*="--ag-tone"] .ag-tile { border-color: color-mix(in oklab, var(--ag-tone) 55%, var(--ag-border)); background: color-mix(in oklab, var(--ag-tone) 9%, var(--ag-surface)); }
45
+ .ag-node-hub .ag-tile::before { content: ""; position: absolute; inset: -28px; z-index: -1; border-radius: 50%; background: radial-gradient(closest-side, color-mix(in oklab, var(--ag-accent) 22%, transparent), transparent); }
46
+ .ag-node-you .ag-tile { background: var(--ag-fg); color: var(--ag-surface); border-color: transparent; }
47
+ .ag-name { margin-top: 6px; font-size: 13px; font-weight: 500; line-height: 16px; max-width: 132px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
+ .ag-node-tool .ag-name { font-weight: 400; color: var(--ag-muted); }
49
+ .ag-sub { font-size: 11px; line-height: 14px; color: var(--ag-muted); max-width: 132px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
+ .ag-badge { position: absolute; top: -5px; right: -5px; display: grid; place-items: center; width: 15px; height: 15px; border-radius: 50%; color: #fff; font-size: 10px; font-weight: 700; box-shadow: 0 0 0 2px var(--ag-surface); }
51
+ .ag-badge-running { background: var(--ag-surface); color: var(--ag-accent); box-shadow: 0 0 0 1px var(--ag-border); }
52
+ .ag-badge-running svg, .ag-spin { animation: ag-spin .9s linear infinite; }
53
+ @keyframes ag-spin { to { transform: rotate(360deg); } }
54
+ .ag-legend { list-style: none; margin: 0; padding: 0; display: flex; justify-content: center; gap: 20px; color: var(--ag-muted); font-size: 12px; }
55
+ .ag-legend li { display: inline-flex; align-items: center; gap: 6px; }
56
+ .ag-legend-line { width: 14px; height: 2px; border-radius: 1px; }
57
+ .ag-popover { position: absolute; z-index: 5; width: 280px; padding: 12px; border-radius: 12px; background: hsl(var(--popover, var(--card))); color: var(--ag-fg);
58
+ border: 1px solid var(--ag-border); box-shadow: 0 16px 40px -12px rgb(0 0 0 / .35), 0 2px 6px rgb(0 0 0 / .08); animation: ag-arrive 150ms cubic-bezier(.2,.8,.2,1); }
59
+ @keyframes ag-arrive { from { opacity: 0; transform: translateY(4px); } }
60
+ .ag-pop-head { display: flex; align-items: center; gap: 8px; }
61
+ .ag-tile-mini { border-radius: 8px; box-shadow: none; flex-shrink: 0; }
62
+ .ag-tile-micro { border-radius: 4px; box-shadow: none; flex-shrink: 0; border: 0; background: none; }
63
+ .ag-pop-title { display: flex; flex-direction: column; min-width: 0; flex: 1; }
64
+ .ag-pop-name { font-size: 14px; font-weight: 600; line-height: 18px; }
65
+ .ag-pop-sub { font-size: 12px; color: var(--ag-muted); line-height: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
66
+ .ag-pill { height: 20px; padding: 0 8px; border-radius: 6px; font-size: 11px; line-height: 20px; font-weight: 500; color: var(--ag-muted); background: color-mix(in oklab, var(--ag-fg) 7%, transparent); }
67
+ .ag-pill-failed { color: var(--agent-graph-failed, hsl(0 72% 50%)); background: color-mix(in oklab, var(--agent-graph-failed, hsl(0 84% 60%)) 13%, transparent); }
68
+ .ag-pill-needs-you { color: var(--agent-graph-needs-you, hsl(38 92% 42%)); background: color-mix(in oklab, var(--agent-graph-needs-you, hsl(38 92% 50%)) 14%, transparent); }
69
+ .ag-pill-running { color: var(--ag-accent); background: color-mix(in oklab, var(--ag-accent) 12%, transparent); }
70
+ .ag-pill-succeeded { color: var(--agent-graph-answer, hsl(152 60% 38%)); background: color-mix(in oklab, var(--agent-graph-answer, hsl(152 60% 45%)) 13%, transparent); }
71
+ .ag-close { display: grid; place-items: center; width: 24px; height: 24px; margin-right: -4px; border: 0; border-radius: 6px; background: none; color: var(--ag-muted); cursor: pointer; }
72
+ .ag-close:hover { background: color-mix(in oklab, var(--ag-fg) 7%, transparent); color: var(--ag-fg); }
73
+ .ag-close:focus-visible { outline: 2px solid var(--ag-accent); }
74
+ .ag-pop-summary { margin: 8px 0 0; font-size: 13px; line-height: 18px; }
75
+ .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; }
76
+ .ag-pop-calls li { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 16px; min-width: 0; }
77
+ .ag-pop-dir { color: var(--ag-muted); width: 12px; flex-shrink: 0; }
78
+ .ag-pop-call { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ag-muted); }
79
+ .ag-pop-call b { color: var(--ag-fg); font-weight: 500; }
80
+ .ag-pop-time { flex-shrink: 0; color: var(--ag-muted); font-variant-numeric: tabular-nums; display: inline-flex; }
81
+ .ag-pop-foot { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--ag-border); font-size: 12px; color: var(--ag-muted); display: flex; gap: 6px; }
82
+ .ag-pop-foot b { color: var(--ag-fg); font-weight: 600; }
83
+ @media (prefers-reduced-motion: reduce) {
84
+ .ag-root *, .ag-root *::before { animation: none !important; transition: none !important; }
85
+ }
86
+ `;
87
+ const HUB_METRICS = {
88
+ cellX: 184,
89
+ cellY: 140,
90
+ tile: 48,
91
+ hubTile: 60,
92
+ label: 34
93
+ };
94
+ const HUB_METRICS_COMPACT = {
95
+ cellX: 112,
96
+ cellY: 112,
97
+ tile: 40,
98
+ hubTile: 48,
99
+ label: 30
100
+ };
101
+ const key = (col, row) => `${col},${row}`;
102
+ /** Ring cells around the hub, nearest first: the diagonals, then the sides, then ring two by angle. */
103
+ function ringCells(count) {
104
+ const cells = [
105
+ [-1, -1],
106
+ [1, -1],
107
+ [-1, 1],
108
+ [1, 1],
109
+ [-1, 0],
110
+ [1, 0],
111
+ [0, 1],
112
+ [0, -1]
113
+ ];
114
+ for (let ring = 2; cells.length < count; ring++) {
115
+ const outer = [];
116
+ 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]);
117
+ outer.sort((a, b) => Math.atan2(a[1], a[0]) - Math.atan2(b[1], b[0]));
118
+ cells.push(...outer);
119
+ }
120
+ return cells.slice(0, count);
121
+ }
122
+ /** The orchestrator: the agent nobody spawned, else the first. */
123
+ function hubAgentId(nodes, edges) {
124
+ const spawned = new Set(edges.filter((e) => e.kind === "spawn").map((e) => e.to));
125
+ return (nodes.find((n) => !spawned.has(n.id)) ?? nodes[0])?.id;
126
+ }
127
+ /**
128
+ * Place every node on the grid. Agents take ring cells in order; each tool takes
129
+ * the first free cell outward from the first agent that called it (beside, then
130
+ * above or below, then diagonal), so it sits next to its user.
131
+ */
132
+ function layoutHub(input) {
133
+ const { nodes, edges, tools, calls, you } = input;
134
+ const hub = hubAgentId(nodes, edges);
135
+ const placements = [];
136
+ const taken = /* @__PURE__ */ new Set();
137
+ const place = (id, kind, col, row) => {
138
+ placements.push({
139
+ id,
140
+ kind,
141
+ col,
142
+ row
143
+ });
144
+ taken.add(key(col, row));
145
+ };
146
+ if (hub) place(hub, "hub", 0, 0);
147
+ const agents = nodes.filter((n) => n.id !== hub);
148
+ ringCells(agents.length).forEach(([c, r], i) => place(agents[i].id, "agent", c, r));
149
+ const ring = Math.max(1, ...placements.map((p) => Math.max(Math.abs(p.col), Math.abs(p.row))));
150
+ if (you) {
151
+ const col = -Math.min(ring, 1);
152
+ let row = -ring - 1;
153
+ while (taken.has(key(col, row))) row--;
154
+ place("you", "you", col, row);
155
+ }
156
+ const cell = new Map(placements.map((p) => [p.id, p]));
157
+ const userOf = /* @__PURE__ */ new Map();
158
+ for (const call of calls) {
159
+ const toolId = tools.some((t) => t.id === call.to) ? call.to : tools.some((t) => t.id === call.from) ? call.from : void 0;
160
+ const agentId = toolId === call.to ? call.from : call.to;
161
+ if (toolId && cell.has(agentId) && !userOf.has(toolId)) userOf.set(toolId, agentId);
162
+ }
163
+ for (const tool of tools) {
164
+ const owner = cell.get(userOf.get(tool.id) ?? hub ?? "");
165
+ const ac = owner?.col ?? 0, ar = owner?.row ?? 0;
166
+ const sx = Math.sign(ac) || 1, sy = Math.sign(ar) || 1;
167
+ const options = ac === 0 ? [
168
+ [0, sy],
169
+ [sx, sy],
170
+ [-sx, sy],
171
+ [sx, 0]
172
+ ] : ar === 0 ? [
173
+ [sx, 0],
174
+ [sx, -1],
175
+ [sx, 1],
176
+ [0, -1]
177
+ ] : [
178
+ [sx, 0],
179
+ [0, sy],
180
+ [sx, sy],
181
+ [-sx, sy],
182
+ [sx, -sy]
183
+ ];
184
+ let spot;
185
+ for (let step = 1; !spot && step < 6; step++) for (const [dx, dy] of options) {
186
+ const c = ac + dx * step, r = ar + dy * step;
187
+ if (!taken.has(key(c, r))) {
188
+ spot = [c, r];
189
+ break;
190
+ }
191
+ }
192
+ place(tool.id, "tool", spot[0], spot[1]);
193
+ }
194
+ const links = [];
195
+ const seen = /* @__PURE__ */ new Set();
196
+ const link = (from, to) => {
197
+ const k = [from, to].sort().join("|");
198
+ if (from === to || seen.has(k) || !taken.size) return;
199
+ seen.add(k);
200
+ links.push({
201
+ from,
202
+ to
203
+ });
204
+ };
205
+ if (you && hub) link("you", hub);
206
+ for (const agent of agents) {
207
+ const parent = edges.find((e) => e.kind === "spawn" && e.to === agent.id)?.from;
208
+ link(parent && cell.has(parent) ? parent : hub, agent.id);
209
+ }
210
+ for (const call of calls) if (cell.has(call.from) && cell.has(call.to)) link(call.from, call.to);
211
+ return {
212
+ placements,
213
+ links,
214
+ minCol: Math.min(...placements.map((p) => p.col)),
215
+ maxCol: Math.max(...placements.map((p) => p.col)),
216
+ minRow: Math.min(...placements.map((p) => p.row)),
217
+ maxRow: Math.max(...placements.map((p) => p.row))
218
+ };
219
+ }
220
+ /**
221
+ * An orthogonal path between two tile centres with rounded elbows. Leaving the
222
+ * hub, and between nodes in one column, it goes vertical first; otherwise
223
+ * horizontal first. `lane` shifts a shared run sideways so parallel edges stay
224
+ * apart. Returns the SVG path and its length (for the travelling pulse).
225
+ */
226
+ function orthogonalPath(a, b, verticalFirst, lane = 0, radius = 10) {
227
+ const dx = b.x - a.x, dy = b.y - a.y;
228
+ if (Math.abs(dx) < 1 || Math.abs(dy) < 1) return {
229
+ d: `M${a.x},${a.y} L${b.x},${b.y}`,
230
+ length: Math.hypot(dx, dy)
231
+ };
232
+ const r = Math.min(radius, Math.abs(dx) / 2, Math.abs(dy) / 2);
233
+ const sx = Math.sign(dx), sy = Math.sign(dy);
234
+ if (verticalFirst) {
235
+ const x = a.x + lane;
236
+ return {
237
+ d: `M${x},${a.y} L${x},${b.y - sy * r} Q${x},${b.y} ${x + sx * r},${b.y} L${b.x},${b.y}`,
238
+ length: Math.abs(dy) + Math.abs(b.x - x) - 2 * r + Math.PI * r / 2
239
+ };
240
+ }
241
+ const y = a.y + lane;
242
+ return {
243
+ d: `M${a.x},${y} L${b.x - sx * r},${y} Q${b.x},${y} ${b.x},${y + sy * r} L${b.x},${b.y}`,
244
+ length: Math.abs(dx) + Math.abs(b.y - y) - 2 * r + Math.PI * r / 2
245
+ };
246
+ }
247
+ // src/workflows/format.ts
248
+ /**
249
+ * Display formatters for the workflow graph's live run-state chips. Pure and
250
+ * dependency-free so they can be unit-tested without rendering the graph.
251
+ */
252
+ /** Human-readable duration: `850ms`, `4.2s`, `1m30s`. Undefined for an absent,
253
+ * non-finite, or negative input (e.g. clock skew) so the caller renders no
254
+ * chip rather than a confusing `-5ms`. */
255
+ function fmtDuration(ms) {
256
+ if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
257
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
258
+ const s = ms / 1e3;
259
+ if (s < 60) return `${s.toFixed(1)}s`;
260
+ const totalSeconds = Math.round(s);
261
+ return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
262
+ }
263
+ /** Human-readable USD cost: `$0`, `$0.0032` (sub-cent, 4dp), `$1.20`. Undefined
264
+ * for an absent, non-finite, or negative input — a cost is a billing amount, so
265
+ * a negative value is bad data, not a refund to render. */
266
+ function fmtCost(usd) {
267
+ if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
268
+ if (usd === 0) return "$0";
269
+ return usd < .01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
270
+ }
271
+ /** Bound host-supplied preview/error text to a fixed length (with an ellipsis)
272
+ * before it reaches the DOM, so an oversized payload can't bloat the node tree —
273
+ * the card only ever shows a short preview, and CSS clamping is visual only.
274
+ * Truncation is code-point aware: it never slices through a surrogate pair (which
275
+ * would leave a lone surrogate that renders as the replacement character). */
276
+ function clampPreview(text, max = 200) {
277
+ if (text.length <= max) return text;
278
+ const lastCode = text.charCodeAt(max - 1);
279
+ const end = lastCode >= 55296 && lastCode <= 56319 ? max - 1 : max;
280
+ return `${text.slice(0, end)}…`;
281
+ }
282
+ /** Human-readable token usage: `1200/340 tok`. Undefined when neither side is a
283
+ * finite, non-negative count, so the caller renders no chip; an invalid side
284
+ * (negative / NaN / absent) shows as 0 rather than leaking `-5/NaN tok`. */
285
+ function fmtTokens(input, output) {
286
+ const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
287
+ const i = valid(input);
288
+ const o = valid(output);
289
+ if (i === void 0 && o === void 0) return void 0;
290
+ return `${i ?? 0}/${o ?? 0} tok`;
291
+ }
292
+ /** A token total in a few characters: `840 tok`, `12.4k tok`, `1.2M tok`.
293
+ * Undefined for an absent, non-finite or negative count. */
294
+ function fmtTokenTotal(tokens) {
295
+ if (tokens == null || !Number.isFinite(tokens) || tokens < 0) return void 0;
296
+ if (tokens < 1e3) return `${Math.round(tokens)} tok`;
297
+ if (tokens < 1e6) return `${(tokens / 1e3).toFixed(tokens < 1e4 ? 1 : 0)}k tok`;
298
+ return `${(tokens / 1e6).toFixed(1)}M tok`;
299
+ }
7
300
  // src/workflows/cron.ts
8
301
  /**
9
302
  * Cron → English for a schedule trigger's node label. A raw `0 9 * * 1-5` tells
@@ -124,51 +417,6 @@ function describeCron(expression) {
124
417
  if (day !== null && day >= 1 && day <= 31) return `Monthly on the ${ordinal(day)} at ${time}`;
125
418
  return raw;
126
419
  }
127
- // src/workflows/format.ts
128
- /**
129
- * Display formatters for the workflow graph's live run-state chips. Pure and
130
- * dependency-free so they can be unit-tested without rendering the graph.
131
- */
132
- /** Human-readable duration: `850ms`, `4.2s`, `1m30s`. Undefined for an absent,
133
- * non-finite, or negative input (e.g. clock skew) so the caller renders no
134
- * chip rather than a confusing `-5ms`. */
135
- function fmtDuration(ms) {
136
- if (ms === void 0 || !Number.isFinite(ms) || ms < 0) return void 0;
137
- if (ms < 1e3) return `${Math.round(ms)}ms`;
138
- const s = ms / 1e3;
139
- if (s < 60) return `${s.toFixed(1)}s`;
140
- const totalSeconds = Math.round(s);
141
- return `${Math.floor(totalSeconds / 60)}m${totalSeconds % 60}s`;
142
- }
143
- /** Human-readable USD cost: `$0`, `$0.0032` (sub-cent, 4dp), `$1.20`. Undefined
144
- * for an absent, non-finite, or negative input — a cost is a billing amount, so
145
- * a negative value is bad data, not a refund to render. */
146
- function fmtCost(usd) {
147
- if (usd === void 0 || !Number.isFinite(usd) || usd < 0) return void 0;
148
- if (usd === 0) return "$0";
149
- return usd < .01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
150
- }
151
- /** Bound host-supplied preview/error text to a fixed length (with an ellipsis)
152
- * before it reaches the DOM, so an oversized payload can't bloat the node tree —
153
- * the card only ever shows a short preview, and CSS clamping is visual only.
154
- * Truncation is code-point aware: it never slices through a surrogate pair (which
155
- * would leave a lone surrogate that renders as the replacement character). */
156
- function clampPreview(text, max = 200) {
157
- if (text.length <= max) return text;
158
- const lastCode = text.charCodeAt(max - 1);
159
- const end = lastCode >= 55296 && lastCode <= 56319 ? max - 1 : max;
160
- return `${text.slice(0, end)}…`;
161
- }
162
- /** Human-readable token usage: `1200/340 tok`. Undefined when neither side is a
163
- * finite, non-negative count, so the caller renders no chip; an invalid side
164
- * (negative / NaN / absent) shows as 0 rather than leaking `-5/NaN tok`. */
165
- function fmtTokens(input, output) {
166
- const valid = (n) => n !== void 0 && Number.isFinite(n) && n >= 0 ? n : void 0;
167
- const i = valid(input);
168
- const o = valid(output);
169
- if (i === void 0 && o === void 0) return void 0;
170
- return `${i ?? 0}/${o ?? 0} tok`;
171
- }
172
420
  // src/workflows/naming.ts
173
421
  /**
174
422
  * Human-facing naming for workflow nodes. A definition addresses things by
@@ -1744,180 +1992,749 @@ function NodeMark({ kind, provider, model, accent, tile }) {
1744
1992
  })
1745
1993
  });
1746
1994
  }
1747
- // src/workflows/node-output.tsx
1748
- /** Failure text, in the semantic danger token — which carries a light AND a dark
1749
- * value. A palette shade (`text-red-400`) only ever reads well in one theme. */
1750
- const ERROR_TEXT = { color: "var(--surface-danger-text)" };
1751
- /** Max key/value pairs a shallow-object output renders before it reports
1752
- * `truncated` — the card only has room for a couple rows anyway. */
1753
- const MAX_JSON_ENTRIES = 6;
1995
+ // src/workflows/agent-hub-parts.tsx
1754
1996
  /**
1755
- * A line that is pure structure — a code fence (with or without a language tag), a
1756
- * table's rule row, a thematic break — and so carries no words to keep.
1757
- *
1758
- * Every test is a SINGLE quantified character class: unambiguous, and linear in the
1759
- * length of the line. Written as one pattern (`[\s|:-]+\|[\s|:-]*`) the two
1760
- * quantifiers would compete for the same `|`, which is the shape that backtracks.
1761
- * Recognize the charset in one pass instead, then ask the cheap questions — is
1762
- * there a pipe? a dash? — with plain string checks.
1997
+ * What the `AgentGraph` hub layout draws: a node (a brand-marked tile with its
1998
+ * name and model under it), the status badge on its corner, and the popover of
1999
+ * its recent calls. Status reads as a glyph and a word as well as a colour.
1763
2000
  */
1764
- function isStructureOnly(line) {
1765
- if (/^(```|~~~)/.test(line)) return true;
1766
- const isBreak = /^([-*_])\1{2,}$/.test(line);
1767
- if (/^[\s|:-]+$/.test(line) && line.includes("|")) return line.includes("-");
1768
- return isBreak;
2001
+ /** One colour per call kind, shared by edges, the legend and popover rows. */
2002
+ const CALL_COLOR = {
2003
+ call: "var(--agent-graph-call, hsl(217 91% 60%))",
2004
+ answer: `var(--agent-graph-answer, ${STATUS_COLOR.succeeded})`,
2005
+ "needs-you": `var(--agent-graph-needs-you, ${STATUS_COLOR.waiting})`,
2006
+ failed: `var(--agent-graph-failed, ${STATUS_COLOR.failed})`
2007
+ };
2008
+ const CALL_LABEL = {
2009
+ call: "Call",
2010
+ answer: "Answer",
2011
+ "needs-you": "Needs you",
2012
+ failed: "Failed"
2013
+ };
2014
+ function NodeIcon({ entity, size }) {
2015
+ if (entity.kind === "you") return /* @__PURE__ */ jsx(User, {
2016
+ size: size * .42,
2017
+ strokeWidth: 2.2,
2018
+ "aria-hidden": true
2019
+ });
2020
+ if (entity.kind === "tool") {
2021
+ const provider = entity.tool?.provider;
2022
+ if (!provider) return /* @__PURE__ */ jsx(Wrench, {
2023
+ size: size * .4,
2024
+ "aria-hidden": true
2025
+ });
2026
+ return /* @__PURE__ */ jsx(ProviderIcon, {
2027
+ id: provider,
2028
+ displayName: entity.label,
2029
+ size: Math.round(size * .46),
2030
+ className: "rounded-[4px]"
2031
+ });
2032
+ }
2033
+ const brand = entity.agent?.model ? modelBrandFor(entity.agent.model) : null;
2034
+ return brand ? /* @__PURE__ */ jsx(ModelBrandStack, {
2035
+ identity: brand,
2036
+ size: "sm"
2037
+ }) : /* @__PURE__ */ jsx(Bot, {
2038
+ size: size * .44,
2039
+ "aria-hidden": true
2040
+ });
1769
2041
  }
1770
- /**
1771
- * Flatten an agent's markdown answer into the sentence a person would read out
1772
- * of it. On a two-line card preview the syntax IS the noise: a `## Heading` mid
1773
- * paragraph, a `**bold**` run and a bullet's `-` all survive the clamp while the
1774
- * words that carry the meaning get pushed past it. Stripping the markup (never
1775
- * the words) is what turns the preview back into prose — the full text, markup
1776
- * and all, is still rendered by the node's detail view.
1777
- *
1778
- * Structure-only lines (a fence, a table rule) are dropped; everything else keeps
1779
- * its text and is joined into one flowing line.
1780
- */
1781
- function condenseText(text) {
1782
- const isListItem = (line) => /^\s*([-*+]\s|\d+[.)]\s)/.test(line);
1783
- return text.split("\n").map((line) => ({
1784
- item: isListItem(line),
1785
- text: line.replace(/^\s{0,3}#{1,6}\s+/, "").replace(/^\s{0,3}>\s?/, "").replace(/^\s*[-*+]\s+(\[[ xX]\]\s+)?/, "").replace(/^\s*\d+[.)]\s+/, "").trim()
1786
- })).filter(({ text: line }) => line !== "" && !isStructureOnly(line)).map(({ item, text: line }, i) => i > 0 && item ? `· ${line}` : line).join(" ").replace(/(?<=^|\s)(\*\*|~~)(?!\s)(.+?)(?<!\s)\1(?=[\s.,:;!?)\]}"']|$)/g, "$2").replace(/(?<![\w*])\*(?!\s)([^*]+?)(?<!\s)\*(?![\w*])/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim();
1787
- }
1788
- /** A single JSON value as a compact one-line string for the key/value render.
1789
- * Scalars render verbatim; a nested object/array collapses to a shape marker so
1790
- * one row never balloons into a wall of text. */
1791
- function scalar(value) {
1792
- if (value === null) return "null";
1793
- if (typeof value === "string") return value;
1794
- if (typeof value === "number" || typeof value === "boolean") return String(value);
1795
- if (Array.isArray(value)) return `[${value.length} item${value.length === 1 ? "" : "s"}]`;
1796
- if (typeof value === "object") return "{…}";
1797
- return "";
2042
+ /** The corner badge: done, needs you, failed or running, each with its own glyph. */
2043
+ function StatusBadge({ status }) {
2044
+ if (status === "succeeded") return /* @__PURE__ */ jsx("span", {
2045
+ className: "ag-badge",
2046
+ style: { background: STATUS_COLOR.succeeded },
2047
+ children: /* @__PURE__ */ jsx(Check, {
2048
+ size: 9,
2049
+ strokeWidth: 3
2050
+ })
2051
+ });
2052
+ if (status === "needs-you") return /* @__PURE__ */ jsx("span", {
2053
+ className: "ag-badge",
2054
+ style: { background: STATUS_COLOR.waiting },
2055
+ children: /* @__PURE__ */ jsx(Clock3, {
2056
+ size: 9,
2057
+ strokeWidth: 3
2058
+ })
2059
+ });
2060
+ if (status === "failed") return /* @__PURE__ */ jsx("span", {
2061
+ className: "ag-badge",
2062
+ style: { background: STATUS_COLOR.failed },
2063
+ children: "!"
2064
+ });
2065
+ if (status === "running") return /* @__PURE__ */ jsx("span", {
2066
+ className: "ag-badge ag-badge-running",
2067
+ children: /* @__PURE__ */ jsx(LoaderCircle, {
2068
+ size: 11,
2069
+ strokeWidth: 2.5
2070
+ })
2071
+ });
2072
+ return null;
2073
+ }
2074
+ function HubNode({ entity, x, y, tile, dimmed, selected, buttonRef, onOpen, onHover, onKeyDown }) {
2075
+ const tone = entity.status === "needs-you" ? STATUS_COLOR.waiting : entity.status === "failed" ? STATUS_COLOR.failed : void 0;
2076
+ const subTone = entity.status === "needs-you" ? STATUS_COLOR.waiting : entity.status === "failed" ? STATUS_COLOR.failed : void 0;
2077
+ const sub = entity.status === "needs-you" ? "Needs you" : entity.status === "failed" ? "Error" : entity.sub;
2078
+ return /* @__PURE__ */ jsxs("button", {
2079
+ ref: buttonRef,
2080
+ type: "button",
2081
+ "data-agent-graph-node": entity.id,
2082
+ "data-kind": entity.kind,
2083
+ "aria-label": `${entity.label}${entity.sub ? `, ${entity.sub}` : ""}${entity.status ? `, ${entity.status === "needs-you" ? "needs you" : entity.status}` : ""}`,
2084
+ "aria-expanded": selected,
2085
+ className: `ag-node ag-node-${entity.kind}${dimmed ? " ag-dimmed" : ""}${selected ? " ag-selected" : ""}`,
2086
+ style: {
2087
+ left: x,
2088
+ top: y,
2089
+ "--ag-tile": `${tile}px`,
2090
+ ...tone ? { "--ag-tone": tone } : {}
2091
+ },
2092
+ onClick: onOpen,
2093
+ onPointerEnter: () => onHover(true),
2094
+ onPointerLeave: () => onHover(false),
2095
+ onFocus: () => onHover(true),
2096
+ onBlur: () => onHover(false),
2097
+ onKeyDown,
2098
+ children: [
2099
+ /* @__PURE__ */ jsxs("span", {
2100
+ className: "ag-tile",
2101
+ children: [/* @__PURE__ */ jsx(NodeIcon, {
2102
+ entity,
2103
+ size: tile
2104
+ }), /* @__PURE__ */ jsx(StatusBadge, { status: entity.status })]
2105
+ }),
2106
+ /* @__PURE__ */ jsx("span", {
2107
+ className: "ag-name",
2108
+ children: entity.label
2109
+ }),
2110
+ sub && /* @__PURE__ */ jsx("span", {
2111
+ className: "ag-sub",
2112
+ style: subTone ? { color: subTone } : void 0,
2113
+ children: sub
2114
+ })
2115
+ ]
2116
+ });
1798
2117
  }
2118
+ const fmtMs = (ms) => ms < 1e3 ? `${Math.round(ms / 100) * 100 || Math.round(ms)}ms` : `${(ms / 1e3).toFixed(ms < 1e4 ? 1 : 0)}s`;
2119
+ /** A node's recent calls: who it called or was called by, what for, how long, and what failed. */
2120
+ function HubPopover({ entity, entities, calls, now, left, top, summary, closeRef, onClose }) {
2121
+ const mine = calls.filter((c) => (c.from === entity.id || c.to === entity.id) && c.startedAt <= now).sort((a, b) => b.startedAt - a.startedAt);
2122
+ const recent = mine.slice(0, 5);
2123
+ const lastMinute = mine.filter((c) => now - c.startedAt <= 6e4).length;
2124
+ const status = entity.status === "needs-you" ? "Needs you" : entity.status === "failed" ? "Error" : entity.status === "running" ? "Running" : entity.status === "succeeded" ? "Done" : void 0;
2125
+ const cost = entity.agent?.costUsd;
2126
+ return /* @__PURE__ */ jsxs("div", {
2127
+ role: "dialog",
2128
+ "aria-label": `${entity.label} calls`,
2129
+ className: "ag-popover",
2130
+ style: {
2131
+ left,
2132
+ top
2133
+ },
2134
+ onKeyDown: (event) => {
2135
+ if (event.key === "Escape") {
2136
+ event.stopPropagation();
2137
+ onClose();
2138
+ }
2139
+ },
2140
+ children: [
2141
+ /* @__PURE__ */ jsxs("div", {
2142
+ className: "ag-pop-head",
2143
+ children: [
2144
+ /* @__PURE__ */ jsx("span", {
2145
+ className: "ag-tile ag-tile-mini",
2146
+ style: { "--ag-tile": "28px" },
2147
+ children: /* @__PURE__ */ jsx(NodeIcon, {
2148
+ entity,
2149
+ size: 28
2150
+ })
2151
+ }),
2152
+ /* @__PURE__ */ jsxs("span", {
2153
+ className: "ag-pop-title",
2154
+ children: [/* @__PURE__ */ jsx("span", {
2155
+ className: "ag-pop-name",
2156
+ children: entity.label
2157
+ }), (entity.sub || entity.tool?.detail) && /* @__PURE__ */ jsx("span", {
2158
+ className: "ag-pop-sub",
2159
+ children: entity.tool?.detail ?? entity.sub
2160
+ })]
2161
+ }),
2162
+ status && /* @__PURE__ */ jsx("span", {
2163
+ className: `ag-pill ag-pill-${entity.status}`,
2164
+ children: status
2165
+ }),
2166
+ /* @__PURE__ */ jsx("button", {
2167
+ ref: closeRef,
2168
+ type: "button",
2169
+ className: "ag-close",
2170
+ "aria-label": "Close",
2171
+ onClick: onClose,
2172
+ children: /* @__PURE__ */ jsx(X, { size: 14 })
2173
+ })
2174
+ ]
2175
+ }),
2176
+ summary && /* @__PURE__ */ jsx("p", {
2177
+ className: "ag-pop-summary",
2178
+ children: summary
2179
+ }),
2180
+ recent.length > 0 && /* @__PURE__ */ jsx("ul", {
2181
+ className: "ag-pop-calls",
2182
+ children: recent.map((c) => {
2183
+ const outgoing = c.from === entity.id;
2184
+ const other = entities.get(outgoing ? c.to : c.from);
2185
+ const running = c.durationMs == null;
2186
+ return /* @__PURE__ */ jsxs("li", { children: [
2187
+ /* @__PURE__ */ jsx("span", {
2188
+ className: "ag-pop-dir",
2189
+ "aria-label": outgoing ? "to" : "from",
2190
+ children: outgoing ? "→" : "←"
2191
+ }),
2192
+ other && /* @__PURE__ */ jsx("span", {
2193
+ className: "ag-tile ag-tile-micro",
2194
+ style: { "--ag-tile": "16px" },
2195
+ children: /* @__PURE__ */ jsx(NodeIcon, {
2196
+ entity: other,
2197
+ size: 16
2198
+ })
2199
+ }),
2200
+ /* @__PURE__ */ jsxs("span", {
2201
+ className: "ag-pop-call",
2202
+ children: [/* @__PURE__ */ jsx("b", { children: other?.label ?? (outgoing ? c.to : c.from) }), c.label ? ` · ${c.label}` : ""]
2203
+ }),
2204
+ /* @__PURE__ */ jsx("span", {
2205
+ className: "ag-pop-time",
2206
+ style: c.kind === "failed" ? { color: CALL_COLOR.failed } : void 0,
2207
+ children: c.kind === "failed" ? "Failed" : running ? /* @__PURE__ */ jsx(LoaderCircle, {
2208
+ size: 12,
2209
+ className: "ag-spin",
2210
+ "aria-label": "running"
2211
+ }) : fmtMs(c.durationMs)
2212
+ })
2213
+ ] }, c.id);
2214
+ })
2215
+ }),
2216
+ /* @__PURE__ */ jsxs("p", {
2217
+ className: "ag-pop-foot",
2218
+ children: [
2219
+ cost != null && /* @__PURE__ */ jsx("b", { children: fmtCost(cost) }),
2220
+ lastMinute,
2221
+ " ",
2222
+ lastMinute === 1 ? "call" : "calls",
2223
+ " in the last minute"
2224
+ ]
2225
+ })
2226
+ ]
2227
+ });
2228
+ }
2229
+ // src/workflows/canvas-theme.ts
2230
+ /** Motion is opt-OUT, so it is only used where the reader's preference can be
2231
+ * read and says nothing against it. Somewhere without `matchMedia` cannot say
2232
+ * a reader tolerates movement, so it gets none. */
2233
+ function motionAllowed() {
2234
+ return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === false;
2235
+ }
2236
+ /** React Flow adds its own .light/.dark class below the graph wrapper. Read
2237
+ * that wrapper's inherited scheme so a nested WorkspaceLayout theme wins over
2238
+ * the page root without React Flow's class influencing the answer. */
2239
+ function currentColorMode(anchor) {
2240
+ if (typeof document === "undefined" || typeof window === "undefined") return "light";
2241
+ const scheme = window.getComputedStyle(anchor ?? document.documentElement).colorScheme;
2242
+ if (scheme.startsWith("dark")) return "dark";
2243
+ if (scheme.startsWith("light")) return "light";
2244
+ return (anchor ?? document.documentElement).closest(".dark") ? "dark" : "light";
2245
+ }
2246
+ function useColorMode(wrapperRef, hasGraph) {
2247
+ const [mode, setMode] = useState(() => currentColorMode(null));
2248
+ useLayoutEffect(() => {
2249
+ const anchor = hasGraph ? wrapperRef.current : null;
2250
+ const update = () => setMode(currentColorMode(anchor));
2251
+ update();
2252
+ const observer = new MutationObserver(update);
2253
+ for (let el = anchor ?? document.documentElement; el; el = el.parentElement) observer.observe(el, {
2254
+ attributes: true,
2255
+ attributeFilter: [
2256
+ "class",
2257
+ "data-theme",
2258
+ "data-sandbox-theme",
2259
+ "style"
2260
+ ]
2261
+ });
2262
+ return () => observer.disconnect();
2263
+ }, [wrapperRef, hasGraph]);
2264
+ return mode;
2265
+ }
2266
+ /** The selected node's ring. An `outline` rather than a border or a box-shadow
2267
+ * because both of those are already spoken for — the border carries tone and
2268
+ * run status, the shadow carries the running/waiting glow — and an outline
2269
+ * composes with both without disturbing the laid-out box. */
2270
+ const SELECTION_OUTLINE = {
2271
+ outline: "2px solid hsl(var(--primary))",
2272
+ outlineOffset: 2
2273
+ };
2274
+ // src/workflows/AgentHubCanvas.tsx
1799
2275
  /**
1800
- * Recover the shape of an output preview string. Only text that *begins* like
1801
- * JSON (`{`/`[`) is parse-attempted, so ordinary prose starting with a brace-free
1802
- * sentence is never mis-parsed. A begins-like-JSON string that fails to parse
1803
- * (the host clamped it mid-value) still renders as `code`, not prose, so it keeps
1804
- * its monospace/data affordance.
1805
- *
1806
- * `condenseMarkdown` says the prose IS markdown and may be flattened for the
1807
- * preview ({@link condenseText}). It is OFF by default, and the caller must only
1808
- * turn it on for text an agent actually authored as markdown. Applied to anything
1809
- * else it does not merely look wrong — it silently REWRITES the text: a stack
1810
- * trace's `__init__.py` loses its underscores, and a shell glob `src/**` pairs
1811
- * with a later `**` and both vanish. An error message is the one string a person
1812
- * reads to find out what broke; it must reach them exactly as it was thrown.
2276
+ * The hub layout of `AgentGraph`: who asked (You), the orchestrator, the agents
2277
+ * it runs and the tools they call, with each call travelling along its edge in
2278
+ * its kind's colour. Plain DOM and SVG on a fixed grid, so it renders on the
2279
+ * server, scales to its container, and costs no graph library.
1813
2280
  */
1814
- function classifyOutput(preview, condenseMarkdown = false) {
1815
- let trimmed = preview?.trim() ?? "";
1816
- trimmed = trimmed.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, "").replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "").trimEnd();
1817
- if (trimmed === "") return { kind: "empty" };
1818
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) try {
1819
- const parsed = JSON.parse(trimmed);
1820
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1821
- const all = Object.entries(parsed);
1822
- const entries = all.slice(0, MAX_JSON_ENTRIES).map(([k, v]) => [k, scalar(v)]);
1823
- if (entries.length > 0) return {
1824
- kind: "json",
1825
- entries,
1826
- truncated: all.length > entries.length
1827
- };
2281
+ /** A call shows on its edge while it runs and for this long after it ends. */
2282
+ const ACTIVE_MS = 2600;
2283
+ const PAD_X = 84;
2284
+ const PAD_Y = 36;
2285
+ function useWidth(ref) {
2286
+ const [width, setWidth] = useState(0);
2287
+ useLayoutEffect(() => {
2288
+ const el = ref.current;
2289
+ if (!el) return;
2290
+ setWidth(el.clientWidth);
2291
+ const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width));
2292
+ observer.observe(el);
2293
+ return () => observer.disconnect();
2294
+ }, [ref]);
2295
+ return width;
2296
+ }
2297
+ function useNow(now) {
2298
+ const [tick, setTick] = useState(() => Date.now());
2299
+ useEffect(() => {
2300
+ if (now !== void 0) return;
2301
+ const timer = setInterval(() => setTick(Date.now()), 500);
2302
+ return () => clearInterval(timer);
2303
+ }, [now]);
2304
+ return now ?? tick;
2305
+ }
2306
+ function AgentHubCanvas({ nodes, edges, tools = [], calls = [], you, now: nowProp, title = "Agent graph", selectedId, onSelect, className }) {
2307
+ const wrap = useRef(null);
2308
+ const width = useWidth(wrap);
2309
+ const now = useNow(nowProp);
2310
+ const [hover, setHover] = useState(null);
2311
+ const [open, setOpen] = useState(selectedId ?? null);
2312
+ const buttons = useRef(/* @__PURE__ */ new Map());
2313
+ const closeRef = useRef(null);
2314
+ const animate = useMemo(() => motionAllowed(), []);
2315
+ const visibleCalls = useMemo(() => calls.filter((c) => c.startedAt <= now), [calls, now]);
2316
+ const layout = useMemo(() => layoutHub({
2317
+ nodes,
2318
+ edges,
2319
+ tools,
2320
+ calls,
2321
+ you: Boolean(you)
2322
+ }), [
2323
+ nodes,
2324
+ edges,
2325
+ tools,
2326
+ calls,
2327
+ you
2328
+ ]);
2329
+ const span = (m) => ({
2330
+ w: (layout.maxCol - layout.minCol) * m.cellX + 168,
2331
+ h: (layout.maxRow - layout.minRow) * m.cellY + 72 + m.hubTile + m.label
2332
+ });
2333
+ const metrics = width && width / span(HUB_METRICS).w < .75 ? HUB_METRICS_COMPACT : HUB_METRICS;
2334
+ const { w, h } = span(metrics);
2335
+ const scale = width ? Math.min(1, width / w) : 1;
2336
+ const entities = useMemo(() => {
2337
+ const map = /* @__PURE__ */ new Map();
2338
+ for (const p of layout.placements) if (p.kind === "you") map.set(p.id, {
2339
+ id: p.id,
2340
+ kind: "you",
2341
+ label: typeof you === "object" && you.label ? you.label : "You"
2342
+ });
2343
+ else if (p.kind === "tool") {
2344
+ const tool = tools.find((t) => t.id === p.id);
2345
+ map.set(p.id, {
2346
+ id: p.id,
2347
+ kind: "tool",
2348
+ label: tool.label,
2349
+ tool
2350
+ });
2351
+ } else {
2352
+ const agent = nodes.find((n) => n.id === p.id);
2353
+ map.set(p.id, {
2354
+ id: p.id,
2355
+ kind: p.kind,
2356
+ label: agent.label,
2357
+ sub: agent.model ? shortModel(agent.model) : void 0,
2358
+ status: agent.status,
2359
+ agent
2360
+ });
1828
2361
  }
2362
+ return map;
2363
+ }, [
2364
+ layout,
2365
+ nodes,
2366
+ tools,
2367
+ you
2368
+ ]);
2369
+ const at = (id) => {
2370
+ const p = layout.placements.find((q) => q.id === id);
1829
2371
  return {
1830
- kind: "code",
1831
- text: trimmed
1832
- };
1833
- } catch {
1834
- return {
1835
- kind: "code",
1836
- text: trimmed
2372
+ x: (p.col - layout.minCol) * metrics.cellX + PAD_X,
2373
+ y: (p.row - layout.minRow) * metrics.cellY + PAD_Y + metrics.hubTile / 2
1837
2374
  };
1838
- }
1839
- if (!condenseMarkdown) return {
1840
- kind: "text",
1841
- text: trimmed
1842
2375
  };
1843
- const condensed = condenseText(trimmed);
1844
- return condensed === "" ? { kind: "empty" } : {
1845
- kind: "text",
1846
- text: condensed
2376
+ const edgeState = (from, to) => {
2377
+ let latest;
2378
+ for (const c of visibleCalls) if (c.from === from && c.to === to || c.from === to && c.to === from) {
2379
+ if (!latest || c.startedAt > latest.startedAt) latest = c;
2380
+ }
2381
+ if (!latest) return void 0;
2382
+ const end = latest.durationMs == null ? Infinity : latest.startedAt + latest.durationMs;
2383
+ return now - end <= ACTIVE_MS ? latest : void 0;
2384
+ };
2385
+ const focus = hover ?? open;
2386
+ const related = useMemo(() => {
2387
+ if (!focus) return null;
2388
+ const set = /* @__PURE__ */ new Set([focus]);
2389
+ for (const l of layout.links) {
2390
+ if (l.from === focus) set.add(l.to);
2391
+ if (l.to === focus) set.add(l.from);
2392
+ }
2393
+ return set;
2394
+ }, [focus, layout]);
2395
+ const agents = nodes.length;
2396
+ const lastMinute = visibleCalls.filter((c) => now - c.startedAt <= 6e4).length;
2397
+ const costs = nodes.map((n) => n.costUsd);
2398
+ const cost = costs.every((c) => c != null) ? costs.reduce((s, c) => s + (c ?? 0), 0) : void 0;
2399
+ const chips = nodes.filter((n) => n.status === "needs-you" || n.status === "failed");
2400
+ const openNode = (id) => {
2401
+ const next = open === id ? null : id;
2402
+ setOpen(next);
2403
+ if (next) onSelect?.(next);
1847
2404
  };
2405
+ useEffect(() => {
2406
+ if (open) closeRef.current?.focus({ preventScroll: true });
2407
+ }, [open]);
2408
+ const keydown = (id) => (event) => {
2409
+ const dir = {
2410
+ ArrowLeft: [-1, 0],
2411
+ ArrowRight: [1, 0],
2412
+ ArrowUp: [0, -1],
2413
+ ArrowDown: [0, 1]
2414
+ }[event.key];
2415
+ if (event.key === "Escape") {
2416
+ setOpen(null);
2417
+ return;
2418
+ }
2419
+ if (!dir) return;
2420
+ event.preventDefault();
2421
+ const from = at(id);
2422
+ let best, bestScore = Infinity;
2423
+ for (const p of layout.placements) {
2424
+ if (p.id === id) continue;
2425
+ const to = at(p.id);
2426
+ const dx = to.x - from.x, dy = to.y - from.y;
2427
+ const along = dx * dir[0] + dy * dir[1];
2428
+ if (along <= 0) continue;
2429
+ const score = along + Math.abs(dx * dir[1] - dy * dir[0]) * 2;
2430
+ if (score < bestScore) {
2431
+ bestScore = score;
2432
+ best = p.id;
2433
+ }
2434
+ }
2435
+ if (best) buttons.current.get(best)?.focus();
2436
+ };
2437
+ const openEntity = open ? entities.get(open) : void 0;
2438
+ const popover = openEntity && (() => {
2439
+ const p = at(openEntity.id);
2440
+ const left = (p.x + 140 + 280) * scale < (width || w) ? (p.x + 40) * scale : Math.max(8, (p.x - 40) * scale - 280);
2441
+ const top = Math.max(8, (p.y - metrics.hubTile / 2 - 8) * scale);
2442
+ const summary = openEntity.kind === "you" ? typeof you === "object" ? you.summary : void 0 : openEntity.agent?.turn ?? void 0;
2443
+ return /* @__PURE__ */ jsx(HubPopover, {
2444
+ entity: openEntity,
2445
+ entities,
2446
+ calls: visibleCalls,
2447
+ now,
2448
+ left,
2449
+ top,
2450
+ summary,
2451
+ closeRef,
2452
+ onClose: () => {
2453
+ const id = open;
2454
+ setOpen(null);
2455
+ buttons.current.get(id)?.focus();
2456
+ }
2457
+ });
2458
+ })();
2459
+ const lanes = /* @__PURE__ */ new Map();
2460
+ const laneOf = (node) => {
2461
+ const n = lanes.get(node) ?? 0;
2462
+ lanes.set(node, n + 1);
2463
+ return (n % 2 ? 1 : -1) * Math.ceil(n / 2) * 6;
2464
+ };
2465
+ return /* @__PURE__ */ jsxs("section", {
2466
+ className: `ag-root ${className ?? ""}`,
2467
+ "aria-label": typeof title === "string" ? title : "Agent graph",
2468
+ children: [
2469
+ /* @__PURE__ */ jsx("style", { children: AGENT_HUB_CSS }),
2470
+ /* @__PURE__ */ jsxs("header", {
2471
+ className: "ag-head",
2472
+ children: [
2473
+ /* @__PURE__ */ jsxs("span", {
2474
+ className: "ag-title",
2475
+ children: [/* @__PURE__ */ jsx("span", {
2476
+ className: "ag-title-mark",
2477
+ "aria-hidden": true
2478
+ }), title]
2479
+ }),
2480
+ chips.map((n) => /* @__PURE__ */ jsxs("span", {
2481
+ className: `ag-chip ag-chip-${n.status}`,
2482
+ children: [
2483
+ n.status === "failed" ? "!" : "◷",
2484
+ " ",
2485
+ n.label,
2486
+ " ",
2487
+ n.status === "failed" ? "failed" : "needs you"
2488
+ ]
2489
+ }, n.id)),
2490
+ /* @__PURE__ */ jsxs("span", {
2491
+ className: "ag-stats",
2492
+ children: [
2493
+ /* @__PURE__ */ jsxs("span", { children: [
2494
+ /* @__PURE__ */ jsx("b", { children: agents }),
2495
+ " ",
2496
+ agents === 1 ? "agent" : "agents"
2497
+ ] }),
2498
+ /* @__PURE__ */ jsxs("span", { children: [
2499
+ /* @__PURE__ */ jsx("b", { children: tools.length }),
2500
+ " ",
2501
+ tools.length === 1 ? "tool" : "tools"
2502
+ ] }),
2503
+ /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("b", { children: lastMinute }), " calls/min"] }),
2504
+ /* @__PURE__ */ jsxs("span", {
2505
+ title: cost === void 0 ? "Not every agent recorded its cost" : void 0,
2506
+ children: [/* @__PURE__ */ jsx("b", { children: cost === void 0 ? "Unknown" : fmtCost(cost) }), cost === void 0 ? " cost" : ""]
2507
+ })
2508
+ ]
2509
+ })
2510
+ ]
2511
+ }),
2512
+ /* @__PURE__ */ jsxs("div", {
2513
+ ref: wrap,
2514
+ className: "ag-canvas-wrap",
2515
+ style: { height: h * scale },
2516
+ onKeyDown: (e) => {
2517
+ if (e.key === "Escape") setOpen(null);
2518
+ },
2519
+ children: [/* @__PURE__ */ jsxs("div", {
2520
+ className: "ag-canvas",
2521
+ style: {
2522
+ width: w,
2523
+ height: h,
2524
+ transform: `scale(${scale})`
2525
+ },
2526
+ children: [/* @__PURE__ */ jsx("svg", {
2527
+ className: "ag-edges",
2528
+ width: w,
2529
+ height: h,
2530
+ "aria-hidden": true,
2531
+ children: layout.links.map((l) => {
2532
+ const a = at(l.from), b = at(l.to);
2533
+ const fromHub = layout.placements.find((p) => p.id === l.from)?.kind === "hub";
2534
+ const sameCol = Math.abs(a.x - b.x) < 1;
2535
+ const { d, length } = orthogonalPath(a, b, l.from !== "you" && (fromHub || sameCol), fromHub ? laneOf(l.from) : 0);
2536
+ const live = edgeState(l.from, l.to);
2537
+ const touches = !related || related.has(l.from) && related.has(l.to);
2538
+ const forward = live ? live.from === l.from : true;
2539
+ return /* @__PURE__ */ jsxs("g", {
2540
+ className: touches ? void 0 : "ag-dimmed",
2541
+ children: [/* @__PURE__ */ jsx("path", {
2542
+ d,
2543
+ className: "ag-edge"
2544
+ }), live && /* @__PURE__ */ jsx("path", {
2545
+ d,
2546
+ className: `ag-edge-live${animate && live.kind !== "failed" ? " ag-flow" : ""}`,
2547
+ style: {
2548
+ stroke: CALL_COLOR[live.kind],
2549
+ "--ag-len": `${length}px`,
2550
+ animationDirection: forward ? "normal" : "reverse"
2551
+ }
2552
+ })]
2553
+ }, `${l.from}-${l.to}`);
2554
+ })
2555
+ }), layout.placements.map((p) => {
2556
+ const entity = entities.get(p.id);
2557
+ const { x, y } = at(p.id);
2558
+ const tile = p.kind === "hub" ? metrics.hubTile : p.kind === "you" ? metrics.tile - 8 : metrics.tile;
2559
+ return /* @__PURE__ */ jsx(HubNode, {
2560
+ entity,
2561
+ x,
2562
+ y,
2563
+ tile,
2564
+ dimmed: Boolean(related && !related.has(p.id)),
2565
+ selected: open === p.id,
2566
+ buttonRef: (el) => {
2567
+ if (el) buttons.current.set(p.id, el);
2568
+ else buttons.current.delete(p.id);
2569
+ },
2570
+ onOpen: () => openNode(p.id),
2571
+ onHover: (on) => setHover((prev) => on ? p.id : prev === p.id ? null : prev),
2572
+ onKeyDown: keydown(p.id)
2573
+ }, p.id);
2574
+ })]
2575
+ }), popover]
2576
+ }),
2577
+ /* @__PURE__ */ jsx("ul", {
2578
+ className: "ag-legend",
2579
+ "aria-label": "Edge colours",
2580
+ children: [
2581
+ "call",
2582
+ "answer",
2583
+ "needs-you",
2584
+ "failed"
2585
+ ].map((k) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
2586
+ className: "ag-legend-line",
2587
+ style: { background: CALL_COLOR[k] }
2588
+ }), CALL_LABEL[k]] }, k))
2589
+ })
2590
+ ]
2591
+ });
1848
2592
  }
1849
- /** Line-clamp utility per row budget, so `code`/`text` shapes honor `rows` the
1850
- * same way the JSON branch does (rather than always clamping to two). Covers the
1851
- * full range a card can reserve; an unsupported count falls back to the two-line
1852
- * clamp, which renders FEWER lines than the caller asked for — so a card whose
1853
- * reservation outgrows this map shows a short body inside a tall well rather
1854
- * than erroring. Extend the map when a design reserves more.
1855
- * Class names are literal so the Tailwind scanner emits each utility. */
1856
- const CLAMP_BY_ROWS = {
1857
- 1: "truncate",
1858
- 2: "line-clamp-2",
1859
- 3: "line-clamp-3",
1860
- 4: "line-clamp-4",
1861
- 5: "line-clamp-5",
1862
- 6: "line-clamp-6"
1863
- };
2593
+ // src/workflows/WorkflowGraphLazy.tsx
1864
2594
  /**
1865
- * Render a classified output preview's CONTENT (no surrounding frame — each node
1866
- * design supplies its own container/label). `rows` caps the visible lines so the
1867
- * body stays within a card's reserved output space; `tone="error"` colors it for
1868
- * a failure message.
2595
+ * Lazy + error-isolated boundary for {@link WorkflowGraph}. React Flow (~50KB)
2596
+ * is only needed on the workflow detail page and on workflow proposal cards, so
2597
+ * it's code-split out of the always-loaded app shell. The Suspense fallback
2598
+ * keeps the caller's height so layout doesn't jump; the error boundary degrades
2599
+ * to the raw YAML if the chunk fails
2600
+ * to load (offline) or React Flow throws, instead of crashing the panel tree.
1869
2601
  *
1870
- * `rows="none"` renders the body UNCLAMPED, for a container that scrolls or is
1871
- * sized by its content — a drawer, a detail panel, a selection overlay. It is a
1872
- * word rather than a large number because there is no number that means "do not
1873
- * clamp": a count outside {@link CLAMP_BY_ROWS} falls back to two lines, so
1874
- * `rows={99}` asks for everything and silently shows the least.
2602
+ * The chunk import auto-retries a transient failure (a flaky network or an
2603
+ * HMR/cache miss) before degrading, and the fallback offers a manual retry that
2604
+ * re-attempts the import — so a recoverable failure isn't permanently pinned to
2605
+ * the YAML view. A chunk that is gone for good (e.g. an old hash after a deploy)
2606
+ * still degrades to the fully-legible YAML; the host can recover those globally
2607
+ * via a Vite `vite:preloadError` reload guard.
1875
2608
  */
1876
- function NodeOutputBody({ shape, rows = 2, tone = "default" }) {
1877
- const unclamped = rows === "none";
1878
- const clampClass = unclamped ? "" : CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
1879
- const codeWrap = rows === 1 ? "" : "whitespace-pre-wrap break-all";
1880
- if (shape.kind === "empty") return null;
1881
- if (shape.kind === "json") {
1882
- const overflow = unclamped ? shape.truncated : shape.truncated || shape.entries.length > rows;
1883
- const visible = unclamped ? shape.entries : shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
1884
- const isError = tone === "error";
1885
- const keyStyle = isError ? ERROR_TEXT : void 0;
1886
- const keyClass = isError ? "" : "text-muted-foreground";
1887
- const valueClass = isError ? "" : "text-foreground";
1888
- return /* @__PURE__ */ jsxs("dl", {
1889
- className: "space-y-0.5 font-mono text-[10.5px] leading-snug",
1890
- children: [visible.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
1891
- className: "flex gap-1.5",
1892
- children: [/* @__PURE__ */ jsx("dt", {
1893
- className: `max-w-[45%] shrink-0 truncate ${keyClass}`,
1894
- style: keyStyle,
1895
- title: key,
1896
- children: key
1897
- }), /* @__PURE__ */ jsx("dd", {
1898
- className: `min-w-0 flex-1 truncate ${valueClass}`,
1899
- style: isError ? ERROR_TEXT : void 0,
1900
- title: value,
1901
- children: value
1902
- })]
1903
- }, key)), overflow && /* @__PURE__ */ jsx("div", {
1904
- className: `text-[10px] ${keyClass}`,
1905
- style: keyStyle,
1906
- children: "…"
1907
- })]
2609
+ /** Re-run `factory` up to `retries` times with a short backoff before giving up.
2610
+ * Covers a transient dynamic-import rejection (network blip, a chunk briefly
2611
+ * unavailable behind a CDN) without bothering the user. Exported for tests. */
2612
+ function retryImport(factory, retries = 2, delayMs = 350) {
2613
+ return factory().catch((err) => {
2614
+ if (retries <= 0) throw err;
2615
+ return new Promise((resolve, reject) => {
2616
+ setTimeout(() => retryImport(factory, retries - 1, delayMs).then(resolve, reject), delayMs);
1908
2617
  });
2618
+ });
2619
+ }
2620
+ function lazyWorkflowGraph() {
2621
+ return lazy(() => retryImport(() => import("./WorkflowGraph-CbIFufIK.js").then((m) => ({ default: m.WorkflowGraph }))));
2622
+ }
2623
+ var GraphErrorBoundary = class extends Component {
2624
+ state = { hasError: false };
2625
+ static getDerivedStateFromError() {
2626
+ return { hasError: true };
1909
2627
  }
1910
- if (shape.kind === "code") return /* @__PURE__ */ jsx("pre", {
1911
- className: `${clampClass} ${codeWrap} font-mono text-[10.5px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
1912
- style: tone === "error" ? ERROR_TEXT : void 0,
1913
- title: shape.text,
1914
- children: shape.text
2628
+ render() {
2629
+ return this.state.hasError ? this.props.fallback : this.props.children;
2630
+ }
2631
+ };
2632
+ /** Graceful degradation when the graph can't render: the workflow is still
2633
+ * fully legible as YAML, with a retry for a recoverable load failure. */
2634
+ function GraphFallback({ yaml, className, onRetry }) {
2635
+ return /* @__PURE__ */ jsxs("div", {
2636
+ className: `overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
2637
+ children: [/* @__PURE__ */ jsxs("div", {
2638
+ className: "mb-1 flex items-center justify-between gap-2",
2639
+ children: [/* @__PURE__ */ jsx("p", {
2640
+ className: "text-muted-foreground text-xs",
2641
+ children: "Couldn't render the graph — showing the definition."
2642
+ }), /* @__PURE__ */ jsx("button", {
2643
+ type: "button",
2644
+ onClick: onRetry,
2645
+ className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-muted-foreground text-xs transition hover:text-foreground",
2646
+ children: "Retry"
2647
+ })]
2648
+ }), /* @__PURE__ */ jsx("pre", {
2649
+ className: "text-foreground text-xs",
2650
+ children: /* @__PURE__ */ jsx("code", { children: yaml })
2651
+ })]
1915
2652
  });
1916
- return /* @__PURE__ */ jsx("p", {
1917
- className: `${clampClass} text-[11px] leading-snug ${tone === "error" ? "" : "text-foreground"}`,
1918
- style: tone === "error" ? ERROR_TEXT : void 0,
1919
- title: shape.text,
1920
- children: shape.text
2653
+ }
2654
+ function WorkflowGraph(props) {
2655
+ const [attempt, setAttempt] = useState(0);
2656
+ const Impl = useMemo(() => lazyWorkflowGraph(), [attempt]);
2657
+ return /* @__PURE__ */ jsx(GraphErrorBoundary, {
2658
+ fallback: /* @__PURE__ */ jsx(GraphFallback, {
2659
+ ...props,
2660
+ onRetry: () => setAttempt((a) => a + 1)
2661
+ }),
2662
+ children: /* @__PURE__ */ jsx(Suspense, {
2663
+ fallback: /* @__PURE__ */ jsx("div", {
2664
+ className: `flex items-center justify-center rounded-lg border border-border border-dashed text-text-muted text-xs ${props.className ?? ""}`,
2665
+ children: "Loading graph…"
2666
+ }),
2667
+ children: /* @__PURE__ */ jsx(Impl, { ...props })
2668
+ })
2669
+ }, `${props.yaml}::${attempt}`);
2670
+ }
2671
+ // src/workflows/AgentGraph.tsx
2672
+ /**
2673
+ * `AgentGraph` — the topology of a multi-agent run, for a run page's side pane.
2674
+ *
2675
+ * Lazy and error-isolated like `WorkflowGraph`: React Flow and ELK load on
2676
+ * first render, a transient chunk failure retries, and a graph that cannot
2677
+ * render degrades to a plain list of the agents (still selectable) instead of
2678
+ * taking the pane down.
2679
+ */
2680
+ function lazyCanvas() {
2681
+ return lazy(() => retryImport(() => import("./AgentGraphCanvas-BHihTTsl.js").then((m) => ({ default: m.AgentGraphCanvas }))));
2682
+ }
2683
+ /** The agents as a list, for when the canvas cannot load. */
2684
+ function AgentListFallback({ nodes, selectedId, onSelect, className, onRetry }) {
2685
+ return /* @__PURE__ */ jsxs("div", {
2686
+ className: `overflow-auto rounded-lg border border-border bg-background p-2 ${className ?? ""}`,
2687
+ children: [/* @__PURE__ */ jsxs("div", {
2688
+ className: "mb-1 flex items-center justify-between gap-2",
2689
+ children: [/* @__PURE__ */ jsx("p", {
2690
+ className: "text-muted-foreground text-xs",
2691
+ children: "Couldn't draw the graph — showing the agents."
2692
+ }), /* @__PURE__ */ jsx("button", {
2693
+ type: "button",
2694
+ onClick: onRetry,
2695
+ className: "shrink-0 rounded border border-border px-1.5 py-0.5 text-muted-foreground text-xs transition hover:text-foreground",
2696
+ children: "Retry"
2697
+ })]
2698
+ }), /* @__PURE__ */ jsx("ul", {
2699
+ className: "space-y-0.5",
2700
+ children: nodes.map((n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
2701
+ type: "button",
2702
+ "aria-pressed": selectedId === n.id,
2703
+ onClick: () => onSelect?.(n.id),
2704
+ 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",
2705
+ children: [/* @__PURE__ */ jsx("span", {
2706
+ className: "truncate",
2707
+ children: n.label
2708
+ }), /* @__PURE__ */ jsx("span", {
2709
+ className: "shrink-0 text-muted-foreground",
2710
+ children: n.status
2711
+ })]
2712
+ }) }, n.id))
2713
+ })]
1921
2714
  });
1922
2715
  }
1923
- export { triggerNodeIndex as A, fmtTokens as B, COMPACT_NODE_SIZE as C, buildWorkflowGraph as D, branchNodeId as E, parseActionPath as F, shortModel as I, clampPreview as L, worstSeverity as M, actionPathLabel as N, isStructuralKind as O, humanizeIdentifier as P, fmtCost as R, statusBorder as S, actionNodeId as T, emptySlotLabel as _, NodeMark as a, problemSurface as b, RESTING_BORDER as c, STATUS_LABEL as d, STATUS_PILL as f, edgeColor as g, StatusPill$1 as h, KIND_ICON as i, wfEdgeId as j, triggerNodeId as k, RESTING_SHADOW as l, StatusFooter as m, classifyOutput as n, ProblemMarker as o, STATUS_TONE as p, condenseText as r, ProblemMessages as s, NodeOutputBody as t, STATUS_COLOR as u, kindAccent as v, TRIGGER_NODE_ID as w, problemTitle as x, problemBorder as y, fmtDuration as z };
2716
+ function AgentGraph(props) {
2717
+ if (props.layout === "hub") {
2718
+ if (props.nodes.length === 0 && props.emptyState) return /* @__PURE__ */ jsx(Fragment, { children: props.emptyState });
2719
+ return /* @__PURE__ */ jsx(AgentHubCanvas, { ...props });
2720
+ }
2721
+ return /* @__PURE__ */ jsx(AgentGraphTree, { ...props });
2722
+ }
2723
+ function AgentGraphTree(props) {
2724
+ const [attempt, setAttempt] = useState(0);
2725
+ const Canvas = useMemo(() => lazyCanvas(), [attempt]);
2726
+ return /* @__PURE__ */ jsx(GraphErrorBoundary, {
2727
+ fallback: /* @__PURE__ */ jsx(AgentListFallback, {
2728
+ ...props,
2729
+ onRetry: () => setAttempt((a) => a + 1)
2730
+ }),
2731
+ children: /* @__PURE__ */ jsx(Suspense, {
2732
+ fallback: /* @__PURE__ */ jsx("div", {
2733
+ className: `flex items-center justify-center rounded-lg border border-border border-dashed text-muted-foreground text-xs ${props.className ?? ""}`,
2734
+ children: "Loading graph…"
2735
+ }),
2736
+ children: /* @__PURE__ */ jsx(Canvas, { ...props })
2737
+ })
2738
+ }, attempt);
2739
+ }
2740
+ export { buildWorkflowGraph as A, clampPreview as B, problemSurface as C, TRIGGER_NODE_ID as D, COMPACT_NODE_SIZE as E, worstSeverity as F, fmtDuration as H, actionPathLabel as I, humanizeIdentifier as L, triggerNodeId as M, triggerNodeIndex as N, actionNodeId as O, wfEdgeId as P, parseActionPath as R, problemBorder as S, statusBorder as T, fmtTokenTotal as U, fmtCost as V, fmtTokens as W, StatusFooter as _, useColorMode as a, emptySlotLabel as b, NodeMark as c, RESTING_BORDER as d, RESTING_SHADOW as f, STATUS_TONE as g, STATUS_PILL as h, motionAllowed as i, isStructuralKind as j, branchNodeId as k, ProblemMarker as l, STATUS_LABEL as m, WorkflowGraph as n, KIND_ACCENT as o, STATUS_COLOR as p, SELECTION_OUTLINE as r, KIND_ICON as s, AgentGraph as t, ProblemMessages as u, StatusPill$1 as v, problemTitle as w, kindAccent as x, edgeColor as y, shortModel as z };