@tangle-network/sandbox-ui 1.1.10 → 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.
Files changed (34) hide show
  1. package/dist/{AgentGraph-DxAeQQch.d.ts → AgentGraph-B4uWLXlD.d.ts} +12 -17
  2. package/dist/AgentGraph-C1DsA-Rd.js +1369 -0
  3. package/dist/{AgentGraphCanvas-CMdOlSlh.js → AgentGraphCanvas-Bcxq-e6E.js} +557 -383
  4. package/dist/{WorkflowGraph-DZ3xejKE.js → WorkflowGraph-BDjc7V_U.js} +3 -2
  5. package/dist/canvas-theme-DGd0fm1H.js +58 -0
  6. package/dist/{chat-4OZD23RA.js → chat-BrlLa4rg.js} +2 -2
  7. package/dist/chat.d.ts +2 -2
  8. package/dist/chat.js +1 -1
  9. package/dist/{dashboard-DZ6FcC_Z.js → dashboard-FMrtIe9S.js} +1 -1
  10. package/dist/dashboard.js +1 -1
  11. package/dist/globals.css +404 -240
  12. package/dist/{index-ucJHuuVZ.d.ts → index-5zOVKgDT.d.ts} +2 -2
  13. package/dist/index-BK8KKLNY.d.ts +26 -0
  14. package/dist/{index-72uBu5lu.d.ts → index-D9-IiEpQ.d.ts} +21 -6
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +7 -6
  17. package/dist/latest-events-B34ElSTE.js +190 -0
  18. package/dist/{AgentGraph-DJEQrKTs.js → node-ui-Bfnh08U3.js} +40 -1194
  19. package/dist/primitives-CCdx5Hh_.js +23 -0
  20. package/dist/primitives.d.ts +2 -2
  21. package/dist/primitives.js +1 -1
  22. package/dist/{run-Qern6muT.js → run-BqpdpP1K.js} +161 -60
  23. package/dist/run.d.ts +2 -2
  24. package/dist/run.js +3 -1
  25. package/dist/styles.css +404 -240
  26. package/dist/tailwind.css +70 -43
  27. package/dist/{workbench-Ca6ZqAs5.js → workbench-BVnZNhP2.js} +1 -1
  28. package/dist/workbench.js +1 -1
  29. package/dist/workflows.d.ts +3 -7
  30. package/dist/workflows.js +2 -1
  31. package/package.json +9 -8
  32. package/dist/elk-loader-Dl9S7jQk.js +0 -12
  33. package/dist/index-CFJ1iJhu.d.ts +0 -26
  34. package/dist/primitives-B-76QdOU.js +0 -23
@@ -1,21 +1,24 @@
1
1
  import { t as MOTION_CONTROL } from "./motion-Bn7ESHrj.js";
2
- import { T as statusBorder, U as fmtTokenTotal, V as fmtCost, a as useColorMode, c as NodeMark, d as RESTING_BORDER, f as RESTING_SHADOW, i as motionAllowed, o as KIND_ACCENT, p as STATUS_COLOR, r as SELECTION_OUTLINE, y as edgeColor } from "./AgentGraph-DJEQrKTs.js";
3
- import { t as loadElk } from "./elk-loader-Dl9S7jQk.js";
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 { CircleCheck, CirclePause, CircleQuestionMark, CircleStop, CircleX, FileText, GitBranch, Globe, Hand, Hourglass, LoaderCircle, Pencil, Search, Sparkles, Terminal, Users } from "lucide-react";
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: 200,
13
- height: 78
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: 152
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
- const MUTED = "hsl(var(--muted-foreground))";
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
- return AGENT_STATUS[status] ?? AGENT_STATUS.unknown;
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
- const s = agentStatus(status);
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
- strokeWidth: 2.25,
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 === "running" || status === "succeeded") return { borderColor: RESTING_BORDER };
343
- if (status === "unknown") return {
344
- borderColor: "hsl(var(--muted-foreground) / 0.45)",
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
- if (status === "idle") return { borderColor: "hsl(var(--border))" };
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 AgentCard({ data }) {
387
- const { agent, selected, direction } = data;
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 cursor-pointer flex-col justify-between rounded-xl border bg-card px-3 pt-2.5 pb-2 text-left hover:-translate-y-px motion-reduce:hover:translate-y-0 after:pointer-events-none after:absolute after:inset-0 after:rounded-xl after:opacity-0 after:shadow-[0_10px_28px_-10px_hsl(var(--foreground)/0.45)] after:transition-opacity after:duration-150 after:ease-out hover:after:opacity-100 motion-reduce:after:transition-none group-focus-visible/agent:outline-2 group-focus-visible/agent:outline-offset-4 group-focus-visible/agent:outline-ring ${MOTION_CONTROL}`,
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.5",
355
+ className: "flex min-w-0 items-center gap-2",
406
356
  children: [
407
- /* @__PURE__ */ jsx(NodeMark, {
408
- kind: "agent.run",
409
- model: agent.model ?? void 0,
410
- accent: KIND_ACCENT["agent.run"],
411
- tile: 30
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("div", {
414
- className: "min-w-0 flex-1",
415
- children: [/* @__PURE__ */ jsx("div", {
416
- className: "truncate font-semibold text-[13px] text-foreground leading-[18px] tracking-[-0.01em]",
417
- children: agent.label
418
- }), /* @__PURE__ */ jsx("div", {
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("div", {
424
- className: "flex shrink-0 flex-col items-end gap-0.5 self-start",
425
- children: /* @__PURE__ */ jsx(StatusGlyph, {
426
- status: agent.status,
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 min-w-0 items-center gap-1.5 text-[11px] leading-4",
434
- children: [
435
- /* @__PURE__ */ jsx("span", {
436
- className: "shrink-0 font-medium",
437
- style: { color: s.color === MUTED ? void 0 : s.color },
438
- children: s.label
439
- }),
440
- /* @__PURE__ */ jsx("span", {
441
- className: "min-w-0 flex-1 truncate text-muted-foreground",
442
- children: agent.turn ? agent.turn : ""
443
- }),
444
- (cost ?? tokens) && /* @__PURE__ */ jsx("span", {
445
- className: "shrink-0 text-muted-foreground tabular-nums",
446
- children: cost ?? tokens
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
- "running",
415
+ "needs-you",
416
+ "failed",
417
+ "quiet",
456
418
  "waiting",
457
- "failed"
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: [/* @__PURE__ */ jsx(StatusGlyph, {
498
- status: c.status,
499
- size: 11
500
- }), c.n]
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
- /** The secondary edges' colours: a message is quiet, a delegation is the
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(--primary)) 75%, transparent)"
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
- const EVENT_GLYPHS = [
601
- [/^(bash|sh|run|exec|cargo|npm|pnpm|make)\b/, Terminal],
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
- function LiveAgentCard({ data }) {
644
- const { agent, selected, direction } = data;
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 now = useNow(data.now, !settled && !failed);
574
+ const quiet = quietActivity(agent, now);
648
575
  const s = agentStatus(agent.status);
649
- const alive = aliveFor(agent.startedAt, now);
650
- if (settled) return /* @__PURE__ */ jsxs("div", {
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
- "data-status": agent.status,
653
- title: agentSummary(agent),
654
- children: [/* @__PURE__ */ jsx(Handles, { direction }), /* @__PURE__ */ jsxs("div", {
655
- className: "ag-live-chip",
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
- style: selected ? SELECTION_OUTLINE : void 0,
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: 13
662
- }),
663
- /* @__PURE__ */ jsx("span", {
664
- className: "ag-live-chip-name",
665
- children: agent.label
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
- const meta = [agent.model?.replace(/^[\w-]+\//, ""), agent.effort].filter(Boolean).join(" · ");
675
- const events = (agent.events ?? []).slice(-3);
676
- return /* @__PURE__ */ jsxs("div", {
677
- className: "ag-live-card",
678
- "data-status": agent.status,
679
- "data-activity": agent.activity ?? void 0,
680
- "data-selected": selected || void 0,
681
- title: agentSummary(agent),
682
- style: {
683
- boxShadow: RESTING_SHADOW,
684
- ...cardBorder(agent.status),
685
- ...selected ? SELECTION_OUTLINE : {},
686
- ["--ag-status"]: s.color
687
- },
688
- children: [
689
- /* @__PURE__ */ jsx(Handles, { direction }),
690
- /* @__PURE__ */ jsxs("div", {
691
- className: "ag-live-head",
692
- children: [
693
- /* @__PURE__ */ jsx(NodeMark, {
694
- kind: "agent.run",
695
- model: agent.model ?? void 0,
696
- accent: KIND_ACCENT["agent.run"],
697
- tile: 28
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
- alive ? " · " : "",
724
- agent.commits,
725
- " ",
726
- agent.commits === 1 ? "commit" : "commits"
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
- failed ? /* @__PURE__ */ jsx("p", {
733
- className: "ag-live-thinking ag-live-failure",
734
- children: agent.result ?? events.at(-1)?.label ?? s.label
735
- }) : /* @__PURE__ */ jsx("p", {
736
- className: "ag-live-thinking",
737
- children: agent.turn || /* @__PURE__ */ jsx("span", {
738
- className: "ag-live-quiet",
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 = el.getBoundingClientRect();
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(el);
787
+ observer.current.observe(viewport);
802
788
  read();
803
789
  }, [])];
804
790
  }
805
- function AgentGraphCanvas({ nodes, edges, selectedId, onSelect, direction = "TB", className, emptyState, live = false, now }) {
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 layout = useMemo(() => layouts && layouts.key === layoutKey && canvas ? pickAgentLayout(layouts.all, canvas, FIT_PADDING) : null, [
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
- now
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(flow),
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
- if (!relayout && readerMoved.current) return;
975
- const animate = framed.current !== null && relayout && motionAllowed();
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 = false;
1003
+ readerMoved.current = preserveView;
978
1004
  const frame = requestAnimationFrame(() => {
979
- inst.fitView({
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 ? 220 : 0
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)) onSelect?.(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
- if (!event.target.classList.contains("react-flow__node")) return;
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
- select(id);
1007
- }, [byId, select]);
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__ */ jsx("div", {
1132
+ return /* @__PURE__ */ jsxs("div", {
1013
1133
  ref: (el) => {
1014
1134
  wrapperRef.current = el;
1015
1135
  measureCanvas(el);
1016
1136
  },
1017
- className: `agent-graph${live ? " ag-live" : ""} relative transition-opacity duration-150 ease-out motion-reduce:transition-none ${layout && shown ? "opacity-100" : "opacity-0"} ${className ?? ""}`,
1018
- "data-layout": layout && shown ? "ready" : "pending",
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: /* @__PURE__ */ jsxs(ReactFlow, {
1021
- nodes: flowNodes,
1022
- edges: flowEdges,
1023
- nodeTypes: NODE_TYPES,
1024
- edgeTypes: EDGE_TYPES,
1025
- colorMode,
1026
- onInit: (inst) => {
1027
- rfRef.current = inst;
1028
- setReady(true);
1029
- },
1030
- onMoveStart: (event) => {
1031
- if (event) readerMoved.current = true;
1032
- },
1033
- onNodeClick: handleNodeClick,
1034
- onKeyDown: handleKeyDown,
1035
- proOptions: { hideAttribution: true },
1036
- nodesDraggable: false,
1037
- nodesConnectable: false,
1038
- elementsSelectable: false,
1039
- nodesFocusable: true,
1040
- edgesFocusable: false,
1041
- deleteKeyCode: null,
1042
- panOnScroll: true,
1043
- zoomOnScroll: false,
1044
- zoomOnPinch: true,
1045
- zoomOnDoubleClick: false,
1046
- panOnDrag: true,
1047
- minZoom,
1048
- maxZoom: 1.75,
1049
- "aria-label": "Agent topology",
1050
- children: [
1051
- /* @__PURE__ */ jsx(Background, {
1052
- gap: 18,
1053
- size: 1.4,
1054
- color: "hsl(var(--muted-foreground) / 0.38)"
1055
- }),
1056
- nodes.length > MINIMAP_MIN_NODES && /* @__PURE__ */ jsx(MiniMap, {
1057
- pannable: true,
1058
- zoomable: true,
1059
- position: "bottom-left",
1060
- style: {
1061
- width: 168,
1062
- height: 104
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
- className: "!hidden sm:!block",
1065
- ariaLabel: "Agent topology overview"
1066
- }),
1067
- /* @__PURE__ */ jsx(Controls, {
1068
- showInteractive: false,
1069
- position: "bottom-right",
1070
- fitViewOptions: {
1071
- padding: `${FIT_PADDING}px`,
1072
- maxZoom: 1
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 };