@tangle-network/sandbox-ui 0.109.0 → 0.111.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.
@@ -16,8 +16,10 @@ import {
16
16
  isStructuralKind,
17
17
  providerLabel,
18
18
  shortModel,
19
- triggerNodeIndex
20
- } from "./chunk-LHUBJPZR.js";
19
+ triggerNodeIndex,
20
+ wfEdgeId,
21
+ worstSeverity
22
+ } from "./chunk-AI25G5MX.js";
21
23
  import {
22
24
  ModelBrandStack,
23
25
  modelBrandFor
@@ -42,7 +44,7 @@ import {
42
44
  useNodesState
43
45
  } from "@xyflow/react";
44
46
  import "@xyflow/react/dist/style.css";
45
- import { Maximize2, Minimize2 } from "lucide-react";
47
+ import { Maximize2, Minimize2, Plus, X } from "lucide-react";
46
48
  import {
47
49
  createContext,
48
50
  useCallback,
@@ -68,9 +70,17 @@ function isNodeStateMap(arg) {
68
70
  (v) => v !== null && typeof v === "object" && "status" in v
69
71
  );
70
72
  }
73
+ var FLOW_GRAPH_OPTION_KEYS = [
74
+ "nodeState",
75
+ "direction",
76
+ "compact",
77
+ "wrap",
78
+ "edges",
79
+ "reserveEdgeInsert"
80
+ ];
71
81
  function normalizeFlowGraphOptions(arg) {
72
82
  if (!arg) return {};
73
- if ("nodeState" in arg || "direction" in arg || "compact" in arg || "wrap" in arg || "edges" in arg) {
83
+ if (FLOW_GRAPH_OPTION_KEYS.some((key) => key in arg)) {
74
84
  return arg;
75
85
  }
76
86
  return isNodeStateMap(arg) ? { nodeState: arg } : {};
@@ -85,7 +95,8 @@ function buildFlowGraph(yaml, optionsOrNodeState) {
85
95
  direction,
86
96
  compact,
87
97
  ...options.wrap ? { wrap: true } : {},
88
- ...options.edges ? { edges: options.edges } : {}
98
+ ...options.edges ? { edges: options.edges } : {},
99
+ ...options.reserveEdgeInsert ? { reserveEdgeInsert: true } : {}
89
100
  });
90
101
  const isLR = direction === "LR";
91
102
  const sourcePosition = isLR ? Position.Right : Position.Bottom;
@@ -149,6 +160,69 @@ function mergeRunState(prev, baseDataById, nodeState) {
149
160
  return sameRunState(n.data.state, nextData.state) ? n : { ...n, data: nextData };
150
161
  });
151
162
  }
163
+ function indexProblems(problems) {
164
+ const byNode = /* @__PURE__ */ new Map();
165
+ const byEdge = /* @__PURE__ */ new Map();
166
+ const named = (value) => typeof value === "string" && value !== "";
167
+ for (const problem of problems ?? []) {
168
+ const into = problem.anchor === "node" ? named(problem.node) ? byNode : null : problem.anchor === "edge" && named(problem.from) && named(problem.to) ? byEdge : null;
169
+ if (!into) continue;
170
+ const key = problem.anchor === "node" ? problem.node : wfEdgeId(problem.from, problem.to);
171
+ const at = into.get(key);
172
+ if (at) at.push(problem);
173
+ else into.set(key, [problem]);
174
+ }
175
+ return { byNode, byEdge };
176
+ }
177
+ var INSERT_CLEARANCE = 8;
178
+ var CLUSTER_HALF_SIZE = {
179
+ /** The insert control alone: a 20-unit square. */
180
+ control: { width: 10, height: 10 },
181
+ /**
182
+ * A problem chip — and, since the two stack in one column, the chip WITH the
183
+ * control under it, which is the taller of the two and so the one the bound
184
+ * has to cover. Measured on the rendered page at 160 x 38.5 flow units (the
185
+ * chip's `max-w-40` cap, over chip + gap + control), and rounded outward: a
186
+ * bound that understates is a bound that lets the cluster lap over a card.
187
+ */
188
+ chip: { width: 80, height: 20 }
189
+ };
190
+ var INSERT_MAX_CARDS_CROSSED = 2;
191
+ function clearOfNodeBoxes(point, boxes, direction, half) {
192
+ const isLR = direction === "LR";
193
+ const base = isLR ? point.y : point.x;
194
+ const main = isLR ? point.x : point.y;
195
+ const padMain = isLR ? half.width : half.height;
196
+ const padCross = isLR ? half.height : half.width;
197
+ const column = boxes.filter((b) => {
198
+ const lo = (isLR ? b.x : b.y) - padMain;
199
+ const size = (isLR ? b.width : b.height) + padMain * 2;
200
+ return main >= lo && main <= lo + size;
201
+ });
202
+ if (column.length === 0) return 0;
203
+ const walk = (sign) => {
204
+ let at = base;
205
+ for (let crossed = 0; crossed <= INSERT_MAX_CARDS_CROSSED; crossed += 1) {
206
+ const hit = column.find((b) => {
207
+ const lo2 = (isLR ? b.y : b.x) - padCross;
208
+ const size2 = (isLR ? b.height : b.width) + padCross * 2;
209
+ return at >= lo2 && at <= lo2 + size2;
210
+ });
211
+ if (!hit) return at - base;
212
+ if (crossed === INSERT_MAX_CARDS_CROSSED) return null;
213
+ const lo = isLR ? hit.y : hit.x;
214
+ const size = isLR ? hit.height : hit.width;
215
+ at = sign > 0 ? lo + size + padCross + INSERT_CLEARANCE : lo - padCross - INSERT_CLEARANCE;
216
+ }
217
+ return null;
218
+ };
219
+ const up = walk(-1);
220
+ const down = walk(1);
221
+ if (up === null && down === null) return null;
222
+ if (up === null) return down;
223
+ if (down === null) return up;
224
+ return Math.abs(up) <= Math.abs(down) ? up : down;
225
+ }
152
226
 
153
227
  // src/workflows/node-output.tsx
154
228
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -284,6 +358,7 @@ function NodeOutputBody({
284
358
 
285
359
  // src/workflows/node-ui.tsx
286
360
  import {
361
+ AlertTriangle,
287
362
  Bell,
288
363
  Box,
289
364
  Cable,
@@ -395,6 +470,73 @@ var KIND_ICON = {
395
470
  "script.run": Code,
396
471
  "trace.analyze": ScanSearch
397
472
  };
473
+ var PROBLEM_SURFACE_BY_SEVERITY = {
474
+ error: {
475
+ background: "var(--surface-danger-bg)",
476
+ color: "var(--surface-danger-text)",
477
+ borderColor: "var(--surface-danger-border)"
478
+ },
479
+ warning: {
480
+ background: "var(--surface-warning-bg)",
481
+ color: "var(--surface-warning-text)",
482
+ borderColor: "var(--surface-warning-border)"
483
+ }
484
+ };
485
+ function problemSurface(severity) {
486
+ return PROBLEM_SURFACE_BY_SEVERITY[severity] ?? PROBLEM_SURFACE_BY_SEVERITY.error;
487
+ }
488
+ function problemBorder(severity) {
489
+ const color = problemSurface(severity).color;
490
+ return {
491
+ borderColor: color,
492
+ boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 35%, transparent)`
493
+ };
494
+ }
495
+ var SEVERITY_WORD = {
496
+ error: "Error",
497
+ warning: "Warning"
498
+ };
499
+ function problemSentence(problem) {
500
+ const raw = typeof problem.message === "string" ? problem.message : "";
501
+ const message = raw.split("\n").map((line) => line.trim()).filter(Boolean).join(" ");
502
+ const word = SEVERITY_WORD[problem.severity] ?? SEVERITY_WORD.error;
503
+ return message ? `${word}: ${message}` : word;
504
+ }
505
+ function joinSentences(parts) {
506
+ return parts.reduce(
507
+ (joined, part) => joined ? `${joined}${/[.!?;,]$/.test(joined) ? "" : "."} ${part}` : part,
508
+ ""
509
+ );
510
+ }
511
+ function problemTitle(problems) {
512
+ return problems.map(problemSentence).join("\n");
513
+ }
514
+ function ProblemMessages({ problems }) {
515
+ if (problems.length === 0) return null;
516
+ return /* @__PURE__ */ jsx2("span", { className: "sr-only", children: problems.length === 1 ? problemSentence(problems[0]) : `${problems.length} problems. ${joinSentences(problems.map(problemSentence))}` });
517
+ }
518
+ function ProblemMarker({
519
+ problems,
520
+ className = ""
521
+ }) {
522
+ const severity = worstSeverity(problems) ?? "error";
523
+ return /* @__PURE__ */ jsxs2(
524
+ "span",
525
+ {
526
+ "data-testid": "wf-node-problem",
527
+ "data-severity": severity,
528
+ className: `flex shrink-0 items-center gap-0.5 rounded-full border px-1 py-[1px] font-medium text-[10px] leading-none ${className}`,
529
+ style: problemSurface(severity),
530
+ children: [
531
+ /* @__PURE__ */ jsxs2("span", { "aria-hidden": true, className: "flex items-center gap-0.5", title: problemTitle(problems), children: [
532
+ /* @__PURE__ */ jsx2(AlertTriangle, { size: 9 }),
533
+ problems.length > 1 && /* @__PURE__ */ jsx2("span", { children: problems.length })
534
+ ] }),
535
+ /* @__PURE__ */ jsx2(ProblemMessages, { problems })
536
+ ]
537
+ }
538
+ );
539
+ }
398
540
  function StatusPill({ status }) {
399
541
  return /* @__PURE__ */ jsx2(
400
542
  "span",
@@ -500,7 +642,7 @@ function NodeMark({
500
642
 
501
643
  // src/workflows/WorkflowGraph.tsx
502
644
  import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
503
- var FIT_VIEW = { padding: 0.16 };
645
+ var FIT_VIEW = { padding: "12px" };
504
646
  function fitZoomCeiling(compact) {
505
647
  return compact ? 1.5 : 1;
506
648
  }
@@ -567,6 +709,9 @@ var SELECTION_OUTLINE = {
567
709
  outlineOffset: 2
568
710
  };
569
711
  var ConnectableContext = createContext(false);
712
+ var NodeProblemsContext = createContext(/* @__PURE__ */ new Map());
713
+ var TriggerDeleteContext = createContext(null);
714
+ var EdgeInsertContext = createContext(null);
570
715
  function isEditableEdge(edge) {
571
716
  return edge.data?.kind !== "fork" && triggerNodeIndex(edge.source) === null;
572
717
  }
@@ -609,6 +754,10 @@ function WorkflowNode({
609
754
  const runMode = useContext(RunModeContext);
610
755
  const connectable = useContext(ConnectableContext);
611
756
  const hostSelection = useContext(SelectedNodeContext);
757
+ const problems = useContext(NodeProblemsContext).get(id);
758
+ const problemSeverity = worstSeverity(problems);
759
+ const removeTrigger = useContext(TriggerDeleteContext);
760
+ const isTrigger = triggerNodeIndex(id) !== null;
612
761
  const selected = hostSelection !== void 0 && hostSelection === id;
613
762
  const isLR = direction === "LR";
614
763
  const targetPos = targetPosition ?? (isLR ? Position2.Left : Position2.Top);
@@ -637,6 +786,21 @@ function WorkflowNode({
637
786
  );
638
787
  return shape.kind === "empty" ? null : { shape, tone: source.tone, label: source.label };
639
788
  })();
789
+ const triggerDeleteButton = isTrigger && removeTrigger ? /* @__PURE__ */ jsx3(
790
+ "button",
791
+ {
792
+ type: "button",
793
+ "data-testid": "wf-trigger-delete",
794
+ title: "Remove this trigger",
795
+ "aria-label": "Remove this trigger",
796
+ className: "nodrag nopan -m-1 flex h-6 w-6 shrink-0 items-center justify-center p-1 text-muted-foreground transition hover:text-[var(--surface-danger-text)]",
797
+ onClick: (event) => {
798
+ event.stopPropagation();
799
+ removeTrigger(id);
800
+ },
801
+ children: /* @__PURE__ */ jsx3("span", { className: "flex h-4 w-4 items-center justify-center rounded-full border border-border bg-card shadow-sm", children: /* @__PURE__ */ jsx3(X, { size: 10, "aria-hidden": true }) })
802
+ }
803
+ ) : null;
640
804
  const handleClass = connectable ? HANDLE_CONNECTABLE_CLASS : HANDLE_CLASS;
641
805
  const handles = /* @__PURE__ */ jsxs3(Fragment, { children: [
642
806
  !d.isRoot && /* @__PURE__ */ jsx3(
@@ -668,11 +832,12 @@ function WorkflowNode({
668
832
  /* @__PURE__ */ jsxs3(
669
833
  "span",
670
834
  {
835
+ "data-testid": "wf-node-card",
671
836
  className: "relative flex items-center justify-center rounded-xl border bg-card shadow-sm transition-colors",
672
837
  style: {
673
838
  width: COMPACT_TILE,
674
839
  height: COMPACT_TILE,
675
- ...state ? statusBorder(state.status) : {
840
+ ...state ? statusBorder(state.status) : problemSeverity ? problemBorder(problemSeverity) : {
676
841
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
677
842
  },
678
843
  // The TILE is the compact node (the name beside it is unboxed), so
@@ -690,6 +855,14 @@ function WorkflowNode({
690
855
  tile: Math.round(COMPACT_TILE * 0.62)
691
856
  }
692
857
  ) }),
858
+ problems && problemSeverity && /* @__PURE__ */ jsx3(
859
+ ProblemMarker,
860
+ {
861
+ problems,
862
+ className: "-left-1.5 -top-1.5 absolute"
863
+ }
864
+ ),
865
+ triggerDeleteButton && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute", children: triggerDeleteButton }),
693
866
  d.badge && !state && /* @__PURE__ */ jsx3("span", { className: "-right-1.5 -top-1.5 absolute rounded-full border border-border bg-card px-1.5 py-[1px] font-medium text-[10px] text-muted-foreground", children: d.badge }),
694
867
  state && /* @__PURE__ */ jsx3(
695
868
  "span",
@@ -738,9 +911,10 @@ function WorkflowNode({
738
911
  const cardShell = (children) => /* @__PURE__ */ jsxs3(
739
912
  "div",
740
913
  {
914
+ "data-testid": "wf-node-card",
741
915
  className: "relative flex h-full w-full flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-colors",
742
916
  style: {
743
- ...runStatus ? statusBorder(runStatus) : {
917
+ ...runStatus ? statusBorder(runStatus) : problemSeverity ? problemBorder(problemSeverity) : {
744
918
  borderColor: `color-mix(in srgb, ${accent} 40%, hsl(var(--border)))`
745
919
  },
746
920
  ...selected ? SELECTION_OUTLINE : {}
@@ -780,7 +954,9 @@ function WorkflowNode({
780
954
  }
781
955
  )
782
956
  ] }),
783
- 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 })
957
+ problems && problemSeverity && /* @__PURE__ */ jsx3(ProblemMarker, { problems }),
958
+ 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 }),
959
+ triggerDeleteButton
784
960
  ] });
785
961
  const footer = runStatus ? /* @__PURE__ */ jsx3(
786
962
  StatusFooter,
@@ -981,9 +1157,25 @@ function WorkflowNode({
981
1157
  ] })
982
1158
  );
983
1159
  }
1160
+ function releasePoint(event) {
1161
+ if (!event) return null;
1162
+ const touch = "changedTouches" in event ? event.changedTouches[0] : null;
1163
+ const source = touch ?? ("clientX" in event ? event : null);
1164
+ return source ? { x: source.clientX, y: source.clientY } : null;
1165
+ }
1166
+ var CANVAS_SURFACE_SELECTOR = ".react-flow__pane, .react-flow__background";
1167
+ function releasedOnCanvas(at, frame) {
1168
+ if (!frame) return false;
1169
+ const under = typeof document === "undefined" ? null : document.elementFromPoint(at.x, at.y);
1170
+ if (!under?.matches(CANVAS_SURFACE_SELECTOR)) return false;
1171
+ return frame.contains(under);
1172
+ }
1173
+ var NodeBoxesContext = createContext([]);
984
1174
  var EDGE_CHIP_CLASS = "rounded-full border border-border bg-card/90 px-1.5 py-[1px] text-[10px] leading-tight backdrop-blur";
985
1175
  function WfEdgeRenderer({
986
1176
  id,
1177
+ source,
1178
+ target,
987
1179
  sourceX,
988
1180
  sourceY,
989
1181
  targetX,
@@ -994,6 +1186,7 @@ function WfEdgeRenderer({
994
1186
  markerEnd,
995
1187
  style
996
1188
  }) {
1189
+ const insertStep = useContext(EdgeInsertContext);
997
1190
  const [path, labelX, labelY] = getSmoothStepPath({
998
1191
  sourceX,
999
1192
  sourceY,
@@ -1005,6 +1198,25 @@ function WfEdgeRenderer({
1005
1198
  const backEdge = data?.backEdge === true;
1006
1199
  const whenLabel = data?.whenLabel;
1007
1200
  const visits = data?.maxNodeVisits;
1201
+ const problems = data?.problems;
1202
+ const problemSeverity = worstSeverity(problems);
1203
+ const offersInsert = data?.insertable === true && insertStep !== null;
1204
+ const nodeBoxes = useContext(NodeBoxesContext);
1205
+ const direction = useContext(DirectionContext);
1206
+ const cleared = offersInsert || problemSeverity ? clearOfNodeBoxes(
1207
+ { x: labelX, y: labelY },
1208
+ nodeBoxes,
1209
+ direction,
1210
+ // A chip is far wider than the control, and in "TB" that width is what
1211
+ // has to clear the cards — so the cluster is measured by what it
1212
+ // actually carries rather than by its tallest member.
1213
+ problemSeverity ? CLUSTER_HALF_SIZE.chip : CLUSTER_HALF_SIZE.control
1214
+ ) : 0;
1215
+ const clearance = cleared ?? 0;
1216
+ const stealsPointer = cleared === null;
1217
+ const isLR = direction === "LR";
1218
+ const clusterX = labelX + (isLR ? 0 : clearance);
1219
+ const clusterY = labelY + (isLR ? clearance : 0);
1008
1220
  return /* @__PURE__ */ jsxs3(Fragment, { children: [
1009
1221
  /* @__PURE__ */ jsx3(BaseEdge, { id, path, markerEnd, style }),
1010
1222
  /* @__PURE__ */ jsx3(EdgeLabelRenderer, { children: /* @__PURE__ */ jsxs3(
@@ -1012,9 +1224,23 @@ function WfEdgeRenderer({
1012
1224
  {
1013
1225
  className: "nodrag nopan absolute flex flex-col items-center gap-0.5",
1014
1226
  style: {
1015
- transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`
1227
+ zIndex: 1,
1228
+ transform: `translate(-50%, -50%) translate(${clusterX}px, ${clusterY}px)`
1016
1229
  },
1017
1230
  children: [
1231
+ problems && problemSeverity && /* @__PURE__ */ jsxs3(
1232
+ "span",
1233
+ {
1234
+ "data-testid": "wf-edge-problem",
1235
+ "data-severity": problemSeverity,
1236
+ className: `${EDGE_CHIP_CLASS} max-w-40 truncate ${stealsPointer ? "" : "pointer-events-auto"}`,
1237
+ style: problemSurface(problemSeverity),
1238
+ children: [
1239
+ /* @__PURE__ */ jsx3("span", { "aria-hidden": true, title: problemTitle(problems), children: problems.length === 1 ? problems[0].message : `${problems.length} problems` }),
1240
+ /* @__PURE__ */ jsx3(ProblemMessages, { problems })
1241
+ ]
1242
+ }
1243
+ ),
1018
1244
  whenLabel && /* @__PURE__ */ jsx3(
1019
1245
  "span",
1020
1246
  {
@@ -1030,6 +1256,21 @@ function WfEdgeRenderer({
1030
1256
  className: `${EDGE_CHIP_CLASS} shrink-0 text-foreground`,
1031
1257
  children: visits !== void 0 ? `\u21BA \u2264${visits}` : "\u21BA"
1032
1258
  }
1259
+ ),
1260
+ offersInsert && /* @__PURE__ */ jsx3(
1261
+ "button",
1262
+ {
1263
+ type: "button",
1264
+ "data-testid": "wf-edge-insert",
1265
+ title: "Insert a step here",
1266
+ "aria-label": "Insert a step here",
1267
+ className: `flex h-5 w-5 items-center justify-center rounded-full border border-border bg-card text-muted-foreground shadow-sm backdrop-blur transition hover:border-primary hover:text-primary ${stealsPointer ? "" : "pointer-events-auto"}`,
1268
+ onClick: (event) => {
1269
+ event.stopPropagation();
1270
+ insertStep(source, target);
1271
+ },
1272
+ children: /* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true })
1273
+ }
1033
1274
  )
1034
1275
  ]
1035
1276
  }
@@ -1066,11 +1307,35 @@ function buildStyledEdges(base, nodeState, maxNodeVisits) {
1066
1307
  };
1067
1308
  });
1068
1309
  }
1310
+ function decorateAuthoringEdges(edges, byEdge, insertable, hasRunOverlay) {
1311
+ if (byEdge.size === 0 && !edges.some(insertable)) return edges;
1312
+ return edges.map((e) => {
1313
+ const problems = byEdge.get(e.id);
1314
+ const severity = worstSeverity(problems);
1315
+ const offersInsert = insertable(e);
1316
+ if (!severity && !offersInsert) return e;
1317
+ const stroke = severity && !hasRunOverlay ? problemSurface(severity).color : void 0;
1318
+ return {
1319
+ ...e,
1320
+ type: WF_EDGE_TYPE,
1321
+ data: {
1322
+ ...e.data ?? { kind: "spine" },
1323
+ ...problems ? { problems } : {},
1324
+ ...offersInsert ? { insertable: true } : {}
1325
+ },
1326
+ ...stroke ? {
1327
+ style: { ...e.style, stroke },
1328
+ markerEnd: e.markerEnd && typeof e.markerEnd === "object" ? { ...e.markerEnd, color: stroke } : e.markerEnd
1329
+ } : {}
1330
+ };
1331
+ });
1332
+ }
1069
1333
  function WorkflowGraph({
1070
1334
  yaml,
1071
1335
  variant = "full",
1072
1336
  direction = "LR",
1073
1337
  defaultCompact,
1338
+ onCompactChange,
1074
1339
  className,
1075
1340
  wrap = false,
1076
1341
  nodeState,
@@ -1080,14 +1345,25 @@ function WorkflowGraph({
1080
1345
  onNodeClick,
1081
1346
  onEdgeConnect,
1082
1347
  onEdgeDelete,
1083
- onEdgeClick
1348
+ onEdgeClick,
1349
+ onEdgeInsert,
1350
+ onNodeInsert,
1351
+ onTriggerAdd,
1352
+ onTriggerDelete,
1353
+ problems
1084
1354
  }) {
1085
1355
  const editable = onEdgeConnect !== void 0;
1356
+ const insertOnEdge = editable ? onEdgeInsert : void 0;
1357
+ const insertArmed = insertOnEdge !== void 0;
1358
+ const insertAtNode = editable ? onNodeInsert : void 0;
1359
+ const addTrigger = editable ? onTriggerAdd : void 0;
1360
+ const deleteTrigger = editable ? onTriggerDelete : void 0;
1086
1361
  const colorMode = useColorMode();
1087
1362
  const isPreview = variant === "preview";
1088
1363
  const [userCompact, setUserCompact] = useState(
1089
1364
  defaultCompact ?? nodeState === void 0
1090
1365
  );
1366
+ const nextDensityFrom = useRef(userCompact);
1091
1367
  const compact = isPreview || userCompact;
1092
1368
  const [displayCompact, setDisplayCompact] = useState(compact);
1093
1369
  const rfRef = useRef(
@@ -1103,17 +1379,54 @@ function WorkflowGraph({
1103
1379
  direction,
1104
1380
  compact,
1105
1381
  wrap,
1382
+ // The insert control lives IN the gap between two layers, so the gap is
1383
+ // reserved here — where the layout is decided — rather than measured
1384
+ // afterwards. It is the one editing prop that reaches the layout, which
1385
+ // is why arming it re-lays the graph out.
1386
+ reserveEdgeInsert: insertArmed,
1106
1387
  ...declaredEdges ? { edges: declaredEdges } : {}
1107
1388
  }),
1108
- [yaml, hasRunOverlay, direction, compact, wrap, declaredEdges]
1389
+ [
1390
+ yaml,
1391
+ hasRunOverlay,
1392
+ direction,
1393
+ compact,
1394
+ wrap,
1395
+ insertArmed,
1396
+ declaredEdges
1397
+ ]
1109
1398
  );
1399
+ const nodeBoxes = useMemo(
1400
+ () => structural.nodes.map((node) => ({
1401
+ x: node.position.x,
1402
+ y: node.position.y,
1403
+ width: node.width ?? 0,
1404
+ height: node.height ?? 0
1405
+ })),
1406
+ [structural]
1407
+ );
1408
+ const problemIndex = useMemo(() => indexProblems(problems), [problems]);
1110
1409
  const styledEdges = useMemo(() => {
1111
- const styled = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
1410
+ const run = buildStyledEdges(structural.edges, nodeState, maxNodeVisits);
1411
+ const styled = decorateAuthoringEdges(
1412
+ run,
1413
+ problemIndex.byEdge,
1414
+ (edge) => insertArmed && isEditableEdge(edge),
1415
+ hasRunOverlay
1416
+ );
1112
1417
  return styled.map((e) => ({
1113
1418
  ...e,
1114
1419
  deletable: editable ? isEditableEdge(e) : false
1115
1420
  }));
1116
- }, [structural.edges, nodeState, maxNodeVisits, editable]);
1421
+ }, [
1422
+ structural.edges,
1423
+ nodeState,
1424
+ maxNodeVisits,
1425
+ editable,
1426
+ problemIndex,
1427
+ insertArmed,
1428
+ hasRunOverlay
1429
+ ]);
1117
1430
  const [nodes, setNodes, onNodesChange] = useNodesState(structural.nodes);
1118
1431
  const [edges, setEdges, onEdgesChange] = useEdgesState(styledEdges);
1119
1432
  const nodesRef = useRef(nodes);
@@ -1242,6 +1555,19 @@ function WorkflowGraph({
1242
1555
  },
1243
1556
  [onNodeClick]
1244
1557
  );
1558
+ const handleConnectEnd = useCallback(
1559
+ (event, state) => {
1560
+ if (!insertAtNode) return;
1561
+ if (state.toNode || state.toHandle) return;
1562
+ const from = state.fromNode;
1563
+ const fromHandle = state.fromHandle;
1564
+ if (!from || !fromHandle) return;
1565
+ const at = releasePoint(event);
1566
+ if (!at || !releasedOnCanvas(at, wrapperRef.current)) return;
1567
+ insertAtNode(from.id, fromHandle.type === "target" ? "before" : "after");
1568
+ },
1569
+ [insertAtNode]
1570
+ );
1245
1571
  const handleNodeKeyDown = useCallback(
1246
1572
  (event) => {
1247
1573
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -1265,7 +1591,7 @@ function WorkflowGraph({
1265
1591
  }
1266
1592
  );
1267
1593
  }
1268
- 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(
1594
+ 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(NodeBoxesContext.Provider, { value: nodeBoxes, children: /* @__PURE__ */ jsx3(NodeProblemsContext.Provider, { value: problemIndex.byNode, children: /* @__PURE__ */ jsx3(TriggerDeleteContext.Provider, { value: deleteTrigger ?? null, children: /* @__PURE__ */ jsx3(EdgeInsertContext.Provider, { value: insertOnEdge ?? null, children: /* @__PURE__ */ jsx3("div", { ref: wrapperRef, className: `wf-graph ${className ?? ""}`, children: /* @__PURE__ */ jsxs3(
1269
1595
  ReactFlow,
1270
1596
  {
1271
1597
  nodes,
@@ -1306,6 +1632,7 @@ function WorkflowGraph({
1306
1632
  }
1307
1633
  }
1308
1634
  } : void 0,
1635
+ onConnectEnd: insertAtNode ? handleConnectEnd : void 0,
1309
1636
  onEdgeClick: editable ? (_event, edge) => {
1310
1637
  if (isEditableEdge(edge)) {
1311
1638
  onEdgeClick?.(edge.source, edge.target);
@@ -1321,37 +1648,72 @@ function WorkflowGraph({
1321
1648
  children: [
1322
1649
  /* @__PURE__ */ jsx3(Background, { gap: 18 }),
1323
1650
  !isPreview && /* @__PURE__ */ jsxs3(Fragment, { children: [
1324
- /* @__PURE__ */ jsx3(Controls, { showInteractive: false, position: "bottom-right" }),
1325
- /* @__PURE__ */ jsx3(Panel, { position: "top-left", children: /* @__PURE__ */ jsxs3(
1326
- "button",
1651
+ /* @__PURE__ */ jsx3(
1652
+ Controls,
1327
1653
  {
1328
- type: "button",
1329
- onClick: () => setUserCompact((c) => !c),
1330
- "aria-pressed": userCompact,
1331
- title: userCompact ? "Expand nodes" : "Collapse nodes",
1332
- className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1333
- children: [
1334
- userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
1335
- userCompact ? "Expand" : "Compact"
1336
- ]
1654
+ showInteractive: false,
1655
+ position: "bottom-right",
1656
+ fitViewOptions: { padding: FIT_VIEW.padding }
1337
1657
  }
1338
- ) })
1658
+ ),
1659
+ /* @__PURE__ */ jsxs3(Panel, { position: "top-left", className: "flex items-center gap-1", children: [
1660
+ addTrigger && /* @__PURE__ */ jsxs3(
1661
+ "button",
1662
+ {
1663
+ type: "button",
1664
+ "data-testid": "wf-trigger-add",
1665
+ onClick: addTrigger,
1666
+ title: "Add a trigger",
1667
+ "aria-label": "Add a trigger",
1668
+ className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1669
+ children: [
1670
+ /* @__PURE__ */ jsx3(Plus, { size: 12, "aria-hidden": true }),
1671
+ "Trigger"
1672
+ ]
1673
+ }
1674
+ ),
1675
+ /* @__PURE__ */ jsxs3(
1676
+ "button",
1677
+ {
1678
+ type: "button",
1679
+ onClick: () => {
1680
+ const next = !nextDensityFrom.current;
1681
+ nextDensityFrom.current = next;
1682
+ setUserCompact(next);
1683
+ onCompactChange?.(next);
1684
+ },
1685
+ "aria-pressed": userCompact,
1686
+ title: userCompact ? "Expand nodes" : "Collapse nodes",
1687
+ className: "flex items-center gap-1 rounded-md border border-border bg-card/80 px-2 py-1 text-[11px] text-muted-foreground shadow-sm backdrop-blur transition hover:text-foreground",
1688
+ children: [
1689
+ userCompact ? /* @__PURE__ */ jsx3(Maximize2, { size: 12 }) : /* @__PURE__ */ jsx3(Minimize2, { size: 12 }),
1690
+ userCompact ? "Expand" : "Compact"
1691
+ ]
1692
+ }
1693
+ )
1694
+ ] })
1339
1695
  ] })
1340
1696
  ]
1341
1697
  }
1342
- ) }) }) }) }) }) });
1698
+ ) }) }) }) }) }) }) }) }) }) });
1343
1699
  }
1344
1700
  export {
1345
1701
  ConnectableContext,
1346
1702
  DensityContext,
1347
1703
  DirectionContext,
1704
+ EdgeInsertContext,
1348
1705
  FIT_VIEW,
1349
1706
  LAYOUT_TRANSITION_MS,
1707
+ NodeBoxesContext,
1708
+ NodeProblemsContext,
1350
1709
  RunModeContext,
1351
1710
  SelectedNodeContext,
1711
+ TriggerDeleteContext,
1712
+ WfEdgeRenderer,
1352
1713
  WorkflowGraph,
1353
1714
  WorkflowNode,
1354
1715
  buildStyledEdges,
1716
+ decorateAuthoringEdges,
1355
1717
  fitZoomCeiling,
1356
1718
  fitZoomFloor,
1357
1719
  framingViewport,
@@ -230,10 +230,18 @@ function actionNodeId(index) {
230
230
  function branchNodeId(actionIndex, branchIndex) {
231
231
  return `${actionNodeId(actionIndex)}-b${branchIndex}`;
232
232
  }
233
+ function wfEdgeId(source, target) {
234
+ return `${source}->${target}`;
235
+ }
236
+ function worstSeverity(problems) {
237
+ if (!problems || problems.length === 0) return null;
238
+ return problems.every((p) => p.severity === "warning") ? "warning" : "error";
239
+ }
233
240
  var NODE_W = 292;
234
241
  var RANK_SEP = 72;
235
242
  var CROSS_SEP = 24;
236
243
  var EDGE_LABEL_LANE = 180;
244
+ var EDGE_INSERT_LANE = 44;
237
245
  var CARD_CHROME = 20;
238
246
  var HEADER_ROW = 34;
239
247
  var DESCRIPTION_ROW = 40;
@@ -814,7 +822,7 @@ function buildWorkflowGraph(yaml, options) {
814
822
  };
815
823
  const edgeIds = /* @__PURE__ */ new Set();
816
824
  const addEdge = (source, target, kind, whenLabel) => {
817
- const id = `${source}->${target}`;
825
+ const id = wfEdgeId(source, target);
818
826
  if (edgeIds.has(id)) return;
819
827
  edgeIds.add(id);
820
828
  edges.push({
@@ -902,7 +910,7 @@ function buildWorkflowGraph(yaml, options) {
902
910
  const labelled = edges.some(
903
911
  (e) => e.whenLabel !== void 0 || e.backEdge === true
904
912
  );
905
- const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : geo.rankSep;
913
+ const rankSep = labelled ? Math.max(geo.rankSep, EDGE_LABEL_LANE) : options?.reserveEdgeInsert ? Math.max(geo.rankSep, EDGE_INSERT_LANE) : geo.rankSep;
906
914
  const folded = options?.wrap && direction === "LR" && !labelled ? layoutSerpentine(logical, edges, rankSep, geo.crossSep) : null;
907
915
  return {
908
916
  nodes: folded ?? layoutLayers(logical, direction, rankSep, geo.crossSep),
@@ -923,6 +931,8 @@ export {
923
931
  triggerNodeIndex,
924
932
  actionNodeId,
925
933
  branchNodeId,
934
+ wfEdgeId,
935
+ worstSeverity,
926
936
  AGENT_BODY_ROWS,
927
937
  ACTION_OUTPUT_ROWS,
928
938
  STRUCTURAL_OUTPUT_ROWS,
package/dist/globals.css CHANGED
@@ -407,6 +407,9 @@
407
407
  .bottom-\[-0\.75rem\] {
408
408
  bottom: -0.75rem;
409
409
  }
410
+ .-left-1\.5 {
411
+ left: calc(var(--spacing) * -1.5);
412
+ }
410
413
  .left-0 {
411
414
  left: 0px;
412
415
  }
@@ -485,6 +488,9 @@
485
488
  max-width: 96rem;
486
489
  }
487
490
  }
491
+ .-m-1 {
492
+ margin: calc(var(--spacing) * -1);
493
+ }
488
494
  .m-4 {
489
495
  margin: calc(var(--spacing) * 4);
490
496
  }
@@ -3884,6 +3890,9 @@
3884
3890
  border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
3885
3891
  }
3886
3892
  }
3893
+ .hover\:border-primary:hover {
3894
+ border-color: var(--color-primary);
3895
+ }
3887
3896
  .hover\:border-primary\/20:hover {
3888
3897
  border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
3889
3898
  }
package/dist/styles.css CHANGED
@@ -407,6 +407,9 @@
407
407
  .bottom-\[-0\.75rem\] {
408
408
  bottom: -0.75rem;
409
409
  }
410
+ .-left-1\.5 {
411
+ left: calc(var(--spacing) * -1.5);
412
+ }
410
413
  .left-0 {
411
414
  left: 0px;
412
415
  }
@@ -485,6 +488,9 @@
485
488
  max-width: 96rem;
486
489
  }
487
490
  }
491
+ .-m-1 {
492
+ margin: calc(var(--spacing) * -1);
493
+ }
488
494
  .m-4 {
489
495
  margin: calc(var(--spacing) * 4);
490
496
  }
@@ -3884,6 +3890,9 @@
3884
3890
  border-color: color-mix(in oklab, var(--color-foreground) 20%, transparent);
3885
3891
  }
3886
3892
  }
3893
+ .hover\:border-primary:hover {
3894
+ border-color: var(--color-primary);
3895
+ }
3887
3896
  .hover\:border-primary\/20:hover {
3888
3897
  border-color: color-mix(in srgb, hsl(var(--primary)) 20%, transparent);
3889
3898
  }
@@ -206,6 +206,48 @@ interface WfEdgeSpec {
206
206
  to: string;
207
207
  whenLabel?: string;
208
208
  }
209
+ /** How serious an authoring problem is. An `error` stops the definition from
210
+ * compiling; a `warning` compiles but is worth saying out loud. */
211
+ type WfProblemSeverity = "error" | "warning";
212
+ /**
213
+ * One AUTHORING problem, anchored to the node or the edge that carries it.
214
+ *
215
+ * This is the DEFINITION-time counterpart of {@link WfNodeState}, and it is a
216
+ * separate channel on purpose: run state is what a node DID (status, cost,
217
+ * duration), while a problem is what is wrong with what it SAYS. Folding one
218
+ * into the other would make an unsaved compile error render as a failed run.
219
+ *
220
+ * The host supplies the message already written for a reader — this library
221
+ * renders graphs and never interprets a compiler's output. Anchors are node ids
222
+ * from THIS graph, so name them with the exported id helpers (`actionNodeId`,
223
+ * `branchNodeId`, `TRIGGER_NODE_ID`); an edge is named by its endpoints, the
224
+ * same way {@link WfEdgeSpec} names one.
225
+ *
226
+ * An anchor the graph has no slot for is simply not drawn — an authoring surface
227
+ * reports against a draft that is still moving, and a stale anchor must never
228
+ * blank the canvas the way a stale declared edge does. That is the ONLY safety
229
+ * this gives, and it is worth being exact about what it leaves out: these ids are
230
+ * POSITIONAL, so an insert, delete or reorder does not retire `a1` — it makes
231
+ * `a1` a different step. A problem computed against an earlier draft therefore
232
+ * survives the check and lands on whichever step now holds its position. Nothing
233
+ * here can tell the two apart, which is why the pairing contract on
234
+ * `WorkflowGraphProps.problems` is a contract and not a suggestion: a list must
235
+ * be handed over with the definition it was computed from.
236
+ */
237
+ type WfProblem = {
238
+ anchor: "node";
239
+ /** The node the problem sits on. */
240
+ node: string;
241
+ severity: WfProblemSeverity;
242
+ message: string;
243
+ } | {
244
+ anchor: "edge";
245
+ /** The edge's endpoints, as {@link WfEdgeSpec} names them. */
246
+ from: string;
247
+ to: string;
248
+ severity: WfProblemSeverity;
249
+ message: string;
250
+ };
209
251
  interface WfGraph {
210
252
  nodes: WfNode[];
211
253
  edges: WfEdge[];
@@ -252,6 +294,16 @@ interface BuildWorkflowGraphOptions {
252
294
  * worse than one that says it cannot be drawn.
253
295
  */
254
296
  edges?: readonly WfEdgeSpec[];
297
+ /**
298
+ * Widen the gap between layers so an edge has room for an in-line control at
299
+ * its midpoint — the insert affordance an EDITING canvas draws there. The
300
+ * compact densities pitch their layers at 20-34px, which is the whole button,
301
+ * so a canvas that renders one without asking for the lane puts it on top of
302
+ * the nodes either side. Reserved by the layout for the same reason run rows
303
+ * are: what a node or an edge renders is spaced for before it is drawn, never
304
+ * measured afterwards. Defaults to `false`.
305
+ */
306
+ reserveEdgeInsert?: boolean;
255
307
  }
256
308
  /** Build a positioned graph from a workflow YAML string. Never throws —
257
309
  * malformed YAML or an empty definition returns an `error` the UI can fall
@@ -279,6 +331,17 @@ interface WorkflowGraphProps {
279
331
  * toggle, and the preview variant is always compact.
280
332
  */
281
333
  defaultCompact?: boolean;
334
+ /**
335
+ * Called when the reader moves the density toggle, with the density they
336
+ * chose.
337
+ *
338
+ * `defaultCompact` seeds the graph; this reports back out of it, which is
339
+ * what a host needs to REMEMBER the choice. Without it a host can set an
340
+ * opening density but never learn that the reader wanted the other one, so
341
+ * every navigation puts them back where they started and the toggle has to
342
+ * be found again.
343
+ */
344
+ onCompactChange?: (compact: boolean) => void;
282
345
  /** Sizing for the wrapper; the caller controls height. */
283
346
  className?: string;
284
347
  /**
@@ -323,9 +386,15 @@ interface WorkflowGraphProps {
323
386
  * Editing gestures. Supplying `onEdgeConnect` turns the canvas from a diagram
324
387
  * into an EDITOR: node handles become visible and draggable, an edge can be
325
388
  * selected and removed with Delete/Backspace, and clicking one asks to edit
326
- * its guard. Omit all three — the default — and the graph stays the read-only
389
+ * its guard. Omit it — the default — and the graph stays the read-only
327
390
  * visualisation it has always been.
328
391
  *
392
+ * `onEdgeConnect` is the ONE prop that decides it. Every other editing
393
+ * callback below (`onEdgeDelete`, `onEdgeClick`, `onEdgeInsert`,
394
+ * `onNodeInsert`, `onTriggerAdd`, `onTriggerDelete`) refines an editor and is
395
+ * inert without it — a canvas that cannot accept a new connection has no
396
+ * business drawing an add control either.
397
+ *
329
398
  * Every callback speaks node ids (`actionNodeId`, `branchNodeId`), never
330
399
  * positions: this component reports the gesture, and turning it into a
331
400
  * definition edit is the host's job — it owns the YAML, and only it knows
@@ -338,8 +407,83 @@ interface WorkflowGraphProps {
338
407
  onEdgeConnect?: (sourceId: string, targetId: string) => void;
339
408
  onEdgeDelete?: (sourceId: string, targetId: string) => void;
340
409
  onEdgeClick?: (sourceId: string, targetId: string) => void;
410
+ /**
411
+ * Add a step ON an edge: the edge draws a "+" at its midpoint, and pressing it
412
+ * reports the pair it sits between. The layout widens the corridor between
413
+ * layers to hold the control, so arming this RELAYS OUT the graph — a compact
414
+ * canvas pitches its layers at 20px, which is the whole button.
415
+ *
416
+ * Offered on exactly the edges the other three gestures are ({@link
417
+ * isEditableEdge}): inserting on a fan-out or trigger edge would name a pair
418
+ * that exists in no definition. "Add a step at the very start" is the
419
+ * {@link onNodeInsert} drop instead — dragged from the first step's inbound
420
+ * handle onto empty canvas, which names one node rather than a synthesized
421
+ * edge. (A definition with no `on:` at all has no inbound handle on its first
422
+ * step, because nothing points at it; there the step list is the only way in.)
423
+ */
424
+ onEdgeInsert?: (sourceId: string, targetId: string) => void;
425
+ /**
426
+ * Add a step at the loose end of a connection dragged from a node's handle and
427
+ * released over empty canvas — the canvas's answer to "and then what?".
428
+ * `side` is which handle it left from: `"after"` for the outbound one (the new
429
+ * step follows `nodeId`), `"before"` for the inbound one (it precedes it).
430
+ *
431
+ * Reported for ANY node, including a trigger and a fan-out branch leaf: unlike
432
+ * an edge gesture, this names a node that certainly exists, and whether a step
433
+ * may go beside it is a question about the definition's schema — which this
434
+ * library does not model and the host already answers.
435
+ */
436
+ onNodeInsert?: (nodeId: string, side: "before" | "after") => void;
437
+ /**
438
+ * The `on:` list gains an entry. Drawn as a canvas control rather than a
439
+ * per-node one, because the workflow that most needs it is the one with NO
440
+ * trigger — which has no trigger node to hang a "+" on.
441
+ *
442
+ * Offered whenever it is supplied, including on a definition that ALREADY has
443
+ * a trigger: `on:` is a list with OR semantics, and a second subscription is a
444
+ * normal thing to want. Whether THIS workflow may take another one is a
445
+ * question about the definition's schema and the provider behind it, which
446
+ * this library does not model — a host that must cap the list withholds the
447
+ * callback.
448
+ */
449
+ onTriggerAdd?: () => void;
450
+ /** Remove the `on:` entry a trigger node stands for — the one node deletion
451
+ * that IS a canvas gesture (every other node is a `do` entry, reordered and
452
+ * removed in the list the host owns). Read the entry's position back with
453
+ * `triggerNodeIndex`. */
454
+ onTriggerDelete?: (nodeId: string) => void;
455
+ /**
456
+ * AUTHORING problems anchored to the nodes and edges that carry them, so a
457
+ * compile error is visible on the canvas rather than only in a list beside it.
458
+ * See {@link WfProblem} — and note it is a separate channel from `nodeState`,
459
+ * which is what a node DID rather than what is wrong with what it says.
460
+ *
461
+ * A problem tints the node's border in either density, and states itself on a
462
+ * mark in the card's identity row (a corner of the compact tile). A graph
463
+ * showing a RUN is not an authoring surface — its cards and edges state the
464
+ * run instead, which takes precedence over a problem's colour on both, and the
465
+ * problem's mark and chip still render beside it.
466
+ *
467
+ * What the canvas gives is a LOCATOR: which step or dependency is at fault,
468
+ * with the messages on the mark's `title` and in the accessibility tree. It is
469
+ * deliberately not a reader for them — at the zoom a whole pipeline is viewed
470
+ * at, a panel of message text on the canvas is unreadable, and the host
471
+ * already owns the problem list that the messages are read and acted on in.
472
+ *
473
+ * Pairing contract: anchors are positional node ids, so a problem list must be
474
+ * derived from the SAME definition passed as `yaml`. Hand over a list computed
475
+ * against an older draft and an id that still exists may now name a different
476
+ * step, which puts the diagnostic on an innocent one — the graph cannot tell
477
+ * the two apart, any more than it can for a run's `nodeState`. A host that
478
+ * validates asynchronously therefore holds each result against the text it was
479
+ * computed from, and passes neither until they agree.
480
+ *
481
+ * Immutability contract, as for `edges`: the index is memoized on this array's
482
+ * reference, so pass a stable one.
483
+ */
484
+ problems?: readonly WfProblem[];
341
485
  }
342
486
 
343
487
  declare function WorkflowGraph(props: WorkflowGraphProps): React.JSX.Element;
344
488
 
345
- export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
489
+ export { type BuildWorkflowGraphOptions, TRIGGER_NODE_ID, type WfEdge, type WfEdgeKind, type WfEdgeSpec, type WfGraph, type WfNode, type WfNodeData, type WfNodeState, type WfNodeStatus, type WfNodeTone, type WfProblem, type WfProblemSeverity, WorkflowGraph as WorkflowGraphLazy, type WorkflowGraphProps, actionNodeId, branchNodeId, buildWorkflowGraph, triggerNodeId, triggerNodeIndex };
package/dist/workflows.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  buildWorkflowGraph,
6
6
  triggerNodeId,
7
7
  triggerNodeIndex
8
- } from "./chunk-LHUBJPZR.js";
8
+ } from "./chunk-AI25G5MX.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-BJ5ZSDZB.js").then((m) => ({ default: m.WorkflowGraph }))
39
+ () => import("./WorkflowGraph-2QOORYIR.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.109.0",
3
+ "version": "0.111.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",