@tangle-network/sandbox-ui 1.1.8 → 2.0.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/README.md +1 -1
- package/dist/{AgentGraph-DxAeQQch.d.ts → AgentGraph-B4uWLXlD.d.ts} +12 -17
- package/dist/AgentGraph-C1DsA-Rd.js +1369 -0
- package/dist/{AgentGraphCanvas-CMdOlSlh.js → AgentGraphCanvas-Bcxq-e6E.js} +557 -383
- package/dist/{WorkflowGraph-DZ3xejKE.js → WorkflowGraph-BDjc7V_U.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-ClKA3opr.js → dashboard-FMrtIe9S.js} +3 -3
- package/dist/dashboard.js +1 -1
- package/dist/globals.css +2366 -1801
- 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 +5 -6
- package/dist/index.js +8 -291
- package/dist/integrations.d.ts +2 -1
- package/dist/integrations.js +6 -1
- package/dist/latest-events-B34ElSTE.js +190 -0
- package/dist/{AgentGraph-DJEQrKTs.js → node-ui-Bfnh08U3.js} +40 -1194
- package/dist/primitives-CCdx5Hh_.js +23 -0
- package/dist/primitives.d.ts +2 -3
- package/dist/primitives.js +1 -2
- package/dist/{run-Qern6muT.js → run-BqpdpP1K.js} +161 -60
- package/dist/run.d.ts +2 -2
- package/dist/run.js +3 -1
- package/dist/styles.css +2366 -1801
- package/dist/tailwind.css +74 -60
- package/dist/tokens.css +452 -851
- package/dist/{workbench-DOgYO3Zg.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 +11 -10
- package/dist/elk-loader-Dl9S7jQk.js +0 -12
- package/dist/index-BAEvGqrn.d.ts +0 -26
- package/dist/primitives-CiwDp4-0.js +0 -32
- package/dist/rolldown-runtime-782esTHO.js +0 -26
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { t as MOTION_CONTROL } from "./motion-Bn7ESHrj.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
2
|
+
import { d as quietActivity, f as syncAmbient, g as useRunNow, h as useMotionAllowed, i as fmtCost, m as useAttention, o as fmtEventTime, p as tokenMilliseconds, s as fmtTokenTotal, u as ambientStyle } from "./AgentGraph-C1DsA-Rd.js";
|
|
3
|
+
import { t as LatestEvents } from "./latest-events-B34ElSTE.js";
|
|
4
|
+
import { P as shortModel, m as edgeColor, o as RESTING_BORDER, r as NodeMark, s as RESTING_SHADOW, t as KIND_ACCENT } from "./node-ui-Bfnh08U3.js";
|
|
5
|
+
import { i as loadElk, r as useColorMode, t as SELECTION_OUTLINE } from "./canvas-theme-DGd0fm1H.js";
|
|
4
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import {
|
|
7
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { Users, Wrench } from "lucide-react";
|
|
9
|
+
import { RunStatusIcon, statusMeta } from "@tangle-network/ui/run";
|
|
7
10
|
import { Background, BaseEdge, Controls, EdgeLabelRenderer, Handle, MarkerType, MiniMap, Position, ReactFlow, useInternalNode } from "@xyflow/react";
|
|
8
11
|
import "@xyflow/react/dist/style.css";
|
|
9
12
|
// src/workflows/agent-graph-layout.ts
|
|
10
13
|
/** Every agent card's size. Fixed, so a status or turn update never moves a card. */
|
|
11
14
|
const AGENT_CARD = {
|
|
12
|
-
width:
|
|
13
|
-
height:
|
|
15
|
+
width: 240,
|
|
16
|
+
height: 88
|
|
14
17
|
};
|
|
15
18
|
/** A live card: room for the header, a two-line thinking line and the last three events. Fixed for the same reason. */
|
|
16
19
|
const LIVE_AGENT_CARD = {
|
|
17
20
|
width: 304,
|
|
18
|
-
height:
|
|
21
|
+
height: 164
|
|
19
22
|
};
|
|
20
23
|
const GRID_GAP = 10;
|
|
21
24
|
const FRAME_PAD = 12;
|
|
@@ -268,88 +271,35 @@ function pickAgentLayout(layouts, canvas, padding) {
|
|
|
268
271
|
* model brand mark, status colours, resting elevation and selection ring, so an
|
|
269
272
|
* agent run and a workflow run read as one system.
|
|
270
273
|
*/
|
|
271
|
-
|
|
272
|
-
/** One word, one glyph and one colour per agent status — never colour alone.
|
|
273
|
-
* `flow` is the workflow status whose border, glow and edge colour it shares. */
|
|
274
|
-
const AGENT_STATUS = {
|
|
275
|
-
running: {
|
|
276
|
-
label: "Running",
|
|
277
|
-
icon: LoaderCircle,
|
|
278
|
-
color: STATUS_COLOR.running,
|
|
279
|
-
flow: "running"
|
|
280
|
-
},
|
|
281
|
-
waiting: {
|
|
282
|
-
label: "Waiting",
|
|
283
|
-
icon: Hourglass,
|
|
284
|
-
color: STATUS_COLOR.waiting,
|
|
285
|
-
flow: "waiting"
|
|
286
|
-
},
|
|
287
|
-
"needs-you": {
|
|
288
|
-
label: "Needs you",
|
|
289
|
-
icon: Hand,
|
|
290
|
-
color: STATUS_COLOR.waiting,
|
|
291
|
-
flow: "waiting"
|
|
292
|
-
},
|
|
293
|
-
succeeded: {
|
|
294
|
-
label: "Done",
|
|
295
|
-
icon: CircleCheck,
|
|
296
|
-
color: STATUS_COLOR.succeeded,
|
|
297
|
-
flow: "succeeded"
|
|
298
|
-
},
|
|
299
|
-
failed: {
|
|
300
|
-
label: "Failed",
|
|
301
|
-
icon: CircleX,
|
|
302
|
-
color: STATUS_COLOR.failed,
|
|
303
|
-
flow: "failed"
|
|
304
|
-
},
|
|
305
|
-
stopped: {
|
|
306
|
-
label: "Stopped",
|
|
307
|
-
icon: CircleStop,
|
|
308
|
-
color: MUTED,
|
|
309
|
-
flow: "succeeded"
|
|
310
|
-
},
|
|
311
|
-
idle: {
|
|
312
|
-
label: "Idle",
|
|
313
|
-
icon: CirclePause,
|
|
314
|
-
color: MUTED,
|
|
315
|
-
flow: "queued"
|
|
316
|
-
},
|
|
317
|
-
unknown: {
|
|
318
|
-
label: "Unknown",
|
|
319
|
-
icon: CircleQuestionMark,
|
|
320
|
-
color: MUTED,
|
|
321
|
-
flow: "queued"
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
/** A status this table has no entry for (a newer host) reads as unknown, not as a crash. */
|
|
274
|
+
/** Workflow state adaptation stays at its boundary; agent states use ui directly. */
|
|
325
275
|
function agentStatus(status) {
|
|
326
|
-
|
|
276
|
+
const meta = statusMeta(status);
|
|
277
|
+
const flow = status === "needs-you" ? "waiting" : status === "waiting" || status === "idle" || status === "unknown" ? "queued" : status === "stopped" ? "succeeded" : status;
|
|
278
|
+
return {
|
|
279
|
+
...meta,
|
|
280
|
+
color: `var(${meta.token})`,
|
|
281
|
+
flow
|
|
282
|
+
};
|
|
327
283
|
}
|
|
328
284
|
function StatusGlyph({ status, size }) {
|
|
329
|
-
|
|
330
|
-
const Icon = s.icon;
|
|
331
|
-
return /* @__PURE__ */ jsx(Icon, {
|
|
285
|
+
return status === "running" ? /* @__PURE__ */ jsx("span", {
|
|
332
286
|
"aria-hidden": true,
|
|
287
|
+
"data-motion": "essential",
|
|
288
|
+
className: "ag-running-dot",
|
|
289
|
+
style: ambientStyle,
|
|
290
|
+
onAnimationStart: syncAmbient
|
|
291
|
+
}) : /* @__PURE__ */ jsx(RunStatusIcon, {
|
|
292
|
+
status,
|
|
333
293
|
size,
|
|
334
|
-
|
|
335
|
-
className: status === "running" ? "shrink-0 motion-safe:animate-spin [animation-duration:1.6s]" : "shrink-0",
|
|
336
|
-
style: { color: s.color }
|
|
294
|
+
"aria-hidden": true
|
|
337
295
|
});
|
|
338
296
|
}
|
|
339
|
-
/** Only states that ask for a reader colour a card's border; running and done cards stay on the resting hairline and
|
|
340
|
-
* say their state by glyph and word, so a busy run never reads as a wall of glowing outlines. */
|
|
341
297
|
function cardBorder(status) {
|
|
342
|
-
if (status === "
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
borderStyle: "dashed"
|
|
298
|
+
if (status === "needs-you" || status === "failed") return {
|
|
299
|
+
borderColor: agentStatus(status).color,
|
|
300
|
+
borderWidth: status === "needs-you" ? 1.5 : 1
|
|
346
301
|
};
|
|
347
|
-
|
|
348
|
-
if (status === "stopped") return {
|
|
349
|
-
borderColor: RESTING_BORDER,
|
|
350
|
-
boxShadow: RESTING_SHADOW
|
|
351
|
-
};
|
|
352
|
-
return statusBorder(agentStatus(status).flow);
|
|
302
|
+
return { borderColor: RESTING_BORDER };
|
|
353
303
|
}
|
|
354
304
|
/** The words a reader without the picture receives for one agent. */
|
|
355
305
|
function agentSummary(agent) {
|
|
@@ -362,6 +312,7 @@ function agentSummary(agent) {
|
|
|
362
312
|
agent.model,
|
|
363
313
|
agentStatus(agent.status).label,
|
|
364
314
|
agent.turn,
|
|
315
|
+
agent.result,
|
|
365
316
|
cost,
|
|
366
317
|
tokens
|
|
367
318
|
].filter(Boolean).join(", ");
|
|
@@ -383,86 +334,97 @@ function Handles({ direction, source = true }) {
|
|
|
383
334
|
className: HANDLE_CLASS
|
|
384
335
|
})] });
|
|
385
336
|
}
|
|
386
|
-
function
|
|
387
|
-
const
|
|
337
|
+
function AgentCardView({ agent, selected, direction, now: fixed, onOpen, parentLabel, handles = false }) {
|
|
338
|
+
const now = useRunNow(fixed);
|
|
339
|
+
const quiet = quietActivity(agent, now);
|
|
388
340
|
const s = agentStatus(agent.status);
|
|
389
|
-
const meta = [agent.role, agent.harness].filter(Boolean).join(" · ");
|
|
390
|
-
const cost = fmtCost(agent.costUsd ?? void 0);
|
|
391
|
-
const tokens = fmtTokenTotal(agent.tokens);
|
|
392
341
|
return /* @__PURE__ */ jsxs("div", {
|
|
393
342
|
"data-status": agent.status,
|
|
343
|
+
"data-activity": quiet ? "quiet" : void 0,
|
|
394
344
|
"data-selected": selected || void 0,
|
|
395
345
|
title: agentSummary(agent),
|
|
396
|
-
className: `relative flex h-full w-full
|
|
346
|
+
className: `ag-agent-card relative flex h-full w-full flex-col gap-0.5 rounded-[var(--radius-card)] border bg-card px-3 py-2 text-left ${MOTION_CONTROL}`,
|
|
397
347
|
style: {
|
|
398
348
|
boxShadow: RESTING_SHADOW,
|
|
399
349
|
...cardBorder(agent.status),
|
|
400
350
|
...selected ? SELECTION_OUTLINE : {}
|
|
401
351
|
},
|
|
402
352
|
children: [
|
|
403
|
-
/* @__PURE__ */ jsx(Handles, { direction }),
|
|
353
|
+
handles && /* @__PURE__ */ jsx(Handles, { direction }),
|
|
404
354
|
/* @__PURE__ */ jsxs("div", {
|
|
405
|
-
className: "flex min-w-0 items-center gap-2
|
|
355
|
+
className: "flex min-w-0 items-center gap-2",
|
|
406
356
|
children: [
|
|
407
|
-
/* @__PURE__ */ jsx(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
357
|
+
/* @__PURE__ */ jsx("span", {
|
|
358
|
+
className: "grayscale hover:grayscale-0",
|
|
359
|
+
children: /* @__PURE__ */ jsx(NodeMark, {
|
|
360
|
+
kind: "agent.run",
|
|
361
|
+
model: agent.model ?? void 0,
|
|
362
|
+
accent: KIND_ACCENT["agent.run"],
|
|
363
|
+
tile: 18
|
|
364
|
+
})
|
|
412
365
|
}),
|
|
413
|
-
/* @__PURE__ */ jsxs("
|
|
414
|
-
className: "min-w-0 flex-1",
|
|
415
|
-
children: [/* @__PURE__ */
|
|
416
|
-
className: "
|
|
417
|
-
children:
|
|
418
|
-
}),
|
|
419
|
-
className: "truncate text-[11px] text-muted-foreground leading-4",
|
|
420
|
-
children: meta || "\xA0"
|
|
421
|
-
})]
|
|
366
|
+
/* @__PURE__ */ jsxs("span", {
|
|
367
|
+
className: "min-w-0 flex-1 truncate text-[13px] font-semibold leading-[18px]",
|
|
368
|
+
children: [parentLabel && /* @__PURE__ */ jsxs("span", {
|
|
369
|
+
className: "ag-parent-label",
|
|
370
|
+
children: [parentLabel, " / "]
|
|
371
|
+
}), agent.label]
|
|
422
372
|
}),
|
|
423
|
-
/* @__PURE__ */ jsx("
|
|
424
|
-
className: "
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
size: 15
|
|
428
|
-
})
|
|
373
|
+
/* @__PURE__ */ jsx("span", {
|
|
374
|
+
className: "text-xs text-muted-foreground tabular-nums",
|
|
375
|
+
title: agent.costUsd == null ? "Cost not reported" : "Cost",
|
|
376
|
+
children: fmtCost(agent.costUsd ?? void 0) ?? "—"
|
|
429
377
|
})
|
|
430
378
|
]
|
|
431
379
|
}),
|
|
380
|
+
/* @__PURE__ */ jsx("p", {
|
|
381
|
+
className: "m-0 line-clamp-2 min-h-4 text-xs text-foreground leading-4",
|
|
382
|
+
children: (agent.status === "failed" ? agent.result ?? agent.turn : agent.turn) || "—"
|
|
383
|
+
}),
|
|
432
384
|
/* @__PURE__ */ jsxs("div", {
|
|
433
|
-
className: "flex
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
385
|
+
className: "mt-auto flex items-center justify-between gap-1 text-xs",
|
|
386
|
+
style: { color: quiet ? "var(--surface-warning-text)" : s.color },
|
|
387
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
388
|
+
className: "flex items-center gap-1",
|
|
389
|
+
children: [/* @__PURE__ */ jsx(StatusGlyph, {
|
|
390
|
+
status: agent.status,
|
|
391
|
+
size: 16
|
|
392
|
+
}), quiet?.label ?? s.label]
|
|
393
|
+
}), agent.status === "needs-you" && /* @__PURE__ */ jsx("button", {
|
|
394
|
+
type: "button",
|
|
395
|
+
className: "ag-live-respond nodrag nopan",
|
|
396
|
+
disabled: !onOpen,
|
|
397
|
+
onClick: (event) => {
|
|
398
|
+
event.stopPropagation();
|
|
399
|
+
onOpen?.();
|
|
400
|
+
},
|
|
401
|
+
children: "Respond"
|
|
402
|
+
})]
|
|
449
403
|
})
|
|
450
404
|
]
|
|
451
405
|
});
|
|
452
406
|
}
|
|
407
|
+
function AgentCard({ data }) {
|
|
408
|
+
return /* @__PURE__ */ jsx(AgentCardView, {
|
|
409
|
+
...data,
|
|
410
|
+
handles: true
|
|
411
|
+
});
|
|
412
|
+
}
|
|
453
413
|
/** The statuses a grid caption counts: the ones a reader looks for. */
|
|
454
414
|
const CAPTION_STATUSES = [
|
|
455
|
-
"
|
|
415
|
+
"needs-you",
|
|
416
|
+
"failed",
|
|
417
|
+
"quiet",
|
|
456
418
|
"waiting",
|
|
457
|
-
"
|
|
419
|
+
"running"
|
|
458
420
|
];
|
|
459
421
|
function AgentFrameBox({ data }) {
|
|
460
422
|
const { frame, statuses, direction } = data;
|
|
461
423
|
const counts = CAPTION_STATUSES.map((status) => ({
|
|
462
424
|
status,
|
|
463
|
-
n: statuses.filter((s) => s === status).length
|
|
425
|
+
n: status === "quiet" ? data.quietCount ?? 0 : statuses.filter((s) => s === status).length
|
|
464
426
|
})).filter((c) => c.n > 0);
|
|
465
|
-
const words = [`${frame.group.memberIds.length} agents`, ...counts.map((c) => `${c.n} ${agentStatus(c.status).label.toLowerCase()}`)].join(", ");
|
|
427
|
+
const words = [`${frame.group.memberIds.length} agents`, ...counts.map((c) => `${c.n} ${c.status === "quiet" ? "quiet" : agentStatus(c.status).label.toLowerCase()}`)].join(", ");
|
|
466
428
|
return /* @__PURE__ */ jsxs("div", {
|
|
467
429
|
className: "relative h-full w-full rounded-2xl border",
|
|
468
430
|
style: {
|
|
@@ -494,20 +456,23 @@ function AgentFrameBox({ data }) {
|
|
|
494
456
|
counts.map((c) => /* @__PURE__ */ jsxs("span", {
|
|
495
457
|
"aria-hidden": true,
|
|
496
458
|
className: "flex items-center gap-1 tabular-nums",
|
|
497
|
-
children: [
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
459
|
+
children: [
|
|
460
|
+
c.status !== "quiet" && /* @__PURE__ */ jsx(StatusGlyph, {
|
|
461
|
+
status: c.status,
|
|
462
|
+
size: 11
|
|
463
|
+
}),
|
|
464
|
+
c.n,
|
|
465
|
+
c.status === "quiet" ? " quiet" : ""
|
|
466
|
+
]
|
|
501
467
|
}, c.status))
|
|
502
468
|
]
|
|
503
469
|
})]
|
|
504
470
|
});
|
|
505
471
|
}
|
|
506
|
-
/**
|
|
507
|
-
* primary accent's wash. Both stay well under a spawn edge's weight. */
|
|
472
|
+
/** Resting message and delegation edges stay neutral. */
|
|
508
473
|
const MESSAGE_EDGE_COLOR = {
|
|
509
474
|
message: "color-mix(in srgb, hsl(var(--muted-foreground)) 70%, transparent)",
|
|
510
|
-
delegation: "color-mix(in srgb, hsl(var(--
|
|
475
|
+
delegation: "color-mix(in srgb, hsl(var(--muted-foreground)) 80%, transparent)"
|
|
511
476
|
};
|
|
512
477
|
function boxOf(node) {
|
|
513
478
|
return {
|
|
@@ -582,7 +547,6 @@ function AgentMessageEdgeView({ id, source, target, data, markerEnd }) {
|
|
|
582
547
|
style: {
|
|
583
548
|
stroke: color,
|
|
584
549
|
strokeWidth: 1.25,
|
|
585
|
-
strokeDasharray: "4 4",
|
|
586
550
|
fill: "none"
|
|
587
551
|
}
|
|
588
552
|
}), count > 1 && /* @__PURE__ */ jsx(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -597,181 +561,202 @@ function AgentMessageEdgeView({ id, source, target, data, markerEnd }) {
|
|
|
597
561
|
}
|
|
598
562
|
// src/workflows/agent-live-card.tsx
|
|
599
563
|
const SETTLED = /* @__PURE__ */ new Set(["succeeded", "stopped"]);
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
[/^(edit|write|patch|apply)\b/, Pencil],
|
|
603
|
-
[/^(read|cat|open|view|ls|glob)\b/, FileText],
|
|
604
|
-
[/^(grep|search|find|rg)\b/, Search],
|
|
605
|
-
[/^(web|fetch|http|browse)\b/, Globe],
|
|
606
|
-
[/^(spawn|steer|release|merge|commit|git)\b/, GitBranch],
|
|
607
|
-
[/^(wait|sleep|queue)\b/, Hourglass]
|
|
608
|
-
];
|
|
609
|
-
const eventGlyph = (label) => EVENT_GLYPHS.find(([match]) => match.test(label.toLowerCase()))?.[1] ?? Sparkles;
|
|
610
|
-
function clockTime(at) {
|
|
611
|
-
return at == null || !Number.isFinite(at) ? void 0 : new Date(at).toLocaleTimeString([], {
|
|
612
|
-
hour: "2-digit",
|
|
613
|
-
minute: "2-digit",
|
|
614
|
-
hourCycle: "h23"
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
function ago(at, now) {
|
|
618
|
-
if (at == null || !Number.isFinite(at)) return void 0;
|
|
619
|
-
const s = Math.max(0, Math.round((now - at) / 1e3));
|
|
620
|
-
if (s < 5) return "now";
|
|
621
|
-
if (s < 60) return `${s}s`;
|
|
622
|
-
const m = Math.floor(s / 60);
|
|
623
|
-
return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h`;
|
|
624
|
-
}
|
|
625
|
-
function aliveFor(startedAt, now) {
|
|
626
|
-
if (startedAt == null || !Number.isFinite(startedAt)) return void 0;
|
|
627
|
-
const s = Math.max(0, Math.round((now - startedAt) / 1e3));
|
|
628
|
-
if (s < 60) return `${s}s`;
|
|
629
|
-
const m = Math.floor(s / 60);
|
|
630
|
-
if (m < 60) return `${m}m`;
|
|
631
|
-
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
632
|
-
}
|
|
633
|
-
/** Time alive ticks once a second while the host follows now; a replay passes its own clock. */
|
|
634
|
-
function useNow(fixed, live) {
|
|
635
|
-
const [now, setNow] = useState(() => fixed ?? Date.now());
|
|
636
|
-
useEffect(() => {
|
|
637
|
-
if (fixed !== void 0 || !live) return;
|
|
638
|
-
const timer = setInterval(() => setNow(Date.now()), 1e3);
|
|
639
|
-
return () => clearInterval(timer);
|
|
640
|
-
}, [fixed, live]);
|
|
641
|
-
return fixed ?? now;
|
|
564
|
+
function middleLabel(label) {
|
|
565
|
+
return label.length <= 52 ? label : `${label.slice(0, 32)} … ${label.slice(-17)}`;
|
|
642
566
|
}
|
|
643
|
-
|
|
644
|
-
|
|
567
|
+
/** One shell survives settling. A failure retains its evidence and absolute event times. */
|
|
568
|
+
function LiveAgentCardView({ agent, selected, direction = "TB", now: fixed, handles = false, onOpen, parentLabel, renderEventIcon }) {
|
|
569
|
+
const now = useRunNow(fixed);
|
|
570
|
+
const ref = useRef(null);
|
|
571
|
+
useAttention(ref, agent.status);
|
|
645
572
|
const settled = SETTLED.has(agent.status);
|
|
646
573
|
const failed = agent.status === "failed";
|
|
647
|
-
const
|
|
574
|
+
const quiet = quietActivity(agent, now);
|
|
648
575
|
const s = agentStatus(agent.status);
|
|
649
|
-
const
|
|
650
|
-
|
|
576
|
+
const meta = [agent.model ? shortModel(agent.model) : void 0, agent.effort].filter(Boolean).join(" · ");
|
|
577
|
+
const [events, setEvents] = useState(() => (agent.events ?? []).slice(-4));
|
|
578
|
+
const cost = Number.isFinite(agent.costUsd) ? fmtCost(agent.costUsd) : void 0;
|
|
579
|
+
const metrics = [
|
|
580
|
+
cost,
|
|
581
|
+
!failed && agent.startedAt != null ? `${fmtEventTime(agent.startedAt, now, false)} elapsed` : void 0,
|
|
582
|
+
Number.isFinite(agent.commits) ? `${agent.commits} ${agent.commits === 1 ? "commit" : "commits"}` : void 0
|
|
583
|
+
].filter(Boolean).join(" · ") || "—";
|
|
584
|
+
const tail = useRef(null);
|
|
585
|
+
const motion = useMotionAllowed();
|
|
586
|
+
const previous = useRef(events.at(-1)?.id);
|
|
587
|
+
const pending = useRef(void 0);
|
|
588
|
+
const latest = useRef(agent.events);
|
|
589
|
+
latest.current = agent.events;
|
|
590
|
+
useEffect(() => {
|
|
591
|
+
if (pending.current !== void 0) return;
|
|
592
|
+
pending.current = setTimeout(() => {
|
|
593
|
+
setEvents((latest.current ?? []).slice(-4));
|
|
594
|
+
pending.current = void 0;
|
|
595
|
+
}, tokenMilliseconds("--duration-ambient", ref.current) * .15);
|
|
596
|
+
}, [agent.events]);
|
|
597
|
+
useEffect(() => () => clearTimeout(pending.current), []);
|
|
598
|
+
const tailAnimation = useRef(null);
|
|
599
|
+
useEffect(() => () => tailAnimation.current?.cancel(), []);
|
|
600
|
+
useLayoutEffect(() => {
|
|
601
|
+
const last = events.at(-1)?.id;
|
|
602
|
+
const old = previous.current;
|
|
603
|
+
previous.current = last;
|
|
604
|
+
const element = tail.current;
|
|
605
|
+
if (!motion) {
|
|
606
|
+
tailAnimation.current?.cancel();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (!element || !old || old === last) return;
|
|
610
|
+
const queued = events.findIndex((event) => event.id === old);
|
|
611
|
+
const offset = new DOMMatrix(getComputedStyle(element).transform).m42 + 16;
|
|
612
|
+
tailAnimation.current?.cancel();
|
|
613
|
+
const frames = queued < 0 ? [{ opacity: 0 }, { opacity: 1 }] : [{ transform: `translateY(${offset}px)` }, { transform: "translateY(-16px)" }];
|
|
614
|
+
tailAnimation.current = element.animate(frames, {
|
|
615
|
+
duration: tokenMilliseconds("--duration-stream", element),
|
|
616
|
+
easing: getComputedStyle(element).getPropertyValue("--ease-standard").trim()
|
|
617
|
+
});
|
|
618
|
+
}, [events, motion]);
|
|
619
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
651
620
|
className: "ag-live-slot",
|
|
652
|
-
"
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
621
|
+
children: [handles && /* @__PURE__ */ jsx(Handles, { direction }), /* @__PURE__ */ jsxs("div", {
|
|
622
|
+
ref,
|
|
623
|
+
className: `ag-live-card${settled ? " ag-live-chip" : ""}`,
|
|
624
|
+
"data-status": agent.status,
|
|
625
|
+
"data-sparse": events.length === 0 || void 0,
|
|
626
|
+
"data-activity": quiet ? "quiet" : agent.activity ?? void 0,
|
|
656
627
|
"data-selected": selected || void 0,
|
|
657
|
-
|
|
628
|
+
title: agentSummary(agent),
|
|
629
|
+
style: {
|
|
630
|
+
boxShadow: RESTING_SHADOW,
|
|
631
|
+
...cardBorder(agent.status),
|
|
632
|
+
...selected ? SELECTION_OUTLINE : {},
|
|
633
|
+
["--ag-status"]: s.color
|
|
634
|
+
},
|
|
658
635
|
children: [
|
|
659
|
-
/* @__PURE__ */ jsx(StatusGlyph, {
|
|
636
|
+
settled && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(StatusGlyph, {
|
|
660
637
|
status: agent.status,
|
|
661
|
-
size:
|
|
662
|
-
}),
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
638
|
+
size: 16
|
|
639
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
640
|
+
className: "ag-live-chip-status",
|
|
641
|
+
children: s.label
|
|
642
|
+
})] }),
|
|
643
|
+
/* @__PURE__ */ jsxs("div", {
|
|
644
|
+
className: "ag-live-head",
|
|
645
|
+
children: [
|
|
646
|
+
/* @__PURE__ */ jsx("span", {
|
|
647
|
+
className: "grayscale hover:grayscale-0",
|
|
648
|
+
children: /* @__PURE__ */ jsx(NodeMark, {
|
|
649
|
+
kind: "agent.run",
|
|
650
|
+
model: agent.model ?? void 0,
|
|
651
|
+
accent: KIND_ACCENT["agent.run"],
|
|
652
|
+
tile: 18
|
|
653
|
+
})
|
|
654
|
+
}),
|
|
655
|
+
/* @__PURE__ */ jsxs("div", {
|
|
656
|
+
className: "ag-live-names",
|
|
657
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
658
|
+
className: "ag-live-name",
|
|
659
|
+
children: [parentLabel && /* @__PURE__ */ jsxs("span", {
|
|
660
|
+
className: "ag-parent-label",
|
|
661
|
+
children: [parentLabel, " / "]
|
|
662
|
+
}), agent.label]
|
|
663
|
+
}), !settled && meta && /* @__PURE__ */ jsx("span", {
|
|
664
|
+
className: "ag-live-meta",
|
|
665
|
+
children: meta
|
|
666
|
+
})]
|
|
667
|
+
}),
|
|
668
|
+
/* @__PURE__ */ jsxs("div", {
|
|
669
|
+
className: "ag-live-stats",
|
|
670
|
+
children: [failed && onOpen ? /* @__PURE__ */ jsxs("button", {
|
|
671
|
+
type: "button",
|
|
672
|
+
className: "ag-live-pill ag-live-inspect nodrag nopan",
|
|
673
|
+
onClick: (event) => {
|
|
674
|
+
event.stopPropagation();
|
|
675
|
+
onOpen();
|
|
676
|
+
},
|
|
677
|
+
children: [
|
|
678
|
+
/* @__PURE__ */ jsx(StatusGlyph, {
|
|
679
|
+
status: agent.status,
|
|
680
|
+
size: 16
|
|
681
|
+
}),
|
|
682
|
+
s.label,
|
|
683
|
+
/* @__PURE__ */ jsx("span", { children: "Inspect" })
|
|
684
|
+
]
|
|
685
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
686
|
+
className: `ag-live-pill${quiet ? " ag-live-quiet" : ""}`,
|
|
687
|
+
children: [/* @__PURE__ */ jsx(StatusGlyph, {
|
|
688
|
+
status: agent.status,
|
|
689
|
+
size: 16
|
|
690
|
+
}), quiet?.label ?? s.label]
|
|
691
|
+
}), !settled && /* @__PURE__ */ jsx("span", {
|
|
692
|
+
className: "ag-live-accounting",
|
|
693
|
+
title: metrics,
|
|
694
|
+
children: metrics
|
|
695
|
+
})]
|
|
696
|
+
})
|
|
697
|
+
]
|
|
666
698
|
}),
|
|
667
|
-
/* @__PURE__ */ jsx("span", {
|
|
699
|
+
settled ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
668
700
|
className: "ag-live-chip-result",
|
|
669
|
-
children: agent.result ?? s.label
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
}),
|
|
699
|
-
/* @__PURE__ */ jsxs("div", {
|
|
700
|
-
className: "ag-live-names",
|
|
701
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
702
|
-
className: "ag-live-name",
|
|
703
|
-
children: agent.label
|
|
704
|
-
}), meta && /* @__PURE__ */ jsx("span", {
|
|
705
|
-
className: "ag-live-meta",
|
|
706
|
-
children: meta
|
|
707
|
-
})]
|
|
708
|
-
}),
|
|
709
|
-
/* @__PURE__ */ jsxs("div", {
|
|
710
|
-
className: "ag-live-stats",
|
|
711
|
-
children: [/* @__PURE__ */ jsxs("span", {
|
|
712
|
-
className: "ag-live-pill",
|
|
713
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
714
|
-
className: "ag-live-dot",
|
|
715
|
-
"aria-hidden": true
|
|
716
|
-
}), s.label]
|
|
717
|
-
}), /* @__PURE__ */ jsxs("span", { children: [alive && /* @__PURE__ */ jsx("span", {
|
|
718
|
-
title: "Time alive",
|
|
719
|
-
children: alive
|
|
720
|
-
}), agent.commits != null && /* @__PURE__ */ jsxs("span", {
|
|
721
|
-
title: "Commits shipped",
|
|
701
|
+
children: agent.result ?? agent.turn ?? s.label
|
|
702
|
+
}), cost && /* @__PURE__ */ jsx("span", {
|
|
703
|
+
className: "ag-live-chip-cost",
|
|
704
|
+
title: "Cost",
|
|
705
|
+
children: cost
|
|
706
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
707
|
+
className: "ag-live-thinking-row",
|
|
708
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
709
|
+
className: `ag-live-thinking${failed ? " ag-live-failure" : ""}`,
|
|
710
|
+
children: failed ? agent.result ?? events.at(-1)?.label ?? agent.turn ?? s.label : agent.turn || "—"
|
|
711
|
+
}), agent.status === "needs-you" && /* @__PURE__ */ jsx("button", {
|
|
712
|
+
type: "button",
|
|
713
|
+
className: "ag-live-respond nodrag nopan",
|
|
714
|
+
disabled: !onOpen,
|
|
715
|
+
title: onOpen ? void 0 : "Responses unavailable in this view",
|
|
716
|
+
onClick: (event) => {
|
|
717
|
+
event.stopPropagation();
|
|
718
|
+
onOpen?.();
|
|
719
|
+
},
|
|
720
|
+
children: "Respond"
|
|
721
|
+
})]
|
|
722
|
+
}), events.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
723
|
+
className: "ag-live-events-clip",
|
|
724
|
+
children: /* @__PURE__ */ jsx("ol", {
|
|
725
|
+
ref: tail,
|
|
726
|
+
className: "ag-live-events",
|
|
727
|
+
"aria-label": "Latest events",
|
|
728
|
+
children: events.map((event) => /* @__PURE__ */ jsxs("li", {
|
|
729
|
+
title: event.label,
|
|
722
730
|
children: [
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
731
|
+
renderEventIcon?.(event) ?? /* @__PURE__ */ jsx(Wrench, {
|
|
732
|
+
size: 12,
|
|
733
|
+
strokeWidth: 1.75,
|
|
734
|
+
"aria-hidden": true
|
|
735
|
+
}),
|
|
736
|
+
/* @__PURE__ */ jsx("span", {
|
|
737
|
+
className: "ag-live-event-label",
|
|
738
|
+
children: middleLabel(event.label)
|
|
739
|
+
}),
|
|
740
|
+
/* @__PURE__ */ jsxs("time", {
|
|
741
|
+
className: "ag-live-event-at",
|
|
742
|
+
dateTime: Number.isFinite(event.at) ? new Date(event.at).toISOString() : void 0,
|
|
743
|
+
children: [failed && event.at != null ? "at " : "", fmtEventTime(event.at, now, failed)]
|
|
744
|
+
})
|
|
727
745
|
]
|
|
728
|
-
}
|
|
746
|
+
}, event.id))
|
|
729
747
|
})
|
|
730
|
-
]
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
children: s.label
|
|
740
|
-
})
|
|
741
|
-
}, agent.turn ?? ""),
|
|
742
|
-
/* @__PURE__ */ jsx("ol", {
|
|
743
|
-
className: "ag-live-events",
|
|
744
|
-
"aria-label": "Latest events",
|
|
745
|
-
children: events.map((event) => {
|
|
746
|
-
const Glyph = eventGlyph(event.label);
|
|
747
|
-
const when = failed ? clockTime(event.at) : ago(event.at, now);
|
|
748
|
-
return /* @__PURE__ */ jsxs("li", { children: [
|
|
749
|
-
/* @__PURE__ */ jsx(Glyph, {
|
|
750
|
-
"aria-hidden": true,
|
|
751
|
-
size: 12,
|
|
752
|
-
strokeWidth: 2
|
|
753
|
-
}),
|
|
754
|
-
/* @__PURE__ */ jsx("span", {
|
|
755
|
-
className: "ag-live-event-label",
|
|
756
|
-
children: event.label
|
|
757
|
-
}),
|
|
758
|
-
when && /* @__PURE__ */ jsx("time", {
|
|
759
|
-
className: "ag-live-event-at",
|
|
760
|
-
children: when
|
|
761
|
-
})
|
|
762
|
-
] }, event.id);
|
|
763
|
-
})
|
|
764
|
-
})
|
|
765
|
-
]
|
|
748
|
+
})] })
|
|
749
|
+
]
|
|
750
|
+
})]
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function LiveAgentCard({ data }) {
|
|
754
|
+
return /* @__PURE__ */ jsx(LiveAgentCardView, {
|
|
755
|
+
...data,
|
|
756
|
+
handles: true
|
|
766
757
|
});
|
|
767
758
|
}
|
|
768
759
|
// src/workflows/AgentGraphCanvas.tsx
|
|
769
|
-
/**
|
|
770
|
-
* The topology of a multi-agent run: who spawned whom as a tree, and who
|
|
771
|
-
* messaged or delegated to whom as curved secondary edges over it. Reached
|
|
772
|
-
* through the lazy `AgentGraph` wrapper, which defers `@xyflow/react` (and
|
|
773
|
-
* `elkjs`) to render time.
|
|
774
|
-
*/
|
|
775
760
|
const NODE_TYPES = {
|
|
776
761
|
agent: AgentCard,
|
|
777
762
|
liveAgent: LiveAgentCard,
|
|
@@ -789,8 +774,9 @@ function useCanvasSize() {
|
|
|
789
774
|
observer.current?.disconnect();
|
|
790
775
|
observer.current = null;
|
|
791
776
|
if (!el || typeof ResizeObserver === "undefined") return;
|
|
777
|
+
const viewport = el.querySelector(".ag-graph-viewport") ?? el;
|
|
792
778
|
const read = () => {
|
|
793
|
-
const r =
|
|
779
|
+
const r = viewport.getBoundingClientRect();
|
|
794
780
|
if (r.width === 0 || r.height === 0) return;
|
|
795
781
|
setSize((prev) => prev && Math.abs(prev.width - r.width) < 1 && Math.abs(prev.height - r.height) < 1 ? prev : {
|
|
796
782
|
width: r.width,
|
|
@@ -798,15 +784,34 @@ function useCanvasSize() {
|
|
|
798
784
|
});
|
|
799
785
|
};
|
|
800
786
|
observer.current = new ResizeObserver(read);
|
|
801
|
-
observer.current.observe(
|
|
787
|
+
observer.current.observe(viewport);
|
|
802
788
|
read();
|
|
803
789
|
}, [])];
|
|
804
790
|
}
|
|
805
|
-
|
|
791
|
+
/** Keep the selected card inside the actual viewport without resetting the reader's camera. */
|
|
792
|
+
function revealSelection(inst, wrapper, id) {
|
|
793
|
+
const node = inst.getInternalNode(id), box = wrapper?.querySelector(".ag-graph-viewport")?.getBoundingClientRect();
|
|
794
|
+
const width = node?.measured.width ?? node?.width, height = node?.measured.height ?? node?.height;
|
|
795
|
+
if (!node || !box || !width || !height) return;
|
|
796
|
+
const current = inst.getViewport(), position = node.internals.positionAbsolute;
|
|
797
|
+
const zoom = Math.max(.02, Math.min(current.zoom, (box.width - 32) / width, (box.height - 32) / height));
|
|
798
|
+
const x = Math.max(FIT_PADDING - position.x * zoom, Math.min(box.width - FIT_PADDING - (position.x + width) * zoom, current.x));
|
|
799
|
+
const y = Math.max(FIT_PADDING - position.y * zoom, Math.min(box.height - FIT_PADDING - (position.y + height) * zoom, current.y));
|
|
800
|
+
if (x !== current.x || y !== current.y || zoom !== current.zoom) inst.setViewport({
|
|
801
|
+
x,
|
|
802
|
+
y,
|
|
803
|
+
zoom
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB", className, emptyState, live = false, renderEventIcon, showLatestEvents = true, now }) {
|
|
806
807
|
const card = live ? LIVE_AGENT_CARD : AGENT_CARD;
|
|
808
|
+
const clock = useRunNow(now);
|
|
809
|
+
const animateMotion = useMotionAllowed();
|
|
810
|
+
const [hovered, setHovered] = useState(null);
|
|
807
811
|
const wrapperRef = useRef(null);
|
|
808
812
|
const [canvas, measureCanvas] = useCanvasSize();
|
|
809
813
|
const colorMode = useColorMode(wrapperRef, nodes.length > 0);
|
|
814
|
+
const narrow = canvas !== null && canvas.width < 600;
|
|
810
815
|
const nextTopology = buildAgentTopology(nodes, edges);
|
|
811
816
|
const topologyRef = useRef(nextTopology);
|
|
812
817
|
if (topologyRef.current.key !== nextTopology.key) topologyRef.current = nextTopology;
|
|
@@ -814,6 +819,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
814
819
|
const [layouts, setLayouts] = useState(null);
|
|
815
820
|
const layoutKey = `${direction}:${live ? "live" : "cards"}:${topology.key}`;
|
|
816
821
|
useEffect(() => {
|
|
822
|
+
if (narrow) return;
|
|
817
823
|
let live = true;
|
|
818
824
|
Promise.all(candidateColumns(topology).map((c) => layoutAgents(topology, direction, c, card))).then((all) => {
|
|
819
825
|
if (live) setLayouts({
|
|
@@ -828,14 +834,21 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
828
834
|
topology,
|
|
829
835
|
direction,
|
|
830
836
|
layoutKey,
|
|
831
|
-
card
|
|
837
|
+
card,
|
|
838
|
+
narrow
|
|
832
839
|
]);
|
|
833
|
-
const
|
|
840
|
+
const priorLayout = useRef(null);
|
|
841
|
+
const layout = useMemo(() => {
|
|
842
|
+
if (!layouts || layouts.key !== layoutKey || !canvas) return null;
|
|
843
|
+
return priorLayout.current = selectedId && priorLayout.current && layouts.all.includes(priorLayout.current) ? priorLayout.current : pickAgentLayout(layouts.all, canvas, FIT_PADDING);
|
|
844
|
+
}, [
|
|
834
845
|
layouts,
|
|
835
846
|
layoutKey,
|
|
836
|
-
canvas
|
|
847
|
+
canvas,
|
|
848
|
+
selectedId
|
|
837
849
|
]);
|
|
838
850
|
const byId = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
|
851
|
+
const rfRef = useRef(null);
|
|
839
852
|
const flowNodes = useMemo(() => {
|
|
840
853
|
if (!layout) return [];
|
|
841
854
|
const frames = layout.frames.map((frame) => ({
|
|
@@ -847,6 +860,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
847
860
|
},
|
|
848
861
|
width: frame.width,
|
|
849
862
|
height: frame.height,
|
|
863
|
+
measured: rfRef.current?.getInternalNode(frame.group.id)?.measured,
|
|
850
864
|
zIndex: -1,
|
|
851
865
|
selectable: false,
|
|
852
866
|
focusable: false,
|
|
@@ -854,6 +868,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
854
868
|
data: {
|
|
855
869
|
frame,
|
|
856
870
|
direction,
|
|
871
|
+
quietCount: frame.group.memberIds.filter((id) => byId.get(id) && quietActivity(byId.get(id), clock)).length,
|
|
857
872
|
statuses: frame.group.memberIds.map((id) => byId.get(id)?.status ?? "unknown")
|
|
858
873
|
}
|
|
859
874
|
}));
|
|
@@ -869,6 +884,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
869
884
|
position,
|
|
870
885
|
width: card.width,
|
|
871
886
|
height: card.height,
|
|
887
|
+
measured: rfRef.current?.getInternalNode(id)?.measured,
|
|
872
888
|
className: AGENT_NODE_CLASS,
|
|
873
889
|
draggable: false,
|
|
874
890
|
ariaRole: "button",
|
|
@@ -878,7 +894,9 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
878
894
|
agent,
|
|
879
895
|
selected,
|
|
880
896
|
direction,
|
|
881
|
-
now
|
|
897
|
+
now: clock,
|
|
898
|
+
onOpen: onSelect ? () => onSelect(agent.id) : void 0,
|
|
899
|
+
renderEventIcon
|
|
882
900
|
}
|
|
883
901
|
});
|
|
884
902
|
}
|
|
@@ -891,7 +909,9 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
891
909
|
direction,
|
|
892
910
|
live,
|
|
893
911
|
card,
|
|
894
|
-
|
|
912
|
+
clock,
|
|
913
|
+
onSelect,
|
|
914
|
+
renderEventIcon
|
|
895
915
|
]);
|
|
896
916
|
const flowEdges = useMemo(() => {
|
|
897
917
|
if (!layout) return [];
|
|
@@ -902,13 +922,6 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
902
922
|
const id = `spawn:${s.from}:${target}`;
|
|
903
923
|
if (seen.has(id)) continue;
|
|
904
924
|
seen.add(id);
|
|
905
|
-
const group = topology.groups.find((g) => g.id === target);
|
|
906
|
-
const statuses = group ? group.memberIds.map((m) => byId.get(m)?.status) : [byId.get(s.to)?.status];
|
|
907
|
-
const flow = [
|
|
908
|
-
"failed",
|
|
909
|
-
"waiting",
|
|
910
|
-
"running"
|
|
911
|
-
].find((st) => statuses.includes(st)) ?? agentStatus(statuses[0] ?? "unknown").flow;
|
|
912
925
|
out.push({
|
|
913
926
|
id,
|
|
914
927
|
source: s.from,
|
|
@@ -918,7 +931,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
918
931
|
selectable: false,
|
|
919
932
|
focusable: false,
|
|
920
933
|
style: {
|
|
921
|
-
stroke: edgeColor(
|
|
934
|
+
stroke: edgeColor(void 0),
|
|
922
935
|
strokeWidth: 1.5
|
|
923
936
|
}
|
|
924
937
|
});
|
|
@@ -927,6 +940,7 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
927
940
|
const messages = /* @__PURE__ */ new Map();
|
|
928
941
|
for (const e of edges) {
|
|
929
942
|
if (e.kind === "spawn" || e.from === e.to || !ids.has(e.from) || !ids.has(e.to)) continue;
|
|
943
|
+
if (![selectedId, hovered].some((id) => id === e.from || id === e.to)) continue;
|
|
930
944
|
const id = `${e.kind}:${e.from}:${e.to}`;
|
|
931
945
|
const prior = messages.get(id);
|
|
932
946
|
const count = (prior?.data?.count ?? 0) + Math.max(1, e.count ?? 1);
|
|
@@ -958,29 +972,42 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
958
972
|
layout,
|
|
959
973
|
topology,
|
|
960
974
|
edges,
|
|
961
|
-
byId
|
|
975
|
+
byId,
|
|
976
|
+
selectedId,
|
|
977
|
+
hovered
|
|
962
978
|
]);
|
|
963
|
-
const rfRef = useRef(null);
|
|
964
979
|
const [ready, setReady] = useState(false);
|
|
965
980
|
const [shown, setShown] = useState(false);
|
|
966
981
|
const readerMoved = useRef(false);
|
|
982
|
+
const selection = useRef(selectedId);
|
|
983
|
+
selection.current = selectedId;
|
|
967
984
|
const framed = useRef(null);
|
|
985
|
+
useEffect(() => {
|
|
986
|
+
if (narrow) {
|
|
987
|
+
rfRef.current = null;
|
|
988
|
+
setReady(false);
|
|
989
|
+
setShown(false);
|
|
990
|
+
framed.current = null;
|
|
991
|
+
}
|
|
992
|
+
}, [narrow]);
|
|
968
993
|
const layoutIdentity = layout ? `${layoutKey}:${layout.columns}` : null;
|
|
969
994
|
const minZoom = layout && canvas ? Math.max(.02, Math.min(.2, (canvas.width - 32) / layout.width, (canvas.height - 32) / layout.height)) : .2;
|
|
970
995
|
useEffect(() => {
|
|
971
996
|
const inst = rfRef.current;
|
|
972
997
|
if (!ready || !inst || !layoutIdentity || !canvas) return;
|
|
973
998
|
const relayout = framed.current !== layoutIdentity;
|
|
974
|
-
|
|
975
|
-
|
|
999
|
+
const preserveView = !relayout && readerMoved.current;
|
|
1000
|
+
if (preserveView && !selection.current) return;
|
|
1001
|
+
const animate = framed.current !== null && relayout && animateMotion;
|
|
976
1002
|
framed.current = layoutIdentity;
|
|
977
|
-
readerMoved.current =
|
|
1003
|
+
readerMoved.current = preserveView;
|
|
978
1004
|
const frame = requestAnimationFrame(() => {
|
|
979
|
-
inst.
|
|
1005
|
+
if (preserveView) revealSelection(inst, wrapperRef.current, selection.current);
|
|
1006
|
+
else inst.fitView({
|
|
980
1007
|
padding: `${FIT_PADDING}px`,
|
|
981
1008
|
maxZoom: 1,
|
|
982
1009
|
minZoom,
|
|
983
|
-
duration: animate ?
|
|
1010
|
+
duration: animate ? tokenMilliseconds("--duration-base", wrapperRef.current) : 0
|
|
984
1011
|
});
|
|
985
1012
|
setShown(true);
|
|
986
1013
|
});
|
|
@@ -989,91 +1016,238 @@ function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB"
|
|
|
989
1016
|
ready,
|
|
990
1017
|
layoutIdentity,
|
|
991
1018
|
canvas,
|
|
992
|
-
minZoom
|
|
1019
|
+
minZoom,
|
|
1020
|
+
animateMotion
|
|
993
1021
|
]);
|
|
994
1022
|
const select = useCallback((id) => {
|
|
995
|
-
if (byId.has(id))
|
|
1023
|
+
if (byId.has(id)) {
|
|
1024
|
+
onSelect?.(id);
|
|
1025
|
+
wrapperRef.current?.querySelector(`[data-agent-id="${CSS.escape(id)}"]`)?.scrollIntoView({ block: "nearest" });
|
|
1026
|
+
}
|
|
996
1027
|
}, [byId, onSelect]);
|
|
997
1028
|
const handleNodeClick = useCallback((_event, node) => select(node.id), [select]);
|
|
998
1029
|
const handleKeyDown = useCallback((event) => {
|
|
999
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1000
1030
|
if (!(event.target instanceof HTMLElement)) return;
|
|
1001
|
-
|
|
1002
|
-
const id = event.target.getAttribute("data-id");
|
|
1031
|
+
const id = event.target.closest(".react-flow__node")?.getAttribute("data-id");
|
|
1003
1032
|
if (!id || !byId.has(id)) return;
|
|
1033
|
+
if (event.key === "Escape") {
|
|
1034
|
+
event.preventDefault();
|
|
1035
|
+
event.stopPropagation();
|
|
1036
|
+
wrapperRef.current?.focus();
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (!event.target.classList.contains("react-flow__node")) return;
|
|
1040
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1041
|
+
event.preventDefault();
|
|
1042
|
+
event.stopPropagation();
|
|
1043
|
+
select(id);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const origin = layout?.positions.get(id);
|
|
1047
|
+
const vector = {
|
|
1048
|
+
ArrowLeft: [-1, 0],
|
|
1049
|
+
ArrowRight: [1, 0],
|
|
1050
|
+
ArrowUp: [0, -1],
|
|
1051
|
+
ArrowDown: [0, 1]
|
|
1052
|
+
}[event.key];
|
|
1053
|
+
const target = event.key === "Home" ? topology.agentIds[0] : origin && vector ? [...layout?.positions ?? []].filter(([key, p]) => byId.has(key) && key !== id && (p.x - origin.x) * vector[0] + (p.y - origin.y) * vector[1] > 0).sort((a, b) => Math.hypot(a[1].x - origin.x, a[1].y - origin.y) - Math.hypot(b[1].x - origin.x, b[1].y - origin.y))[0]?.[0] : null;
|
|
1054
|
+
if (!target) return;
|
|
1004
1055
|
event.preventDefault();
|
|
1005
1056
|
event.stopPropagation();
|
|
1006
|
-
|
|
1007
|
-
|
|
1057
|
+
const position = layout?.positions.get(target);
|
|
1058
|
+
wrapperRef.current?.querySelector(`.react-flow__node[data-id="${CSS.escape(target)}"]`)?.focus();
|
|
1059
|
+
if (position) rfRef.current?.setCenter(position.x + card.width / 2, position.y + card.height / 2, {
|
|
1060
|
+
zoom: rfRef.current.getZoom(),
|
|
1061
|
+
duration: animateMotion ? tokenMilliseconds("--duration-base", wrapperRef.current) : 0
|
|
1062
|
+
});
|
|
1063
|
+
}, [
|
|
1064
|
+
byId,
|
|
1065
|
+
select,
|
|
1066
|
+
layout,
|
|
1067
|
+
topology,
|
|
1068
|
+
card,
|
|
1069
|
+
animateMotion
|
|
1070
|
+
]);
|
|
1071
|
+
const activityRank = (agent) => agent.status === "needs-you" ? 0 : agent.status === "failed" ? 1 : quietActivity(agent, clock) ? 2 : agent.status === "waiting" ? 3 : agent.status === "running" ? 4 : 5;
|
|
1072
|
+
const parentOf = new Map(edges.filter((e) => e.kind === "spawn").map((e) => [e.to, e.from]));
|
|
1073
|
+
const depthOf = (id) => {
|
|
1074
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
1075
|
+
let depth = 0;
|
|
1076
|
+
while (parentOf.has(id) && !seen.has(parentOf.get(id))) {
|
|
1077
|
+
id = parentOf.get(id);
|
|
1078
|
+
seen.add(id);
|
|
1079
|
+
depth++;
|
|
1080
|
+
}
|
|
1081
|
+
return depth;
|
|
1082
|
+
};
|
|
1083
|
+
const counts = [
|
|
1084
|
+
"needs-you",
|
|
1085
|
+
"failed",
|
|
1086
|
+
"quiet"
|
|
1087
|
+
].map((status) => {
|
|
1088
|
+
const matches = nodes.filter((agent) => status === "quiet" ? quietActivity(agent, clock) : agent.status === status);
|
|
1089
|
+
return {
|
|
1090
|
+
status,
|
|
1091
|
+
matches,
|
|
1092
|
+
current: matches.find((agent) => agent.id === selectedId) ?? matches[0]
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
const caption = /* @__PURE__ */ jsxs("div", {
|
|
1096
|
+
className: "flex h-10 shrink-0 items-center gap-3 px-3 text-xs text-muted-foreground",
|
|
1097
|
+
"aria-label": "Run status",
|
|
1098
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [nodes.length, " agents"] }), counts.filter((group) => group.matches.length).map((group) => /* @__PURE__ */ jsxs("button", {
|
|
1099
|
+
type: "button",
|
|
1100
|
+
className: "ag-status-button rounded-full border border-border px-2 py-1",
|
|
1101
|
+
style: { color: group.status === "quiet" ? "var(--surface-warning-text)" : agentStatus(group.status).color },
|
|
1102
|
+
onClick: () => {
|
|
1103
|
+
const index = group.matches.findIndex((agent) => agent.id === selectedId);
|
|
1104
|
+
const next = group.matches[(index + 1) % group.matches.length];
|
|
1105
|
+
select(next.id);
|
|
1106
|
+
const p = layout?.positions.get(next.id);
|
|
1107
|
+
if (p) rfRef.current?.setCenter(p.x + card.width / 2, p.y + card.height / 2, {
|
|
1108
|
+
zoom: 1,
|
|
1109
|
+
duration: animateMotion ? tokenMilliseconds("--duration-base", wrapperRef.current) : 0
|
|
1110
|
+
});
|
|
1111
|
+
},
|
|
1112
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1113
|
+
className: "shrink-0",
|
|
1114
|
+
children: [
|
|
1115
|
+
group.matches.length,
|
|
1116
|
+
" ",
|
|
1117
|
+
group.status === "needs-you" ? "needs you" : group.status
|
|
1118
|
+
]
|
|
1119
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1120
|
+
className: "ag-caption-target",
|
|
1121
|
+
children: [group.current.label, group.status === "failed" && /* @__PURE__ */ jsxs("span", {
|
|
1122
|
+
className: "font-normal",
|
|
1123
|
+
children: [" · ", group.current.result ?? group.current.turn ?? "Failure cause not recorded"]
|
|
1124
|
+
})]
|
|
1125
|
+
})]
|
|
1126
|
+
}, group.status))]
|
|
1127
|
+
});
|
|
1008
1128
|
if (nodes.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
1009
1129
|
className: `flex items-center justify-center rounded-lg border border-border border-dashed p-4 text-center text-muted-foreground text-xs ${className ?? ""}`,
|
|
1010
1130
|
children: emptyState ?? "No agents yet"
|
|
1011
1131
|
});
|
|
1012
|
-
return /* @__PURE__ */
|
|
1132
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1013
1133
|
ref: (el) => {
|
|
1014
1134
|
wrapperRef.current = el;
|
|
1015
1135
|
measureCanvas(el);
|
|
1016
1136
|
},
|
|
1017
|
-
className: `agent-graph
|
|
1018
|
-
|
|
1137
|
+
className: `agent-graph ag-live relative flex flex-col ${className ?? ""}`,
|
|
1138
|
+
tabIndex: -1,
|
|
1139
|
+
onKeyDownCapture: handleKeyDown,
|
|
1140
|
+
"data-layout": narrow ? "list" : layout && shown ? "ready" : "pending",
|
|
1019
1141
|
"data-columns": layout?.columns,
|
|
1020
|
-
children:
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1142
|
+
children: [
|
|
1143
|
+
caption,
|
|
1144
|
+
showLatestEvents && live && /* @__PURE__ */ jsx(LatestEvents, {
|
|
1145
|
+
agents: nodes,
|
|
1146
|
+
now,
|
|
1147
|
+
live,
|
|
1148
|
+
selectedId,
|
|
1149
|
+
onSelect: onSelect ? select : void 0
|
|
1150
|
+
}),
|
|
1151
|
+
narrow ? /* @__PURE__ */ jsx("ul", {
|
|
1152
|
+
className: "ag-graph-viewport ag-phone-list overflow-auto",
|
|
1153
|
+
"aria-label": "Agents by attention",
|
|
1154
|
+
children: [...nodes].sort((a, b) => activityRank(a) - activityRank(b)).map((agent) => /* @__PURE__ */ jsx("li", {
|
|
1155
|
+
style: { ["--depth"]: depthOf(agent.id) },
|
|
1156
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1157
|
+
role: "button",
|
|
1158
|
+
tabIndex: 0,
|
|
1159
|
+
"aria-pressed": selectedId === agent.id,
|
|
1160
|
+
"aria-label": agentSummary(agent),
|
|
1161
|
+
"data-agent-id": agent.id,
|
|
1162
|
+
className: "ag-phone-select",
|
|
1163
|
+
onClick: () => select(agent.id),
|
|
1164
|
+
onKeyDown: (e) => {
|
|
1165
|
+
if ((e.key === "Enter" || e.key === " ") && e.target === e.currentTarget) {
|
|
1166
|
+
e.preventDefault();
|
|
1167
|
+
select(agent.id);
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
children: live || agent.status === "succeeded" || agent.status === "stopped" ? /* @__PURE__ */ jsx(LiveAgentCardView, {
|
|
1171
|
+
agent,
|
|
1172
|
+
parentLabel: byId.get(parentOf.get(agent.id) ?? "")?.label,
|
|
1173
|
+
selected: selectedId === agent.id,
|
|
1174
|
+
now: clock,
|
|
1175
|
+
renderEventIcon,
|
|
1176
|
+
onOpen: onSelect ? () => select(agent.id) : void 0
|
|
1177
|
+
}) : /* @__PURE__ */ jsx(AgentCardView, {
|
|
1178
|
+
agent,
|
|
1179
|
+
parentLabel: byId.get(parentOf.get(agent.id) ?? "")?.label,
|
|
1180
|
+
selected: selectedId === agent.id,
|
|
1181
|
+
now: clock,
|
|
1182
|
+
direction,
|
|
1183
|
+
onOpen: onSelect ? () => select(agent.id) : void 0
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
}, agent.id))
|
|
1187
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
1188
|
+
className: "ag-graph-viewport min-h-0 flex-1",
|
|
1189
|
+
style: { opacity: shown ? 1 : 0 },
|
|
1190
|
+
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
1191
|
+
nodes: flowNodes,
|
|
1192
|
+
edges: flowEdges,
|
|
1193
|
+
nodeTypes: NODE_TYPES,
|
|
1194
|
+
edgeTypes: EDGE_TYPES,
|
|
1195
|
+
colorMode,
|
|
1196
|
+
onInit: (inst) => {
|
|
1197
|
+
rfRef.current = inst;
|
|
1198
|
+
setReady(true);
|
|
1063
1199
|
},
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1200
|
+
onMoveStart: (event) => {
|
|
1201
|
+
if (event) readerMoved.current = true;
|
|
1202
|
+
},
|
|
1203
|
+
onNodeClick: handleNodeClick,
|
|
1204
|
+
onNodeMouseEnter: (_, node) => setHovered(node.id),
|
|
1205
|
+
onNodeMouseLeave: () => setHovered(null),
|
|
1206
|
+
proOptions: { hideAttribution: true },
|
|
1207
|
+
nodesDraggable: false,
|
|
1208
|
+
nodesConnectable: false,
|
|
1209
|
+
elementsSelectable: false,
|
|
1210
|
+
nodesFocusable: true,
|
|
1211
|
+
edgesFocusable: false,
|
|
1212
|
+
deleteKeyCode: null,
|
|
1213
|
+
panOnScroll: true,
|
|
1214
|
+
zoomOnScroll: false,
|
|
1215
|
+
zoomOnPinch: true,
|
|
1216
|
+
zoomOnDoubleClick: false,
|
|
1217
|
+
panOnDrag: true,
|
|
1218
|
+
minZoom,
|
|
1219
|
+
maxZoom: 1.75,
|
|
1220
|
+
"aria-label": "Agent topology",
|
|
1221
|
+
children: [
|
|
1222
|
+
/* @__PURE__ */ jsx(Background, {
|
|
1223
|
+
bgColor: "var(--bg-root)",
|
|
1224
|
+
gap: 16,
|
|
1225
|
+
size: 1,
|
|
1226
|
+
color: "hsl(var(--foreground) / 0.06)"
|
|
1227
|
+
}),
|
|
1228
|
+
nodes.length > MINIMAP_MIN_NODES && /* @__PURE__ */ jsx(MiniMap, {
|
|
1229
|
+
pannable: true,
|
|
1230
|
+
zoomable: true,
|
|
1231
|
+
position: "bottom-left",
|
|
1232
|
+
style: {
|
|
1233
|
+
width: 168,
|
|
1234
|
+
height: 104
|
|
1235
|
+
},
|
|
1236
|
+
className: "!hidden sm:!block",
|
|
1237
|
+
ariaLabel: "Agent topology overview"
|
|
1238
|
+
}),
|
|
1239
|
+
/* @__PURE__ */ jsx(Controls, {
|
|
1240
|
+
showInteractive: false,
|
|
1241
|
+
position: "bottom-right",
|
|
1242
|
+
fitViewOptions: {
|
|
1243
|
+
padding: `${FIT_PADDING}px`,
|
|
1244
|
+
maxZoom: 1
|
|
1245
|
+
}
|
|
1246
|
+
})
|
|
1247
|
+
]
|
|
1074
1248
|
})
|
|
1075
|
-
|
|
1076
|
-
|
|
1249
|
+
})
|
|
1250
|
+
]
|
|
1077
1251
|
});
|
|
1078
1252
|
}
|
|
1079
1253
|
export { AgentGraphCanvas };
|