@tangle-network/sandbox-ui 0.94.2 → 0.95.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.
@@ -2,19 +2,22 @@ import {
2
2
  ProviderIcon
3
3
  } from "./chunk-GTJ5PSK5.js";
4
4
  import {
5
+ ACTION_OUTPUT_ROWS,
6
+ AGENT_BODY_ROWS,
5
7
  COMPACT_NODE_SIZE,
6
8
  COMPACT_TILE,
7
9
  OUTPUT_PREVIEW_CHARS,
8
- OUTPUT_ROWS,
10
+ STRUCTURAL_OUTPUT_ROWS,
9
11
  buildWorkflowGraph,
10
12
  clampPreview,
11
13
  fmtCost,
12
14
  fmtDuration,
13
15
  fmtTokens,
16
+ isStructuralKind,
14
17
  providerLabel,
15
18
  shortModel,
16
19
  triggerNodeIndex
17
- } from "./chunk-7QKAVIZX.js";
20
+ } from "./chunk-O7MHUR4U.js";
18
21
  import {
19
22
  ModelBrandStack,
20
23
  modelBrandFor
@@ -28,7 +31,6 @@ import {
28
31
  BaseEdge,
29
32
  Controls,
30
33
  EdgeLabelRenderer,
31
- getNodesBounds,
32
34
  getSmoothStepPath,
33
35
  getViewportForBounds,
34
36
  Handle,
@@ -54,6 +56,12 @@ import {
54
56
  // src/workflows/flow-graph.ts
55
57
  import { Position } from "@xyflow/react";
56
58
  var WF_EDGE_TYPE = "wfEdge";
59
+ var SIDE_POSITION = {
60
+ left: Position.Left,
61
+ right: Position.Right,
62
+ top: Position.Top,
63
+ bottom: Position.Bottom
64
+ };
57
65
  function isNodeStateMap(arg) {
58
66
  const values = Object.values(arg);
59
67
  return values.length > 0 && values.every(
@@ -62,7 +70,7 @@ function isNodeStateMap(arg) {
62
70
  }
63
71
  function normalizeFlowGraphOptions(arg) {
64
72
  if (!arg) return {};
65
- if ("nodeState" in arg || "direction" in arg || "compact" in arg || "edges" in arg) {
73
+ if ("nodeState" in arg || "direction" in arg || "compact" in arg || "wrap" in arg || "edges" in arg) {
66
74
  return arg;
67
75
  }
68
76
  return isNodeStateMap(arg) ? { nodeState: arg } : {};
@@ -76,6 +84,7 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
76
84
  reserveRunState: nodeState !== void 0,
77
85
  direction,
78
86
  compact,
87
+ ...options.wrap ? { wrap: true } : {},
79
88
  ...options.edges ? { edges: options.edges } : {}
80
89
  });
81
90
  const isLR = direction === "LR";
@@ -100,8 +109,11 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
100
109
  // Fix the DOM box to the laid-out size so the card can't grow past its
101
110
  // reserved space and overlap a neighbour (see model.ts nodeHeight).
102
111
  style: { width: n.width, height: n.height },
103
- sourcePosition,
104
- targetPosition,
112
+ // A folded layout names each node's own sides (a mirrored row, a corner
113
+ // that leaves downward); a straight one names none and takes the
114
+ // direction's answer for every node.
115
+ sourcePosition: n.sourceSide ? SIDE_POSITION[n.sourceSide] : sourcePosition,
116
+ targetPosition: n.targetSide ? SIDE_POSITION[n.targetSide] : targetPosition,
105
117
  data: nodeState?.[n.id] ? { ...n.data, state: nodeState[n.id] } : n.data
106
118
  })
107
119
  ),
@@ -203,19 +215,23 @@ function classifyOutput(preview, condenseMarkdown = false) {
203
215
  var CLAMP_BY_ROWS = {
204
216
  1: "truncate",
205
217
  2: "line-clamp-2",
206
- 3: "line-clamp-3"
218
+ 3: "line-clamp-3",
219
+ 4: "line-clamp-4",
220
+ 5: "line-clamp-5",
221
+ 6: "line-clamp-6"
207
222
  };
208
223
  function NodeOutputBody({
209
224
  shape,
210
225
  rows = 2,
211
226
  tone = "default"
212
227
  }) {
213
- const clampClass = CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
228
+ const unclamped = rows === "none";
229
+ const clampClass = unclamped ? "" : CLAMP_BY_ROWS[rows] ?? "line-clamp-2";
214
230
  const codeWrap = rows === 1 ? "" : "whitespace-pre-wrap break-all";
215
231
  if (shape.kind === "empty") return null;
216
232
  if (shape.kind === "json") {
217
- const overflow = shape.truncated || shape.entries.length > rows;
218
- const visible = shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
233
+ const overflow = unclamped ? shape.truncated : shape.truncated || shape.entries.length > rows;
234
+ const visible = unclamped ? shape.entries : shape.entries.slice(0, overflow ? Math.max(0, rows - 1) : rows);
219
235
  const isError = tone === "error";
220
236
  const keyStyle = isError ? ERROR_TEXT : void 0;
221
237
  const keyClass = isError ? "" : "text-muted-foreground";
@@ -283,7 +299,8 @@ import {
283
299
  Webhook,
284
300
  Zap
285
301
  } from "lucide-react";
286
- import { jsx as jsx2 } from "react/jsx-runtime";
302
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
303
+ var MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
287
304
  var TONE_ACCENT = {
288
305
  trigger: "hsl(var(--primary))",
289
306
  structural: "var(--surface-warning-text)",
@@ -378,6 +395,62 @@ var KIND_ICON = {
378
395
  "script.run": Code,
379
396
  "trace.analyze": ScanSearch
380
397
  };
398
+ function StatusPill({ status }) {
399
+ return /* @__PURE__ */ jsx2(
400
+ "span",
401
+ {
402
+ className: "shrink-0 whitespace-nowrap rounded-full border px-2 py-[1px] font-medium text-[10px]",
403
+ style: STATUS_PILL[status],
404
+ children: STATUS_LABEL[status]
405
+ }
406
+ );
407
+ }
408
+ function StatusFooter({
409
+ status,
410
+ rounds,
411
+ cost,
412
+ tokens,
413
+ elapsed
414
+ }) {
415
+ const roundsLabel = rounds !== void 0 ? `${rounds} round${rounds === 1 ? "" : "s"}` : void 0;
416
+ const caption = [roundsLabel, cost, tokens].filter(Boolean).join(" \xB7 ");
417
+ return /* @__PURE__ */ jsxs2("div", { className: "wf-node-body-in mt-auto border-border border-t", children: [
418
+ /* @__PURE__ */ jsx2(
419
+ "div",
420
+ {
421
+ className: "h-1 w-full overflow-hidden",
422
+ style: { background: MUTED_TRACK },
423
+ children: /* @__PURE__ */ jsx2(
424
+ "div",
425
+ {
426
+ "data-testid": "wf-node-progress",
427
+ className: `h-full ${status === "running" ? "animate-pulse" : ""}`,
428
+ style: {
429
+ width: progressFill(status),
430
+ background: STATUS_COLOR[status]
431
+ }
432
+ }
433
+ )
434
+ }
435
+ ),
436
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 px-3.5 py-1 text-[10px] text-muted-foreground leading-[15px]", children: [
437
+ /* @__PURE__ */ jsx2("span", { className: "min-h-[15px] truncate tabular-nums", children: caption }),
438
+ /* @__PURE__ */ jsx2("span", { className: "min-h-[15px] shrink-0 tabular-nums", children: elapsed ?? "" })
439
+ ] })
440
+ ] });
441
+ }
442
+ function emptySlotLabel(status) {
443
+ switch (status) {
444
+ case "queued":
445
+ return "Not run yet";
446
+ case "succeeded":
447
+ return "No output";
448
+ case "failed":
449
+ return "No error reported";
450
+ default:
451
+ return void 0;
452
+ }
453
+ }
381
454
  function NodeMark({
382
455
  kind,
383
456
  provider,
@@ -425,12 +498,44 @@ function NodeMark({
425
498
  }
426
499
 
427
500
  // src/workflows/WorkflowGraph.tsx
428
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
429
- var MUTED_TRACK = "color-mix(in srgb, hsl(var(--muted-foreground)) 22%, transparent)";
430
- var FIT_VIEW = { padding: 0.16, minZoom: 0.55 };
501
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
502
+ var FIT_VIEW = { padding: 0.16 };
431
503
  function fitZoomCeiling(compact) {
432
504
  return compact ? 1.5 : 1;
433
505
  }
506
+ function fitZoomFloor(compact) {
507
+ return compact ? 0.55 : 0.35;
508
+ }
509
+ function layoutBounds(nodes) {
510
+ if (nodes.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
511
+ let minX = Number.POSITIVE_INFINITY;
512
+ let minY = Number.POSITIVE_INFINITY;
513
+ let maxX = Number.NEGATIVE_INFINITY;
514
+ let maxY = Number.NEGATIVE_INFINITY;
515
+ for (const node of nodes) {
516
+ minX = Math.min(minX, node.position.x);
517
+ minY = Math.min(minY, node.position.y);
518
+ maxX = Math.max(maxX, node.position.x + (node.width ?? 0));
519
+ maxY = Math.max(maxY, node.position.y + (node.height ?? 0));
520
+ }
521
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
522
+ }
523
+ function framingViewport(bounds, width, height, compact) {
524
+ const fit = getViewportForBounds(
525
+ bounds,
526
+ width,
527
+ height,
528
+ fitZoomFloor(compact),
529
+ fitZoomCeiling(compact),
530
+ FIT_VIEW.padding
531
+ );
532
+ const anchored = (offset, origin) => offset + Math.max(0, -(offset + origin * fit.zoom));
533
+ return {
534
+ zoom: fit.zoom,
535
+ x: anchored(fit.x, bounds.x),
536
+ y: anchored(fit.y, bounds.y)
537
+ };
538
+ }
434
539
  var LAYOUT_TRANSITION_MS = 220;
435
540
  var DENSITY_FLIP_AT = { grow: 0.12, shrink: 0.75 };
436
541
  function motionAllowed() {
@@ -455,6 +560,7 @@ function useColorMode() {
455
560
  var DirectionContext = createContext("LR");
456
561
  var DensityContext = createContext(false);
457
562
  var SelectedNodeContext = createContext(void 0);
563
+ var RunModeContext = createContext(false);
458
564
  var SELECTION_OUTLINE = {
459
565
  outline: "2px solid hsl(var(--primary))",
460
566
  outlineOffset: 2
@@ -463,59 +569,6 @@ var ConnectableContext = createContext(false);
463
569
  function isEditableEdge(edge) {
464
570
  return edge.data?.kind !== "fork" && triggerNodeIndex(edge.source) === null;
465
571
  }
466
- function StatusFooter({
467
- status,
468
- rounds,
469
- elapsed
470
- }) {
471
- const roundsLabel = rounds !== void 0 ? `${rounds} round${rounds === 1 ? "" : "s"}` : void 0;
472
- return /* @__PURE__ */ jsxs2("div", { className: "wf-node-body-in mt-auto border-border border-t", children: [
473
- /* @__PURE__ */ jsx3(
474
- "div",
475
- {
476
- className: "h-1 w-full overflow-hidden",
477
- style: { background: MUTED_TRACK },
478
- children: /* @__PURE__ */ jsx3(
479
- "div",
480
- {
481
- "data-testid": "wf-node-progress",
482
- className: `h-full ${status === "running" ? "animate-pulse" : ""}`,
483
- style: {
484
- width: progressFill(status),
485
- background: STATUS_COLOR[status]
486
- }
487
- }
488
- )
489
- }
490
- ),
491
- /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 px-3.5 py-1 text-[10px] text-muted-foreground leading-[15px]", children: [
492
- /* @__PURE__ */ jsx3("span", { className: "min-h-[15px] truncate", children: roundsLabel ?? "" }),
493
- /* @__PURE__ */ jsx3("span", { className: "min-h-[15px] shrink-0 tabular-nums", children: elapsed ?? "" })
494
- ] })
495
- ] });
496
- }
497
- function emptySlotLabel(status) {
498
- switch (status) {
499
- case "queued":
500
- return "Not run yet";
501
- case "succeeded":
502
- return "No output";
503
- case "failed":
504
- return "No error reported";
505
- default:
506
- return void 0;
507
- }
508
- }
509
- function StatusPill({ status }) {
510
- return /* @__PURE__ */ jsx3(
511
- "span",
512
- {
513
- className: "shrink-0 rounded-full border px-2 py-[1px] font-medium text-[10px]",
514
- style: STATUS_PILL[status],
515
- children: STATUS_LABEL[status]
516
- }
517
- );
518
- }
519
572
  var HANDLE_CLASS = "!h-2 !w-2 !min-w-0 !border-0 !bg-transparent opacity-0";
520
573
  var HANDLE_CONNECTABLE_CLASS = "!h-2.5 !w-2.5 !min-w-0 !rounded-full !border !border-border !bg-muted-foreground/70 opacity-100 transition-opacity hover:!bg-primary";
521
574
  function compactHandleStyle(position, isLR) {
@@ -539,22 +592,32 @@ function compactHandleStyle(position, isLR) {
539
592
  return anchor(tileLeft + center, COMPACT_TILE);
540
593
  }
541
594
  }
542
- function WorkflowNode({ id, data }) {
595
+ function WorkflowNode({
596
+ id,
597
+ data,
598
+ // Set only where the LAYOUT decides a node's own sides — a folded row that
599
+ // runs right-to-left, or the step that turns the corner and leaves downward.
600
+ // A straight layer flow leaves them to the direction, below.
601
+ sourcePosition,
602
+ targetPosition
603
+ }) {
543
604
  const d = data;
544
605
  const state = d.state;
545
606
  const direction = useContext(DirectionContext);
546
607
  const compact = useContext(DensityContext);
608
+ const runMode = useContext(RunModeContext);
547
609
  const connectable = useContext(ConnectableContext);
548
610
  const hostSelection = useContext(SelectedNodeContext);
549
611
  const selected = hostSelection !== void 0 && hostSelection === id;
550
612
  const isLR = direction === "LR";
551
- const targetPos = isLR ? Position2.Left : Position2.Top;
552
- const sourcePos = isLR ? Position2.Right : Position2.Bottom;
613
+ const targetPos = targetPosition ?? (isLR ? Position2.Left : Position2.Top);
614
+ const sourcePos = sourcePosition ?? (isLR ? Position2.Right : Position2.Bottom);
553
615
  const accent = TONE_ACCENT[d.tone];
554
616
  const isAgent = d.kind === "agent.run";
555
617
  const subtitle = isAgent && state?.model ? shortModel(state.model) : d.subtitle;
556
618
  const markModel = isAgent && state?.model ? state.model : d.model;
557
- const runBands = state !== void 0 && d.tone !== "trigger";
619
+ const runBands = runMode && d.tone !== "trigger";
620
+ const runStatus = runBands ? state?.status ?? "queued" : state?.status;
558
621
  const duration = fmtDuration(state?.durationMs);
559
622
  const cost = fmtCost(state?.costUsd);
560
623
  const tokens = isAgent ? fmtTokens(state?.inputTokens, state?.outputTokens) : void 0;
@@ -574,7 +637,7 @@ function WorkflowNode({ id, data }) {
574
637
  return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
575
638
  })();
576
639
  const handleClass = connectable ? HANDLE_CONNECTABLE_CLASS : HANDLE_CLASS;
577
- const handles = /* @__PURE__ */ jsxs2(Fragment, { children: [
640
+ const handles = /* @__PURE__ */ jsxs3(Fragment, { children: [
578
641
  !d.isRoot && /* @__PURE__ */ jsx3(
579
642
  Handle,
580
643
  {
@@ -595,13 +658,13 @@ function WorkflowNode({ id, data }) {
595
658
  )
596
659
  ] });
597
660
  if (compact) {
598
- return /* @__PURE__ */ jsxs2(
661
+ return /* @__PURE__ */ jsxs3(
599
662
  "div",
600
663
  {
601
664
  className: `relative flex h-full w-full ${isLR ? "flex-col items-center" : "flex-row items-center"}`,
602
665
  children: [
603
666
  handles,
604
- /* @__PURE__ */ jsxs2(
667
+ /* @__PURE__ */ jsxs3(
605
668
  "span",
606
669
  {
607
670
  className: "relative flex items-center justify-center rounded-xl border bg-card shadow-sm transition-colors",
@@ -641,7 +704,7 @@ function WorkflowNode({ id, data }) {
641
704
  ]
642
705
  }
643
706
  ),
644
- /* @__PURE__ */ jsxs2(
707
+ /* @__PURE__ */ jsxs3(
645
708
  "div",
646
709
  {
647
710
  className: `wf-node-body-in min-w-0 ${isLR ? "mt-2 w-full px-1 text-center" : "ml-2 flex-1 text-left"}`,
@@ -670,82 +733,250 @@ function WorkflowNode({ id, data }) {
670
733
  }
671
734
  );
672
735
  }
673
- return /* @__PURE__ */ jsxs2(
736
+ const cardShell = (children) => /* @__PURE__ */ jsxs3(
674
737
  "div",
675
738
  {
676
739
  className: "relative flex h-full w-full flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors",
677
740
  style: {
678
- ...state ? statusBorder(state.status) : {
741
+ ...runStatus ? statusBorder(runStatus) : {
679
742
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
680
743
  },
681
744
  ...selected ? SELECTION_OUTLINE : {}
682
745
  },
683
746
  children: [
684
747
  handles,
685
- /* @__PURE__ */ jsxs2("div", { className: "wf-node-body-in flex min-h-0 flex-1 flex-col overflow-hidden px-3.5 pt-2.5 pb-2", children: [
686
- /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-2.5", children: [
687
- /* @__PURE__ */ jsx3(
688
- NodeMark,
748
+ children
749
+ ]
750
+ }
751
+ );
752
+ const identityRow = /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-2.5", children: [
753
+ /* @__PURE__ */ jsx3(
754
+ NodeMark,
755
+ {
756
+ kind: d.kind,
757
+ provider: d.provider,
758
+ model: markModel,
759
+ accent,
760
+ tile: 34
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
764
+ /* @__PURE__ */ jsx3(
765
+ "div",
766
+ {
767
+ className: "truncate font-semibold text-[13px] text-foreground leading-tight",
768
+ title: d.title,
769
+ children: d.title
770
+ }
771
+ ),
772
+ subtitle && /* @__PURE__ */ jsx3(
773
+ "div",
774
+ {
775
+ className: "truncate text-[11px] text-muted-foreground leading-tight",
776
+ title: subtitle,
777
+ children: subtitle
778
+ }
779
+ )
780
+ ] }),
781
+ runStatus ? /* @__PURE__ */ jsx3(StatusPill, { status: runStatus }) : d.badge && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-surface-container-high px-2 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge })
782
+ ] });
783
+ const footer = runStatus ? /* @__PURE__ */ jsx3(
784
+ StatusFooter,
785
+ {
786
+ status: runStatus,
787
+ rounds: isAgent ? state?.rounds : void 0,
788
+ cost,
789
+ tokens,
790
+ elapsed: duration
791
+ }
792
+ ) : null;
793
+ const descriptionBand = (emphasized) => d.description ? /* @__PURE__ */ jsx3(
794
+ "p",
795
+ {
796
+ className: `mt-2 line-clamp-2 shrink-0 text-[11px] leading-snug ${emphasized ? "text-foreground" : "text-muted-foreground"}`,
797
+ title: d.description,
798
+ children: d.description
799
+ }
800
+ ) : null;
801
+ const emptySlot = runStatus && !runOutput && emptySlotLabel(runStatus) ? /* @__PURE__ */ jsx3("div", { className: "mt-2 flex flex-1 items-center text-[11px] text-muted-foreground italic", children: emptySlotLabel(runStatus) }) : null;
802
+ const outputWell = (rows) => runOutput ? /* @__PURE__ */ jsxs3("div", { className: "mt-2 min-h-0 grow shrink-0 rounded-lg border border-border bg-surface-container-high/60 px-2 py-1.5", children: [
803
+ /* @__PURE__ */ jsx3("div", { className: "mb-1 font-semibold text-[9px] text-muted-foreground uppercase tracking-[0.09em]", children: runOutput.label }),
804
+ /* @__PURE__ */ jsx3(
805
+ NodeOutputBody,
806
+ {
807
+ shape: runOutput.shape,
808
+ tone: runOutput.tone,
809
+ rows
810
+ }
811
+ )
812
+ ] }) : emptySlot;
813
+ const body = (children, justify = "") => (
814
+ /* The content region owns its overflow: each band is `shrink-0`, so a band
815
+ that renders taller than its reservation (a consumer's font metrics) is
816
+ CLIPPED here rather than squeezed — a squeezed band cuts a line of text
817
+ in half, and pushes the pinned footer off the card. Fades in when the
818
+ density swap lands mid layout-morph; the card's border/surface (the
819
+ root) stays opaque so the node never blinks out. */
820
+ /* @__PURE__ */ jsx3(
821
+ "div",
822
+ {
823
+ className: `wf-node-body-in flex min-h-0 flex-1 flex-col overflow-hidden px-3.5 pt-2.5 pb-2 ${justify}`,
824
+ children
825
+ }
826
+ )
827
+ );
828
+ if (!runBands) {
829
+ return cardShell(body(
830
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
831
+ identityRow,
832
+ descriptionBand(false)
833
+ ] })
834
+ ));
835
+ }
836
+ if (d.kind === "decision") {
837
+ return cardShell(
838
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
839
+ body(
840
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
841
+ /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-start gap-2.5", children: [
842
+ /* @__PURE__ */ jsx3(
843
+ NodeMark,
844
+ {
845
+ kind: d.kind,
846
+ provider: d.provider,
847
+ model: markModel,
848
+ accent,
849
+ tile: 34
850
+ }
851
+ ),
852
+ /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
853
+ /* @__PURE__ */ jsx3(
854
+ "div",
855
+ {
856
+ className: "truncate font-semibold text-[13px] text-foreground leading-tight",
857
+ title: d.title,
858
+ children: d.title
859
+ }
860
+ ),
861
+ /* @__PURE__ */ jsxs3("div", { className: "mt-0.5 flex items-center gap-2", children: [
862
+ subtitle && /* @__PURE__ */ jsx3(
863
+ "span",
864
+ {
865
+ className: "truncate text-[11px] text-muted-foreground leading-tight",
866
+ title: subtitle,
867
+ children: subtitle
868
+ }
869
+ ),
870
+ runStatus && /* @__PURE__ */ jsx3("span", { className: "ml-auto shrink-0", children: /* @__PURE__ */ jsx3(StatusPill, { status: runStatus }) })
871
+ ] })
872
+ ] })
873
+ ] }),
874
+ runOutput && runStatus === "failed" ? /* @__PURE__ */ jsx3("div", { className: "mt-2 min-h-0 shrink-0", children: /* @__PURE__ */ jsx3(
875
+ NodeOutputBody,
689
876
  {
690
- kind: d.kind,
691
- provider: d.provider,
692
- model: markModel,
693
- accent,
694
- tile: 34
877
+ shape: runOutput.shape,
878
+ tone: runOutput.tone,
879
+ rows: 2
695
880
  }
696
- ),
697
- /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
881
+ ) }) : descriptionBand(true)
882
+ ] })
883
+ ),
884
+ footer
885
+ ] })
886
+ );
887
+ }
888
+ if (isStructuralKind(d.kind)) {
889
+ return cardShell(
890
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
891
+ body(
892
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
893
+ identityRow,
894
+ /* @__PURE__ */ jsx3("div", { className: "mt-1.5 min-h-0 shrink-0", children: runOutput && /* @__PURE__ */ jsx3(
895
+ NodeOutputBody,
896
+ {
897
+ shape: runOutput.shape,
898
+ tone: runOutput.tone,
899
+ rows: STRUCTURAL_OUTPUT_ROWS
900
+ }
901
+ ) })
902
+ ] }),
903
+ // Centered, because the failure slot is empty on every run that does
904
+ // NOT fail — which is most of them. Reserved space it cannot give back
905
+ // (the layout is fixed before the run) reads as a gap under a
906
+ // top-aligned row, and as breathing room around a centered one.
907
+ "justify-center"
908
+ ),
909
+ footer
910
+ ] })
911
+ );
912
+ }
913
+ if (isAgent) {
914
+ const strip = [d.title, subtitle].filter(Boolean).join(" \xB7 ");
915
+ return cardShell(
916
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
917
+ body(
918
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
919
+ /* @__PURE__ */ jsxs3("div", { className: "flex shrink-0 items-center gap-1.5", children: [
698
920
  /* @__PURE__ */ jsx3(
699
- "div",
921
+ NodeMark,
700
922
  {
701
- className: "truncate font-semibold text-[13px] text-foreground leading-tight",
702
- title: d.title,
703
- children: d.title
923
+ kind: d.kind,
924
+ provider: d.provider,
925
+ model: markModel,
926
+ accent,
927
+ tile: 18
704
928
  }
705
929
  ),
706
- subtitle && /* @__PURE__ */ jsx3(
707
- "div",
930
+ /* @__PURE__ */ jsx3(
931
+ "span",
708
932
  {
709
- className: "truncate text-[11px] text-muted-foreground leading-tight",
710
- title: subtitle,
711
- children: subtitle
933
+ className: "truncate font-medium text-[11px] text-muted-foreground",
934
+ title: strip,
935
+ children: strip
712
936
  }
713
- )
937
+ ),
938
+ /* @__PURE__ */ jsx3("span", { className: "flex-1" }),
939
+ runStatus && /* @__PURE__ */ jsx3(StatusPill, { status: runStatus })
714
940
  ] }),
715
- state ? /* @__PURE__ */ jsx3(StatusPill, { status: state.status }) : d.badge && /* @__PURE__ */ jsx3("span", { className: "shrink-0 rounded-full border border-border bg-surface-container-high px-2 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge })
716
- ] }),
717
- d.description && /* @__PURE__ */ jsx3(
718
- "p",
719
- {
720
- className: "mt-2 line-clamp-2 shrink-0 text-[11px] text-muted-foreground leading-snug",
721
- title: d.description,
722
- children: d.description
723
- }
724
- ),
725
- runBands && (cost || tokens) && /* @__PURE__ */ jsx3("div", { className: "mt-2 shrink-0 truncate text-[11px] text-muted-foreground tabular-nums", children: [cost, tokens].filter(Boolean).join(" \xB7 ") }),
726
- runBands && runOutput && /* @__PURE__ */ jsxs2("div", { className: "mt-2 min-h-0 shrink-0 rounded-lg border border-border bg-surface-container-high/60 px-2 py-1.5", children: [
727
- /* @__PURE__ */ jsx3("div", { className: "mb-1 font-semibold text-[9px] text-muted-foreground uppercase tracking-[0.09em]", children: runOutput.label }),
728
- /* @__PURE__ */ jsx3(
941
+ /* @__PURE__ */ jsx3("div", { className: "mt-2 min-h-0 flex-1 text-foreground leading-snug", children: runOutput ? /* @__PURE__ */ jsx3(
729
942
  NodeOutputBody,
730
943
  {
731
944
  shape: runOutput.shape,
732
945
  tone: runOutput.tone,
733
- rows: OUTPUT_ROWS
946
+ rows: AGENT_BODY_ROWS
734
947
  }
735
- )
736
- ] }),
737
- runBands && state && !runOutput && emptySlotLabel(state.status) && /* @__PURE__ */ jsx3("div", { className: "mt-2 flex flex-1 items-center text-[11px] text-muted-foreground italic", children: emptySlotLabel(state.status) })
738
- ] }),
739
- runBands && state && /* @__PURE__ */ jsx3(
740
- StatusFooter,
741
- {
742
- status: state.status,
743
- rounds: isAgent ? state.rounds : void 0,
744
- elapsed: duration
745
- }
746
- )
747
- ]
748
- }
948
+ ) : runStatus && runStatus !== "succeeded" && runStatus !== "failed" && d.description ? (
949
+ // What it is ABOUT to do, in the muted token — the answer, when it
950
+ // arrives, is the thing set in the foreground.
951
+ //
952
+ // Only while the node is still going to produce one. A FINISHED
953
+ // node that produced nothing must say so: showing its prompt
954
+ // there reads as the result it never returned.
955
+ /* @__PURE__ */ jsx3(
956
+ "p",
957
+ {
958
+ className: "line-clamp-5 text-[11px] text-muted-foreground leading-snug",
959
+ title: d.description,
960
+ children: d.description
961
+ }
962
+ )
963
+ ) : runStatus && /* @__PURE__ */ jsx3("span", { className: "text-muted-foreground italic", children: emptySlotLabel(runStatus) }) })
964
+ ] })
965
+ ),
966
+ footer
967
+ ] })
968
+ );
969
+ }
970
+ return cardShell(
971
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
972
+ body(
973
+ /* @__PURE__ */ jsxs3(Fragment, { children: [
974
+ identityRow,
975
+ outputWell(ACTION_OUTPUT_ROWS)
976
+ ] })
977
+ ),
978
+ footer
979
+ ] })
749
980
  );
750
981
  }
751
982
  var EDGE_CHIP_CLASS = "rounded-full border border-border bg-card/90 px-1.5 py-[1px] text-[10px] leading-tight backdrop-blur";
@@ -772,9 +1003,9 @@ function WfEdgeRenderer({
772
1003
  const backEdge = data?.backEdge === true;
773
1004
  const whenLabel = data?.whenLabel;
774
1005
  const visits = data?.maxNodeVisits;
775
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
1006
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
776
1007
  /* @__PURE__ */ jsx3(BaseEdge, { id, path, markerEnd, style }),
777
- /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs2(
1008
+ /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs3(
778
1009
  "div",
779
1010
  {
780
1011
  className: "nodrag nopan absolute flex flex-col items-center gap-0.5",
@@ -837,8 +1068,9 @@ function WorkflowGraph({
837
1068
  yaml,
838
1069
  variant = "full",
839
1070
  direction = "LR",
840
- defaultCompact = true,
1071
+ defaultCompact,
841
1072
  className,
1073
+ wrap = false,
842
1074
  nodeState,
843
1075
  edges: declaredEdges,
844
1076
  maxNodeVisits,
@@ -851,22 +1083,27 @@ function WorkflowGraph({
851
1083
  const editable = onEdgeConnect !== void 0;
852
1084
  const colorMode = useColorMode();
853
1085
  const isPreview = variant === "preview";
854
- const [userCompact, setUserCompact] = useState(defaultCompact);
1086
+ const [userCompact, setUserCompact] = useState(
1087
+ defaultCompact ?? nodeState === void 0
1088
+ );
855
1089
  const compact = isPreview || userCompact;
856
1090
  const [displayCompact, setDisplayCompact] = useState(compact);
857
1091
  const rfRef = useRef(
858
1092
  null
859
1093
  );
860
1094
  const wrapperRef = useRef(null);
1095
+ const framedRef = useRef(false);
1096
+ const [framingCue, setFramingCue] = useState(0);
861
1097
  const hasRunOverlay = nodeState !== void 0;
862
1098
  const structural = useMemo(
863
1099
  () => buildFlowGraph(yaml, {
864
1100
  nodeState: hasRunOverlay ? {} : void 0,
865
1101
  direction,
866
1102
  compact,
1103
+ wrap,
867
1104
  ...declaredEdges ? { edges: declaredEdges } : {}
868
1105
  }),
869
- [yaml, hasRunOverlay, direction, compact, declaredEdges]
1106
+ [yaml, hasRunOverlay, direction, compact, wrap, declaredEdges]
870
1107
  );
871
1108
  const styledEdges = useMemo(() => {
872
1109
  const styled = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
@@ -910,14 +1147,13 @@ function WorkflowGraph({
910
1147
  setDisplayCompact(compact);
911
1148
  return;
912
1149
  }
913
- const endViewport = getViewportForBounds(
914
- getNodesBounds(structural.nodes),
1150
+ const endViewport = framingViewport(
1151
+ layoutBounds(structural.nodes),
915
1152
  frame.width,
916
1153
  frame.height,
917
- FIT_VIEW.minZoom,
918
- fitZoomCeiling(compact),
919
- FIT_VIEW.padding
1154
+ compact
920
1155
  );
1156
+ framedRef.current = true;
921
1157
  const prev = nodesRef.current;
922
1158
  const prevById = new Map(prev.map((n) => [n.id, n]));
923
1159
  const sameNodeSet = prev.length === structural.nodes.length && structural.nodes.every((n) => prevById.has(n.id));
@@ -970,6 +1206,34 @@ function WorkflowGraph({
970
1206
  clearTimeout(flipTimer);
971
1207
  };
972
1208
  }, [structural, compact, setNodes]);
1209
+ useEffect(() => {
1210
+ if (framedRef.current) return;
1211
+ const inst = rfRef.current;
1212
+ const frame = wrapperRef.current?.getBoundingClientRect();
1213
+ if (!inst || !frame || frame.width === 0 || frame.height === 0) return;
1214
+ inst.setViewport(
1215
+ framingViewport(
1216
+ layoutBounds(structural.nodes),
1217
+ frame.width,
1218
+ frame.height,
1219
+ compact
1220
+ )
1221
+ );
1222
+ framedRef.current = true;
1223
+ }, [framingCue, structural, compact]);
1224
+ useEffect(() => {
1225
+ const el = wrapperRef.current;
1226
+ if (!el || typeof ResizeObserver === "undefined") return;
1227
+ const observer = new ResizeObserver(() => {
1228
+ if (framedRef.current) {
1229
+ observer.disconnect();
1230
+ return;
1231
+ }
1232
+ setFramingCue((cue) => cue + 1);
1233
+ });
1234
+ observer.observe(el);
1235
+ return () => observer.disconnect();
1236
+ }, []);
973
1237
  const handleNodeClick = useCallback(
974
1238
  (_event, node) => {
975
1239
  onNodeClick?.(node.id, node.data);
@@ -999,7 +1263,7 @@ function WorkflowGraph({
999
1263
  }
1000
1264
  );
1001
1265
  }
1002
- return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs2(
1266
+ return /* @__PURE__ */ jsx3(DirectionContext.Provider, { value: direction, children: /* @__PURE__ */ jsx3(DensityContext.Provider, { value: displayCompact, children: /* @__PURE__ */ jsx3(RunModeContext.Provider, { value: hasRunOverlay, children: /* @__PURE__ */ jsx3(SelectedNodeContext.Provider, { value: selectedNodeId, children: /* @__PURE__ */ jsx3(ConnectableContext.Provider, { value: editable, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
1003
1267
  ReactFlow,
1004
1268
  {
1005
1269
  nodes,
@@ -1011,11 +1275,16 @@ function WorkflowGraph({
1011
1275
  colorMode,
1012
1276
  onInit: (inst) => {
1013
1277
  rfRef.current = inst;
1278
+ if (!framedRef.current) setFramingCue((cue) => cue + 1);
1014
1279
  },
1015
1280
  onNodeClick: onNodeClick ? handleNodeClick : void 0,
1016
1281
  onKeyDown: onNodeClick ? handleNodeKeyDown : void 0,
1017
1282
  fitView: true,
1018
- fitViewOptions: { ...FIT_VIEW, maxZoom: fitZoomCeiling(compact) },
1283
+ fitViewOptions: {
1284
+ ...FIT_VIEW,
1285
+ minZoom: fitZoomFloor(compact),
1286
+ maxZoom: fitZoomCeiling(compact)
1287
+ },
1019
1288
  proOptions: { hideAttribution: true },
1020
1289
  nodesDraggable: !isPreview,
1021
1290
  nodesConnectable: editable,
@@ -1049,9 +1318,9 @@ function WorkflowGraph({
1049
1318
  maxZoom: 1.5,
1050
1319
  children: [
1051
1320
  /* @__PURE__ */ jsx3(Background, { gap: 18 }),
1052
- !isPreview && /* @__PURE__ */ jsxs2(Fragment, { children: [
1321
+ !isPreview && /* @__PURE__ */ jsxs3(Fragment, { children: [
1053
1322
  /* @__PURE__ */ jsx3(Controls, { showInteractive: false, position: "bottom-right" }),
1054
- /* @__PURE__ */ jsx3(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs2(
1323
+ /* @__PURE__ */ jsx3(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs3(
1055
1324
  "button",
1056
1325
  {
1057
1326
  type: "button",
@@ -1068,17 +1337,22 @@ function WorkflowGraph({
1068
1337
  ] })
1069
1338
  ]
1070
1339
  }
1071
- ) }) }) }) }) });
1340
+ ) }) }) }) }) }) });
1072
1341
  }
1073
1342
  export {
1074
1343
  ConnectableContext,
1075
1344
  DensityContext,
1076
1345
  DirectionContext,
1346
+ FIT_VIEW,
1077
1347
  LAYOUT_TRANSITION_MS,
1348
+ RunModeContext,
1078
1349
  SelectedNodeContext,
1079
1350
  WorkflowGraph,
1080
1351
  WorkflowNode,
1081
1352
  buildStyledEdges,
1082
1353
  fitZoomCeiling,
1083
- isEditableEdge
1354
+ fitZoomFloor,
1355
+ framingViewport,
1356
+ isEditableEdge,
1357
+ layoutBounds
1084
1358
  };