@tangle-network/sandbox-ui 1.1.10 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AgentGraph-DxAeQQch.d.ts → AgentGraph-B4uWLXlD.d.ts} +12 -17
- package/dist/AgentGraph-P7V_oEOv.js +1369 -0
- package/dist/{AgentGraphCanvas-CMdOlSlh.js → AgentGraphCanvas-BwFppclI.js} +560 -383
- package/dist/{WorkflowGraph-DZ3xejKE.js → WorkflowGraph-BcGSF2W_.js} +3 -2
- package/dist/canvas-theme-DGd0fm1H.js +58 -0
- package/dist/{chat-4OZD23RA.js → chat-BrlLa4rg.js} +2 -2
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +1 -1
- package/dist/{dashboard-DZ6FcC_Z.js → dashboard-FMrtIe9S.js} +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/globals.css +410 -239
- package/dist/{index-ucJHuuVZ.d.ts → index-5zOVKgDT.d.ts} +2 -2
- package/dist/index-BK8KKLNY.d.ts +26 -0
- package/dist/{index-72uBu5lu.d.ts → index-D9-IiEpQ.d.ts} +21 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +7 -6
- package/dist/latest-events-MXv4SYzc.js +190 -0
- package/dist/{AgentGraph-DJEQrKTs.js → node-ui-C1WEjoGO.js} +40 -1194
- package/dist/primitives-CCdx5Hh_.js +23 -0
- package/dist/primitives.d.ts +2 -2
- package/dist/primitives.js +1 -1
- package/dist/{run-Qern6muT.js → run-ByG1psJn.js} +161 -60
- package/dist/run.d.ts +2 -2
- package/dist/run.js +3 -1
- package/dist/styles.css +410 -239
- package/dist/tailwind.css +71 -43
- package/dist/{workbench-Ca6ZqAs5.js → workbench-BVnZNhP2.js} +1 -1
- package/dist/workbench.js +1 -1
- package/dist/workflows.d.ts +3 -7
- package/dist/workflows.js +2 -1
- package/package.json +9 -8
- package/dist/elk-loader-Dl9S7jQk.js +0 -12
- package/dist/index-CFJ1iJhu.d.ts +0 -26
- package/dist/primitives-B-76QdOU.js +0 -23
|
@@ -1,302 +1,10 @@
|
|
|
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
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { r as clampPreview } from "./AgentGraph-P7V_oEOv.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { AlertTriangle, Bell, Box, Cable, Camera, Circle, Clock, Code, GitBranch, Repeat, ScanSearch, Sparkles, UserCheck, Webhook, Zap } from "lucide-react";
|
|
6
|
+
import { RunStatusIcon, statusMeta } from "@tangle-network/ui/run";
|
|
7
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
|
-
}
|
|
300
8
|
// src/workflows/cron.ts
|
|
301
9
|
/**
|
|
302
10
|
* Cron → English for a schedule trigger's node label. A raw `0 9 * * 1-5` tells
|
|
@@ -1556,34 +1264,9 @@ function buildWorkflowGraph(yaml, options) {
|
|
|
1556
1264
|
};
|
|
1557
1265
|
}
|
|
1558
1266
|
// src/workflows/node-ui.tsx
|
|
1559
|
-
/**
|
|
1560
|
-
* Shared presentational atoms for a workflow graph node — the type glyph, the
|
|
1561
|
-
* status color/label/badge maps, and the edge coloring. One source of truth so
|
|
1562
|
-
* the node card and the edges that point at it read as one system.
|
|
1563
|
-
*
|
|
1564
|
-
* Colors resolve to the SEMANTIC surface tokens (`--surface-success-text`,
|
|
1565
|
-
* `--surface-danger-text`, …), which carry a light AND a dark value tuned for
|
|
1566
|
-
* contrast against the card. A literal palette hex (`#22c55e`) or a stock
|
|
1567
|
-
* Tailwind shade (`text-red-400`) carries ONE value, so it can only ever hold its
|
|
1568
|
-
* contrast in one of the two themes — never use one for a status color.
|
|
1569
|
-
*
|
|
1570
|
-
* Inline `style` colors here use the RAW brand tokens (`hsl(var(--primary))`,
|
|
1571
|
-
* `var(--surface-…)`) — NOT the `--color-*` @theme aliases, which are undefined
|
|
1572
|
-
* in a consumer that only imports `tokens.css` (e.g. platform-web), where a
|
|
1573
|
-
* `var(--color-*)` would silently resolve to nothing. See the edge-color test.
|
|
1574
|
-
*/
|
|
1575
1267
|
/** Track color behind a progress bar — a faint wash of the muted token. */
|
|
1576
1268
|
const MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
|
|
1577
|
-
/**
|
|
1578
|
-
* critique's first blocker: kind and state shared the same channel and hue
|
|
1579
|
-
* family (sandbox-kind was green like DONE; structural-kind amber like
|
|
1580
|
-
* WAITING; agent-kind indigo like RUNNING). The split:
|
|
1581
|
-
* KIND = the glyph-tile tint + a 2px top rule. Hues chosen to NEVER
|
|
1582
|
-
* collide with the state family (green/amber/red are reserved
|
|
1583
|
-
* for state alone).
|
|
1584
|
-
* STATE = the border, ring, glow and fill (see statusBorder).
|
|
1585
|
-
* Kind hue duplicates are deliberate: shape + glyph carry identity; the
|
|
1586
|
-
* tint is a hint, not the signal. */
|
|
1269
|
+
/** Glyph accents identify the node kind; ui's status metadata owns run state. */
|
|
1587
1270
|
const KIND_ACCENT = {
|
|
1588
1271
|
"agent.run": "hsl(var(--primary))",
|
|
1589
1272
|
"script.run": "var(--surface-info-text)",
|
|
@@ -1593,8 +1276,7 @@ const KIND_ACCENT = {
|
|
|
1593
1276
|
"line.send": "var(--surface-info-text)",
|
|
1594
1277
|
notify: "var(--surface-info-text)"
|
|
1595
1278
|
};
|
|
1596
|
-
/** Structural
|
|
1597
|
-
* reserved for `waiting`, full stop. */
|
|
1279
|
+
/** Structural nodes stay neutral; approval status owns warning ink. */
|
|
1598
1280
|
const TONE_ACCENT = {
|
|
1599
1281
|
trigger: "hsl(var(--primary))",
|
|
1600
1282
|
structural: "hsl(var(--muted-foreground))",
|
|
@@ -1605,120 +1287,39 @@ const TONE_ACCENT = {
|
|
|
1605
1287
|
function kindAccent(tone, kind) {
|
|
1606
1288
|
return (kind !== void 0 ? KIND_ACCENT[kind] : void 0) ?? TONE_ACCENT[tone];
|
|
1607
1289
|
}
|
|
1608
|
-
/**
|
|
1609
|
-
|
|
1610
|
-
* The card must sit ON the canvas, not in it: a real two-layer shadow plus a
|
|
1611
|
-
* 1px inner top highlight, foreground-token based so both themes get depth
|
|
1612
|
-
* without a dark-mode black box. */
|
|
1613
|
-
const RESTING_SHADOW = "inset 0 1px 0 hsl(var(--foreground) / 0.07), 0 1px 2px hsl(var(--foreground) / 0.10), 0 12px 32px -14px hsl(var(--foreground) / 0.40)";
|
|
1290
|
+
/** Brand shadow channels stay black in dark mode. */
|
|
1291
|
+
const RESTING_SHADOW = "inset 0 1px 0 hsl(var(--foreground) / 0.06), 0 1px 3px hsl(var(--shadow-color) / 0.08)";
|
|
1614
1292
|
/** The resting card border: visible against the canvas in BOTH themes —
|
|
1615
1293
|
* `--border` vanished on dark. */
|
|
1616
1294
|
const RESTING_BORDER = "hsl(var(--foreground) / 0.14)";
|
|
1617
|
-
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
* neutral so the eye finds the front, not the history. */
|
|
1295
|
+
/** Workflow waiting means human approval; peer waiting remains neutral in agent views. */
|
|
1296
|
+
const workflowStatus = (status) => status === "waiting" ? "needs-you" : status === "skipped" ? "stopped" : status;
|
|
1297
|
+
const workflowMeta = (status) => statusMeta(workflowStatus(status));
|
|
1298
|
+
const workflowStatuses = [
|
|
1299
|
+
"queued",
|
|
1300
|
+
"running",
|
|
1301
|
+
"waiting",
|
|
1302
|
+
"succeeded",
|
|
1303
|
+
"failed",
|
|
1304
|
+
"skipped"
|
|
1305
|
+
];
|
|
1306
|
+
`${statusMeta("running").token}`;
|
|
1307
|
+
const STATUS_COLOR = Object.fromEntries(workflowStatuses.map((status) => [status, `var(${workflowMeta(status).token})`]));
|
|
1308
|
+
const STATUS_LABEL = Object.fromEntries(workflowStatuses.map((status) => [status, workflowMeta(status).label]));
|
|
1309
|
+
/** Retained public styling contract, derived from the owner's status metadata. */
|
|
1310
|
+
const STATUS_PILL = Object.fromEntries(workflowStatuses.map((status) => [status, {
|
|
1311
|
+
color: STATUS_COLOR[status],
|
|
1312
|
+
background: "transparent",
|
|
1313
|
+
borderColor: "hsl(var(--border))"
|
|
1314
|
+
}]));
|
|
1638
1315
|
function edgeColor(status) {
|
|
1639
|
-
|
|
1640
|
-
if (status === "waiting") return STATUS_WAITING;
|
|
1641
|
-
if (status === "failed") return STATUS_FAILED;
|
|
1642
|
-
return "color-mix(in srgb, hsl(var(--muted-foreground)) 38%, transparent)";
|
|
1316
|
+
return status === "failed" || status === "waiting" ? STATUS_COLOR[status] : "color-mix(in srgb, hsl(var(--muted-foreground)) 80%, transparent)";
|
|
1643
1317
|
}
|
|
1644
|
-
const STATUS_LABEL = {
|
|
1645
|
-
queued: "Queued",
|
|
1646
|
-
running: "Running",
|
|
1647
|
-
waiting: "Waiting on you",
|
|
1648
|
-
succeeded: "Done",
|
|
1649
|
-
failed: "Failed",
|
|
1650
|
-
skipped: "Not taken"
|
|
1651
|
-
};
|
|
1652
|
-
/** @deprecated `StatusPill` now renders ui's StatusPill from `STATUS_TONE`;
|
|
1653
|
-
* kept for consumers that styled their own pill from it.
|
|
1654
|
-
* Status pill styling — a tinted well in the status color. Built from the
|
|
1655
|
-
* semantic surface trio (bg/border/text) so the pill keeps its contrast in both
|
|
1656
|
-
* themes rather than washing out in one of them. */
|
|
1657
|
-
const STATUS_PILL = {
|
|
1658
|
-
queued: {
|
|
1659
|
-
background: "color-mix(in srgb, hsl(var(--muted-foreground)) 12%, transparent)",
|
|
1660
|
-
color: "hsl(var(--muted-foreground))",
|
|
1661
|
-
borderColor: "color-mix(in srgb, hsl(var(--muted-foreground)) 28%, transparent)"
|
|
1662
|
-
},
|
|
1663
|
-
running: {
|
|
1664
|
-
background: "color-mix(in srgb, hsl(var(--primary)) 14%, transparent)",
|
|
1665
|
-
color: "hsl(var(--primary))",
|
|
1666
|
-
borderColor: "color-mix(in srgb, hsl(var(--primary)) 40%, transparent)"
|
|
1667
|
-
},
|
|
1668
|
-
waiting: {
|
|
1669
|
-
background: "var(--surface-warning-bg)",
|
|
1670
|
-
color: "var(--surface-warning-text)",
|
|
1671
|
-
borderColor: "var(--surface-warning-border)"
|
|
1672
|
-
},
|
|
1673
|
-
succeeded: {
|
|
1674
|
-
background: "var(--surface-success-bg)",
|
|
1675
|
-
color: "var(--surface-success-text)",
|
|
1676
|
-
borderColor: "var(--surface-success-border)"
|
|
1677
|
-
},
|
|
1678
|
-
failed: {
|
|
1679
|
-
background: "var(--surface-danger-bg)",
|
|
1680
|
-
color: "var(--surface-danger-text)",
|
|
1681
|
-
borderColor: "var(--surface-danger-border)"
|
|
1682
|
-
},
|
|
1683
|
-
skipped: {
|
|
1684
|
-
background: "transparent",
|
|
1685
|
-
color: "hsl(var(--muted-foreground))",
|
|
1686
|
-
borderColor: "color-mix(in srgb, hsl(var(--muted-foreground)) 28%, transparent)"
|
|
1687
|
-
}
|
|
1688
|
-
};
|
|
1689
|
-
/** The card's border + ring once a node has live run state. The emphasis
|
|
1690
|
-
* law (the critique's second blocker, inverted): the EXCEPTION is the
|
|
1691
|
-
* loudest thing on the canvas —
|
|
1692
|
-
* failed saturated danger + a halo wider than running's
|
|
1693
|
-
* waiting amber + glow (the node the viewer must act on)
|
|
1694
|
-
* running primary + glow (the live front)
|
|
1695
|
-
* succeeded QUIET: a neutral border, no ring, no glow — done is the
|
|
1696
|
-
* resting state of a healthy run and must not shout */
|
|
1697
1318
|
function statusBorder(status) {
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
};
|
|
1703
|
-
case "running":
|
|
1704
|
-
case "waiting": {
|
|
1705
|
-
const color = STATUS_COLOR[status];
|
|
1706
|
-
return {
|
|
1707
|
-
borderColor: color,
|
|
1708
|
-
boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 45%, transparent), 0 0 24px -6px ${color}`
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
case "queued": return { borderColor: "hsl(var(--border))" };
|
|
1712
|
-
case "skipped": return {
|
|
1713
|
-
borderColor: "hsl(var(--muted-foreground) / 0.45)",
|
|
1714
|
-
borderStyle: "dashed",
|
|
1715
|
-
opacity: .5
|
|
1716
|
-
};
|
|
1717
|
-
case "succeeded": return {
|
|
1718
|
-
borderColor: RESTING_BORDER,
|
|
1719
|
-
boxShadow: RESTING_SHADOW
|
|
1720
|
-
};
|
|
1721
|
-
}
|
|
1319
|
+
return {
|
|
1320
|
+
borderColor: status === "failed" || status === "waiting" ? STATUS_COLOR[status] : RESTING_BORDER,
|
|
1321
|
+
boxShadow: RESTING_SHADOW
|
|
1322
|
+
};
|
|
1722
1323
|
}
|
|
1723
1324
|
/** Determinate progress fraction for a node's status: queued reads near-empty,
|
|
1724
1325
|
* running and waiting a partial (the run reached this node but has not finished
|
|
@@ -1879,25 +1480,15 @@ const STATUS_TONE = {
|
|
|
1879
1480
|
* tone and label as every other status in the product. `shrink-0` because the
|
|
1880
1481
|
* longest label is two words ("Waiting on you") and the pill is often nested a
|
|
1881
1482
|
* level below the flex row it sits in. */
|
|
1882
|
-
function StatusPill
|
|
1883
|
-
|
|
1884
|
-
className: "inline-flex shrink-0 items-center gap-1 rounded-full
|
|
1885
|
-
style: {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
borderColor: "color-mix(in srgb, hsl(var(--muted-foreground)) 26%, transparent)"
|
|
1889
|
-
},
|
|
1890
|
-
children: [/* @__PURE__ */ jsx(Check, {
|
|
1891
|
-
className: "size-2.5",
|
|
1892
|
-
strokeWidth: 3,
|
|
1483
|
+
function StatusPill({ status }) {
|
|
1484
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
1485
|
+
className: "inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs",
|
|
1486
|
+
style: { color: STATUS_COLOR[status] },
|
|
1487
|
+
children: [/* @__PURE__ */ jsx(RunStatusIcon, {
|
|
1488
|
+
status: workflowStatus(status),
|
|
1893
1489
|
"aria-hidden": true
|
|
1894
1490
|
}), STATUS_LABEL[status]]
|
|
1895
1491
|
});
|
|
1896
|
-
return /* @__PURE__ */ jsx(StatusPill, {
|
|
1897
|
-
tone: STATUS_TONE[status],
|
|
1898
|
-
className: "shrink-0",
|
|
1899
|
-
children: STATUS_LABEL[status]
|
|
1900
|
-
});
|
|
1901
1492
|
}
|
|
1902
1493
|
/**
|
|
1903
1494
|
* The run status FOOTER: a progress bar (queued = near-empty, running = pulsing
|
|
@@ -1992,749 +1583,4 @@ function NodeMark({ kind, provider, model, accent, tile }) {
|
|
|
1992
1583
|
})
|
|
1993
1584
|
});
|
|
1994
1585
|
}
|
|
1995
|
-
|
|
1996
|
-
/**
|
|
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.
|
|
2000
|
-
*/
|
|
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
|
-
});
|
|
2041
|
-
}
|
|
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
|
-
});
|
|
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
|
|
2275
|
-
/**
|
|
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.
|
|
2280
|
-
*/
|
|
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
|
-
});
|
|
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);
|
|
2371
|
-
return {
|
|
2372
|
-
x: (p.col - layout.minCol) * metrics.cellX + PAD_X,
|
|
2373
|
-
y: (p.row - layout.minRow) * metrics.cellY + PAD_Y + metrics.hubTile / 2
|
|
2374
|
-
};
|
|
2375
|
-
};
|
|
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);
|
|
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
|
-
});
|
|
2592
|
-
}
|
|
2593
|
-
// src/workflows/WorkflowGraphLazy.tsx
|
|
2594
|
-
/**
|
|
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.
|
|
2601
|
-
*
|
|
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.
|
|
2608
|
-
*/
|
|
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);
|
|
2617
|
-
});
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
2620
|
-
function lazyWorkflowGraph() {
|
|
2621
|
-
return lazy(() => retryImport(() => import("./WorkflowGraph-DZ3xejKE.js").then((m) => ({ default: m.WorkflowGraph }))));
|
|
2622
|
-
}
|
|
2623
|
-
var GraphErrorBoundary = class extends Component {
|
|
2624
|
-
state = { hasError: false };
|
|
2625
|
-
static getDerivedStateFromError() {
|
|
2626
|
-
return { hasError: true };
|
|
2627
|
-
}
|
|
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
|
-
})]
|
|
2652
|
-
});
|
|
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-CMdOlSlh.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
|
-
})]
|
|
2714
|
-
});
|
|
2715
|
-
}
|
|
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 };
|
|
1586
|
+
export { worstSeverity as A, actionNodeId as C, triggerNodeId as D, isStructuralKind as E, humanizeIdentifier as M, parseActionPath as N, triggerNodeIndex as O, shortModel as P, TRIGGER_NODE_ID as S, buildWorkflowGraph as T, problemBorder as _, ProblemMessages as a, statusBorder as b, STATUS_COLOR as c, STATUS_TONE as d, StatusFooter as f, kindAccent as g, emptySlotLabel as h, ProblemMarker as i, actionPathLabel as j, wfEdgeId as k, STATUS_LABEL as l, edgeColor as m, KIND_ICON as n, RESTING_BORDER as o, StatusPill as p, NodeMark as r, RESTING_SHADOW as s, KIND_ACCENT as t, STATUS_PILL as u, problemSurface as v, branchNodeId as w, COMPACT_NODE_SIZE as x, problemTitle as y };
|