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