@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.
- package/dist/{node-output-udnwhShN.js → AgentGraph-Beld8p6x.js} +1021 -204
- package/dist/AgentGraph-DL-QLjkw.d.ts +132 -0
- package/dist/AgentGraphCanvas-BHihTTsl.js +1203 -0
- package/dist/{WorkflowGraph-DXuWXMw9.js → WorkflowGraph-CbIFufIK.js} +4 -56
- package/dist/{chat-BYTVLZQW.js → chat-4OZD23RA.js} +1 -1
- package/dist/chat.js +1 -1
- package/dist/{diff-view-A0O0Z_SR.js → diff-view-B9mNeTLF.js} +2 -1
- package/dist/elk-loader-Dl9S7jQk.js +12 -0
- package/dist/globals.css +109 -5
- package/dist/{index-C5h5T0nb.d.ts → index-D6gtWRHq.d.ts} +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/node-output-DlySTwxr.js +178 -0
- package/dist/reveal-in-scroller-CzKwuqCG.js +25 -0
- package/dist/{run-CliTxkfw.js → run-CC-DESx8.js} +79 -15
- package/dist/run.d.ts +1 -1
- package/dist/run.js +1 -1
- package/dist/styles.css +109 -5
- package/dist/{tree-data-YXtcK6Ld.js → tree-data-Dmiu8aMf.js} +3 -1
- package/dist/vault-tree.js +1 -1
- package/dist/workbench/diff.js +1 -1
- package/dist/{workbench-B3JL1qbt.js → workbench-DOgYO3Zg.js} +1 -1
- package/dist/workbench.js +2 -2
- package/dist/workflows.d.ts +3 -2
- package/dist/workflows.js +3 -82
- package/dist/{workspace-DRhi6S3o.js → workspace-C9TH6A2O.js} +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +3 -3
|
@@ -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 {
|
|
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/
|
|
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
|
-
*
|
|
1756
|
-
*
|
|
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
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
*/
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
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
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
1831
|
-
|
|
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
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
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
|
-
*
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
-
|
|
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 };
|