@tangle-network/sandbox-ui 0.94.3 → 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
  };
@@ -237,9 +237,29 @@ var EDGE_LABEL_LANE = 180;
237
237
  var CARD_CHROME = 20;
238
238
  var HEADER_ROW = 34;
239
239
  var DESCRIPTION_ROW = 40;
240
- var META_ROW = 27;
241
- var OUTPUT_ROWS = 3;
242
- var OUTPUT_ROW = 89;
240
+ var AGENT_BODY_ROWS = 5;
241
+ var ACTION_OUTPUT_ROWS = 3;
242
+ var STRUCTURAL_OUTPUT_ROWS = 1;
243
+ var WELL_PROSE_LINE = 15.125;
244
+ var WELL_JSON_LINE = 14.4375;
245
+ var WELL_JSON_GAP = 2;
246
+ var WELL_CHROME = 39.5;
247
+ function wellRow(rows) {
248
+ const body = Math.max(
249
+ rows * WELL_PROSE_LINE,
250
+ rows * WELL_JSON_LINE + (rows - 1) * WELL_JSON_GAP
251
+ );
252
+ return Math.ceil(WELL_CHROME + body) + 3;
253
+ }
254
+ var SLIM_HEADER_ROW = 22;
255
+ var BODY_LINE = 15.125;
256
+ var AGENT_BODY_ROW = Math.ceil(8 + AGENT_BODY_ROWS * BODY_LINE);
257
+ var CHIP_SLOT_ROW = 22;
258
+ var DECISION_HEADER_ROW = 45;
259
+ var STRUCTURAL_KINDS = /* @__PURE__ */ new Set(["parallel", "foreach"]);
260
+ function isStructuralKind(kind) {
261
+ return kind !== void 0 && STRUCTURAL_KINDS.has(kind);
262
+ }
243
263
  var OUTPUT_PREVIEW_CHARS = 240;
244
264
  var FOOTER_ROW = 28;
245
265
  var COMPACT_TILE = 76;
@@ -258,10 +278,19 @@ var COMPACT_NODE_SIZE_TB = {
258
278
  height: COMPACT_TILE
259
279
  };
260
280
  function nodeHeight(data, withRunState) {
261
- let h = CARD_CHROME + HEADER_ROW;
262
- if (data.description) h += DESCRIPTION_ROW;
263
- if (withRunState) h += META_ROW + OUTPUT_ROW + FOOTER_ROW;
264
- return h;
281
+ if (!withRunState || data.tone === "trigger") {
282
+ return CARD_CHROME + HEADER_ROW + (data.description ? DESCRIPTION_ROW : 0);
283
+ }
284
+ if (data.kind === "decision") {
285
+ return CARD_CHROME + DECISION_HEADER_ROW + DESCRIPTION_ROW + FOOTER_ROW;
286
+ }
287
+ if (isStructuralKind(data.kind)) {
288
+ return CARD_CHROME + HEADER_ROW + CHIP_SLOT_ROW + FOOTER_ROW;
289
+ }
290
+ if (data.kind === "agent.run") {
291
+ return CARD_CHROME + SLIM_HEADER_ROW + AGENT_BODY_ROW + FOOTER_ROW;
292
+ }
293
+ return CARD_CHROME + HEADER_ROW + wellRow(ACTION_OUTPUT_ROWS) + FOOTER_ROW;
265
294
  }
266
295
  function geometry(compact, runMode, direction) {
267
296
  if (compact) {
@@ -552,6 +581,75 @@ function describeTrigger(on) {
552
581
  asRecord(kind ? rec[kind] : void 0)
553
582
  );
554
583
  }
584
+ var WRAP_TARGET_ASPECT = 742 / 480;
585
+ var WRAP_ASPECT_TRIGGER = 4.2;
586
+ var WRAP_ROW_GAP = 28;
587
+ function foldColumns(count, cellW, cellH, colSep, rowSep) {
588
+ let best = count;
589
+ let bestError = Number.POSITIVE_INFINITY;
590
+ for (let cols = 1; cols <= count; cols++) {
591
+ const rows = Math.ceil(count / cols);
592
+ const width = cols * cellW + (cols - 1) * colSep;
593
+ const height = rows * cellH + (rows - 1) * rowSep;
594
+ const error = Math.abs(Math.log(width / height / WRAP_TARGET_ASPECT));
595
+ if (error < bestError) {
596
+ bestError = error;
597
+ best = cols;
598
+ }
599
+ }
600
+ return best;
601
+ }
602
+ function layoutSerpentine(nodes, edges, colSep, laneSep) {
603
+ const rowSep = Math.max(laneSep, WRAP_ROW_GAP);
604
+ const byRank = /* @__PURE__ */ new Map();
605
+ for (const n of nodes) {
606
+ const arr = byRank.get(n.rank);
607
+ if (arr) arr.push(n);
608
+ else byRank.set(n.rank, [n]);
609
+ }
610
+ const ranks = [...byRank.keys()].sort((a, b) => a - b);
611
+ if (ranks.some((r) => byRank.get(r).length !== 1)) return null;
612
+ const rankOf = new Map(nodes.map((n) => [n.id, n.rank]));
613
+ const joinsNextLayer = (e) => {
614
+ const from = rankOf.get(e.source);
615
+ const to = rankOf.get(e.target);
616
+ return from !== void 0 && to !== void 0 && to === from + 1;
617
+ };
618
+ if (!edges.every(joinsNextLayer)) return null;
619
+ const ordered = ranks.map((r) => byRank.get(r)[0]);
620
+ const count = ordered.length;
621
+ const cellW = Math.max(...ordered.map((n) => n.width));
622
+ const cellH = Math.max(...ordered.map((n) => n.height));
623
+ const lineAspect = (count * cellW + (count - 1) * colSep) / cellH;
624
+ if (lineAspect <= WRAP_TARGET_ASPECT * WRAP_ASPECT_TRIGGER) return null;
625
+ const cols = foldColumns(count, cellW, cellH, colSep, rowSep);
626
+ if (cols >= count) return null;
627
+ return ordered.map((node, i) => {
628
+ const row = Math.floor(i / cols);
629
+ const indexInRow = i % cols;
630
+ const leftToRight = row % 2 === 0;
631
+ const col = leftToRight ? indexInRow : cols - 1 - indexInRow;
632
+ return {
633
+ id: node.id,
634
+ position: {
635
+ x: col * (cellW + colSep) + (cellW - node.width) / 2,
636
+ y: row * (cellH + rowSep) + (cellH - node.height) / 2
637
+ },
638
+ width: node.width,
639
+ height: node.height,
640
+ data: node.data,
641
+ // Every node leaves the way its row travels and is entered from behind —
642
+ // including the one that turns the corner. Mirroring puts the last cell
643
+ // of a row directly above the first cell of the next, so a turn is
644
+ // source-right into target-right (or left into left): the renderer draws
645
+ // it as a bracket in the margin beside the column. Routing it through the
646
+ // BOTTOM instead would have to start below the name that sits under the
647
+ // tile, leaving the arrow floating clear of the node it comes from.
648
+ targetSide: leftToRight ? "left" : "right",
649
+ sourceSide: leftToRight ? "right" : "left"
650
+ };
651
+ });
652
+ }
555
653
  function layoutLayers(nodes, direction, rankSep, crossSep) {
556
654
  const isLR = direction === "LR";
557
655
  const mainSize = (n) => isLR ? n.width : n.height;
@@ -778,8 +876,9 @@ function buildWorkflowGraph(yaml, options) {
778
876
  (e) => e.whenLabel !== void 0 || e.backEdge === true
779
877
  );
780
878
  const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : geo.rankSep;
879
+ const folded = options?.wrap && direction === "LR" && !labelled ? layoutSerpentine(logical, edges, rankSep, geo.crossSep) : null;
781
880
  return {
782
- nodes: layoutLayers(logical, direction, rankSep, geo.crossSep),
881
+ nodes: folded ?? layoutLayers(logical, direction, rankSep, geo.crossSep),
783
882
  edges,
784
883
  error: null
785
884
  };
@@ -797,7 +896,10 @@ export {
797
896
  triggerNodeIndex,
798
897
  actionNodeId,
799
898
  branchNodeId,
800
- OUTPUT_ROWS,
899
+ AGENT_BODY_ROWS,
900
+ ACTION_OUTPUT_ROWS,
901
+ STRUCTURAL_OUTPUT_ROWS,
902
+ isStructuralKind,
801
903
  OUTPUT_PREVIEW_CHARS,
802
904
  COMPACT_TILE,
803
905
  COMPACT_NODE_SIZE,
package/dist/globals.css CHANGED
@@ -643,6 +643,24 @@
643
643
  -webkit-box-orient: vertical;
644
644
  -webkit-line-clamp: 3;
645
645
  }
646
+ .line-clamp-4 {
647
+ overflow: hidden;
648
+ display: -webkit-box;
649
+ -webkit-box-orient: vertical;
650
+ -webkit-line-clamp: 4;
651
+ }
652
+ .line-clamp-5 {
653
+ overflow: hidden;
654
+ display: -webkit-box;
655
+ -webkit-box-orient: vertical;
656
+ -webkit-line-clamp: 5;
657
+ }
658
+ .line-clamp-6 {
659
+ overflow: hidden;
660
+ display: -webkit-box;
661
+ -webkit-box-orient: vertical;
662
+ -webkit-line-clamp: 6;
663
+ }
646
664
  .block {
647
665
  display: block;
648
666
  }
@@ -805,6 +823,9 @@
805
823
  .h-\[28rem\] {
806
824
  height: 28rem;
807
825
  }
826
+ .h-\[30rem\] {
827
+ height: 30rem;
828
+ }
808
829
  .h-\[32rem\] {
809
830
  height: 32rem;
810
831
  }
package/dist/styles.css CHANGED
@@ -643,6 +643,24 @@
643
643
  -webkit-box-orient: vertical;
644
644
  -webkit-line-clamp: 3;
645
645
  }
646
+ .line-clamp-4 {
647
+ overflow: hidden;
648
+ display: -webkit-box;
649
+ -webkit-box-orient: vertical;
650
+ -webkit-line-clamp: 4;
651
+ }
652
+ .line-clamp-5 {
653
+ overflow: hidden;
654
+ display: -webkit-box;
655
+ -webkit-box-orient: vertical;
656
+ -webkit-line-clamp: 5;
657
+ }
658
+ .line-clamp-6 {
659
+ overflow: hidden;
660
+ display: -webkit-box;
661
+ -webkit-box-orient: vertical;
662
+ -webkit-line-clamp: 6;
663
+ }
646
664
  .block {
647
665
  display: block;
648
666
  }
@@ -805,6 +823,9 @@
805
823
  .h-\[28rem\] {
806
824
  height: 28rem;
807
825
  }
826
+ .h-\[30rem\] {
827
+ height: 30rem;
828
+ }
808
829
  .h-\[32rem\] {
809
830
  height: 32rem;
810
831
  }
@@ -106,6 +106,9 @@ interface WfNodeData extends Record<string, unknown> {
106
106
  /** Live run state, merged in by the host at render time (never from YAML). */
107
107
  state?: WfNodeState;
108
108
  }
109
+ /** Which edge of a node box an edge attaches to. Named in plain compass terms
110
+ * rather than React Flow's `Position` so the layouter stays free of it. */
111
+ type WfSide = "left" | "right" | "top" | "bottom";
109
112
  interface WfNode {
110
113
  id: string;
111
114
  position: {
@@ -118,6 +121,15 @@ interface WfNode {
118
121
  width: number;
119
122
  height: number;
120
123
  data: WfNodeData;
124
+ /**
125
+ * Where this node's edges attach, when the LAYOUT decides rather than the
126
+ * flow direction. A straight layer flow has one answer for every node (edges
127
+ * enter the leading edge and leave the trailing one, per `direction`), so it
128
+ * sets neither and the renderer derives both. A folded layout does not — a row
129
+ * that runs right-to-left mirrors them. Absent ⇒ derive from `direction`.
130
+ */
131
+ sourceSide?: WfSide;
132
+ targetSide?: WfSide;
121
133
  }
122
134
  /**
123
135
  * Node ids are PUBLIC contract, not an internal detail. A host keys its live
@@ -217,6 +229,13 @@ interface BuildWorkflowGraphOptions {
217
229
  /** Collapse every node to the fixed icon-tile size, and pitch the layers for
218
230
  * it. Defaults to `false` (the full, expanded card). */
219
231
  compact?: boolean;
232
+ /**
233
+ * Fold a long single-file pipeline into rows instead of one unbounded line,
234
+ * so it occupies both axes of the panel rather than running off one edge.
235
+ * Only a straight chain folds — a graph with any fan-out keeps its layered
236
+ * flow, whose branches need the cross axis. "LR" only. Defaults to `false`.
237
+ */
238
+ wrap?: boolean;
220
239
  /**
221
240
  * The graph's DECLARED topology. Omit — the default — and edges are inferred
222
241
  * from `do`-list order: a linear spine, which is exactly right for a workflow
@@ -250,14 +269,25 @@ interface WorkflowGraphProps {
250
269
  /** Flow direction: "LR" (default) reads left-to-right — best for the wide/short
251
270
  * run-detail panel; "TB" is a vertical column. */
252
271
  direction?: WfDirection;
253
- /** Start collapsed (compact icon tiles) rather than expanded. Defaults to
254
- * `true`: a graph is read structure-first, and the tiles are what make the
255
- * shape of the run legible at a glance — the per-node detail is one density
256
- * toggle (or one node click) away. The full variant exposes the toggle; the
257
- * preview variant is always compact. */
272
+ /**
273
+ * Start collapsed (compact icon tiles) rather than expanded.
274
+ *
275
+ * Defaults to whether the graph has a RUN: a definition is read
276
+ * structure-first, so it opens as tiles, while a run is read result-first and
277
+ * opens as cards — the answers are the reason the reader opened it. Passing
278
+ * the prop pins the density either way; the full variant still exposes the
279
+ * toggle, and the preview variant is always compact.
280
+ */
258
281
  defaultCompact?: boolean;
259
282
  /** Sizing for the wrapper; the caller controls height. */
260
283
  className?: string;
284
+ /**
285
+ * Fold a long single-file pipeline into rows instead of one unbounded line,
286
+ * so it uses both axes of the panel rather than running off one edge. Only a
287
+ * straight chain folds; anything with fan-out keeps its layered flow. "LR"
288
+ * only. Defaults to `false`.
289
+ */
290
+ wrap?: boolean;
261
291
  /**
262
292
  * Live per-node run state, keyed by graph node id (`trigger`, `a0`, `a0-b1`).
263
293
  * Absent ⇒ the static definition view (the proposal-card preview passes
package/dist/workflows.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  buildWorkflowGraph,
6
6
  triggerNodeId,
7
7
  triggerNodeIndex
8
- } from "./chunk-7QKAVIZX.js";
8
+ } from "./chunk-O7MHUR4U.js";
9
9
  import {
10
10
  ModelBrandStack,
11
11
  modelBrandFor
@@ -36,7 +36,7 @@ function retryImport(factory, retries = 2, delayMs = 350) {
36
36
  function lazyWorkflowGraph() {
37
37
  return lazy(
38
38
  () => retryImport(
39
- () => import("./WorkflowGraph-4TQFLY3E.js").then((m) => ({ default: m.WorkflowGraph }))
39
+ () => import("./WorkflowGraph-MPNVC5FE.js").then((m) => ({ default: m.WorkflowGraph }))
40
40
  )
41
41
  );
42
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/sandbox-ui",
3
- "version": "0.94.3",
3
+ "version": "0.95.0",
4
4
  "description": "Unified UI component library for Tangle Sandbox — primitives, chat, dashboard, terminal, editor, and workspace components",
5
5
  "repository": {
6
6
  "type": "git",