@bian-womp/spark-workbench 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/lib/cjs/index.cjs +484 -118
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +1 -2
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/contracts.d.ts +7 -0
  6. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  7. package/lib/cjs/src/index.d.ts +1 -0
  8. package/lib/cjs/src/index.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/DefaultNode.d.ts +1 -1
  10. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/NodeHandles.d.ts +20 -0
  13. package/lib/cjs/src/misc/NodeHandles.d.ts.map +1 -0
  14. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  15. package/lib/cjs/src/misc/WorkbenchStudio.d.ts +2 -2
  16. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  17. package/lib/cjs/src/misc/constants.d.ts +3 -0
  18. package/lib/cjs/src/misc/constants.d.ts.map +1 -0
  19. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +1 -0
  20. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  21. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  22. package/lib/cjs/src/misc/hooks.d.ts +2 -2
  23. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  24. package/lib/cjs/src/misc/mapping.d.ts +22 -0
  25. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  26. package/lib/esm/index.js +481 -117
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/src/core/InMemoryWorkbench.d.ts +1 -2
  29. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  30. package/lib/esm/src/core/contracts.d.ts +7 -0
  31. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  32. package/lib/esm/src/index.d.ts +1 -0
  33. package/lib/esm/src/index.d.ts.map +1 -1
  34. package/lib/esm/src/misc/DefaultNode.d.ts +1 -1
  35. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  36. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  37. package/lib/esm/src/misc/NodeHandles.d.ts +20 -0
  38. package/lib/esm/src/misc/NodeHandles.d.ts.map +1 -0
  39. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  40. package/lib/esm/src/misc/WorkbenchStudio.d.ts +2 -2
  41. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  42. package/lib/esm/src/misc/constants.d.ts +3 -0
  43. package/lib/esm/src/misc/constants.d.ts.map +1 -0
  44. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +1 -0
  45. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  46. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  47. package/lib/esm/src/misc/hooks.d.ts +2 -2
  48. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  49. package/lib/esm/src/misc/mapping.d.ts +22 -0
  50. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  51. package/package.json +7 -5
package/lib/cjs/index.cjs CHANGED
@@ -3,9 +3,10 @@
3
3
  var sparkGraph = require('@bian-womp/spark-graph');
4
4
  var sparkRemote = require('@bian-womp/spark-remote');
5
5
  var React = require('react');
6
+ var react = require('@xyflow/react');
6
7
  var jsxRuntime = require('react/jsx-runtime');
7
- var react = require('@phosphor-icons/react');
8
- var react$1 = require('@xyflow/react');
8
+ var react$1 = require('@phosphor-icons/react');
9
+ var isEqual = require('lodash/isEqual');
9
10
  var cx = require('classnames');
10
11
 
11
12
  class DefaultUIExtensionRegistry {
@@ -201,6 +202,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
201
202
  });
202
203
  this.emit("validationChanged", this.validate());
203
204
  }
205
+ updateEdgeType(edgeId, typeId) {
206
+ const e = this.def.edges.find((x) => x.id === edgeId);
207
+ if (!e)
208
+ return;
209
+ if (!typeId)
210
+ delete e.typeId;
211
+ else
212
+ e.typeId = typeId;
213
+ this.emit("graphChanged", {
214
+ def: this.def,
215
+ change: { type: "updateEdgeType", edgeId, typeId },
216
+ });
217
+ this.refreshValidation();
218
+ }
204
219
  updateParams(nodeId, params) {
205
220
  const n = this.def.nodes.find((n) => n.nodeId === nodeId);
206
221
  if (!n)
@@ -232,6 +247,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
232
247
  setSelection(sel) {
233
248
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
234
249
  this.emit("selectionChanged", this.selection);
250
+ this.emit("graphUiChanged", {
251
+ def: this.def,
252
+ change: { type: "selection" },
253
+ });
235
254
  }
236
255
  getSelection() {
237
256
  return {
@@ -239,18 +258,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
239
258
  edges: [...this.selection.edges],
240
259
  };
241
260
  }
242
- toggleNodeSelection(nodeId) {
243
- this.selection.nodes = this.selection.nodes.includes(nodeId)
244
- ? this.selection.nodes.filter((id) => id !== nodeId)
245
- : [...this.selection.nodes, nodeId];
246
- this.emit("selectionChanged", this.selection);
247
- }
248
- toggleEdgeSelection(edgeId) {
249
- this.selection.edges = this.selection.edges.includes(edgeId)
250
- ? this.selection.edges.filter((id) => id !== edgeId)
251
- : [...this.selection.edges, edgeId];
252
- this.emit("selectionChanged", this.selection);
253
- }
254
261
  on(event, handler) {
255
262
  if (!this.listeners.has(event))
256
263
  this.listeners.set(event, new Set());
@@ -760,49 +767,91 @@ function useWorkbenchBridge(wb) {
760
767
  });
761
768
  }, [wb]);
762
769
  const onNodesChange = React.useCallback((changes) => {
770
+ // Apply position updates
763
771
  changes.forEach((c) => {
764
- if (c.type === "position" && c.position)
772
+ if (c.type === "position" && c.position) {
765
773
  wb.setPosition(c.id, c.position);
766
- if (c.type === "remove")
767
- wb.removeNode(c.id);
768
- if (c.type === "select")
769
- wb.toggleNodeSelection(c.id);
774
+ }
770
775
  });
776
+ // Derive next node selection from change set
777
+ const current = wb.getSelection();
778
+ const nextNodeIds = new Set(current.nodes);
779
+ let selectionChanged = false;
780
+ for (const change of changes) {
781
+ const type = change?.type;
782
+ if (type === "select") {
783
+ const id = change.id;
784
+ const selected = change.selected;
785
+ if (typeof selected === "boolean") {
786
+ if (selected) {
787
+ if (!nextNodeIds.has(id)) {
788
+ nextNodeIds.add(id);
789
+ selectionChanged = true;
790
+ }
791
+ }
792
+ else if (nextNodeIds.delete(id)) {
793
+ selectionChanged = true;
794
+ }
795
+ }
796
+ }
797
+ else if (type === "remove") {
798
+ const id = change.id;
799
+ if (nextNodeIds.delete(id))
800
+ selectionChanged = true;
801
+ }
802
+ }
803
+ if (selectionChanged) {
804
+ wb.setSelection({
805
+ nodes: Array.from(nextNodeIds),
806
+ edges: current.edges,
807
+ });
808
+ }
771
809
  }, [wb]);
772
810
  const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
773
811
  const onEdgesChange = React.useCallback((changes) => {
774
- changes.forEach((c) => {
775
- if (c.type === "remove")
776
- wb.disconnect(c.id);
777
- else if (c.type === "select")
778
- wb.toggleEdgeSelection(c.id);
779
- });
812
+ const current = wb.getSelection();
813
+ const nextEdgeIds = new Set(current.edges);
814
+ let selectionChanged = false;
815
+ for (const change of changes) {
816
+ const type = change?.type;
817
+ if (type === "select") {
818
+ const id = change.id;
819
+ const selected = change.selected;
820
+ if (typeof selected === "boolean") {
821
+ if (selected) {
822
+ if (!nextEdgeIds.has(id)) {
823
+ nextEdgeIds.add(id);
824
+ selectionChanged = true;
825
+ }
826
+ }
827
+ else if (nextEdgeIds.delete(id)) {
828
+ selectionChanged = true;
829
+ }
830
+ }
831
+ }
832
+ else if (type === "remove") {
833
+ const id = change.id;
834
+ if (nextEdgeIds.delete(id))
835
+ selectionChanged = true;
836
+ }
837
+ }
838
+ if (selectionChanged) {
839
+ wb.setSelection({
840
+ nodes: current.nodes,
841
+ edges: Array.from(nextEdgeIds),
842
+ });
843
+ }
780
844
  }, [wb]);
781
845
  const onNodesDelete = React.useCallback((nodes) => {
782
846
  for (const n of nodes)
783
847
  wb.removeNode(n.id);
784
848
  }, [wb]);
785
- const onSelectionChange = React.useCallback((sel) => {
786
- const next = {
787
- nodes: sel.nodes.map((n) => n.id),
788
- edges: sel.edges.map((e) => e.id),
789
- };
790
- const cur = wb.getSelection();
791
- const sameLen = cur.nodes.length === next.nodes.length &&
792
- cur.edges.length === next.edges.length;
793
- const same = sameLen &&
794
- cur.nodes.every((id, i) => id === next.nodes[i]) &&
795
- cur.edges.every((id, i) => id === next.edges[i]);
796
- if (!same)
797
- wb.setSelection(next);
798
- }, [wb]);
799
849
  return {
800
850
  onConnect,
801
851
  onNodesChange,
802
852
  onEdgesChange,
803
853
  onEdgesDelete,
804
854
  onNodesDelete,
805
- onSelectionChange,
806
855
  };
807
856
  }
808
857
  function useWorkbenchGraphTick(wb) {
@@ -842,6 +891,44 @@ function useWorkbenchVersionTick(runner) {
842
891
  }, [runner]);
843
892
  return version;
844
893
  }
894
+ function useThrottledValue(value, intervalMs) {
895
+ const [throttled, setThrottled] = React.useState(value);
896
+ const lastSetAtRef = React.useRef(0);
897
+ const timeoutRef = React.useRef(null);
898
+ React.useEffect(() => {
899
+ const now = typeof performance !== "undefined" && performance.now
900
+ ? performance.now()
901
+ : Date.now();
902
+ const elapsed = now - lastSetAtRef.current;
903
+ if (elapsed >= intervalMs) {
904
+ lastSetAtRef.current = now;
905
+ setThrottled(value);
906
+ }
907
+ else {
908
+ if (timeoutRef.current !== null) {
909
+ window.clearTimeout(timeoutRef.current);
910
+ }
911
+ timeoutRef.current = window.setTimeout(() => {
912
+ lastSetAtRef.current =
913
+ typeof performance !== "undefined" && performance.now
914
+ ? performance.now()
915
+ : Date.now();
916
+ setThrottled(value);
917
+ timeoutRef.current = null;
918
+ }, Math.max(0, intervalMs - elapsed));
919
+ }
920
+ return () => { };
921
+ }, [value, intervalMs]);
922
+ React.useEffect(() => {
923
+ return () => {
924
+ if (timeoutRef.current !== null) {
925
+ window.clearTimeout(timeoutRef.current);
926
+ timeoutRef.current = null;
927
+ }
928
+ };
929
+ }, []);
930
+ return throttled;
931
+ }
845
932
  // Query param helpers
846
933
  function setSearchParam(key, val) {
847
934
  if (typeof window === "undefined")
@@ -1002,7 +1089,13 @@ function summarizeDeep(value) {
1002
1089
  return value;
1003
1090
  }
1004
1091
 
1092
+ // Shared UI constants for node layout to keep mapping and rendering in sync
1093
+ const NODE_HEADER_HEIGHT_PX = 24;
1094
+ const NODE_ROW_HEIGHT_PX = 22;
1095
+
1005
1096
  function toReactFlow(def, positions, registry, opts) {
1097
+ const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
1098
+ const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
1006
1099
  const nodeHandleMap = {};
1007
1100
  const nodes = def.nodes.map((n) => {
1008
1101
  const desc = registry.nodes.get(n.typeId);
@@ -1014,6 +1107,35 @@ function toReactFlow(def, positions, registry, opts) {
1014
1107
  inputs: new Set(inputHandles.map((h) => h.id)),
1015
1108
  outputs: new Set(outputHandles.map((h) => h.id)),
1016
1109
  };
1110
+ // Match DefaultNode sizing heuristics to avoid hidden nodes during re-measure
1111
+ const HEADER_SIZE = NODE_HEADER_HEIGHT_PX;
1112
+ const ROW_SIZE = NODE_ROW_HEIGHT_PX;
1113
+ const maxRows = Math.max(inputHandles.length, outputHandles.length);
1114
+ const initialWidth = opts.showValues ? 320 : 240;
1115
+ const initialHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1116
+ // Precompute handle bounds so edges can render immediately without waiting for measurement
1117
+ const handles = [
1118
+ // Inputs on the left as targets
1119
+ ...inputHandles.map((h, i) => ({
1120
+ id: h.id,
1121
+ type: "target",
1122
+ position: react.Position.Left,
1123
+ x: 0,
1124
+ y: HEADER_SIZE + i * ROW_SIZE,
1125
+ width: 1,
1126
+ height: ROW_SIZE + 2,
1127
+ })),
1128
+ // Outputs on the right as sources
1129
+ ...outputHandles.map((h, i) => ({
1130
+ id: h.id,
1131
+ type: "source",
1132
+ position: react.Position.Right,
1133
+ x: initialWidth - 1,
1134
+ y: HEADER_SIZE + i * ROW_SIZE,
1135
+ width: 1,
1136
+ height: ROW_SIZE + 2,
1137
+ })),
1138
+ ];
1017
1139
  return {
1018
1140
  id: n.nodeId,
1019
1141
  data: {
@@ -1021,7 +1143,23 @@ function toReactFlow(def, positions, registry, opts) {
1021
1143
  params: n.params,
1022
1144
  inputHandles,
1023
1145
  outputHandles,
1146
+ handleLayout: [
1147
+ ...inputHandles.map((h, i) => ({
1148
+ id: h.id,
1149
+ type: "target",
1150
+ position: react.Position.Left,
1151
+ y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
1152
+ })),
1153
+ ...outputHandles.map((h, i) => ({
1154
+ id: h.id,
1155
+ type: "source",
1156
+ position: react.Position.Right,
1157
+ y: HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2,
1158
+ })),
1159
+ ],
1024
1160
  showValues: opts.showValues,
1161
+ renderWidth: initialWidth,
1162
+ renderHeight: initialHeight,
1025
1163
  inputValues: opts.inputs?.[n.nodeId],
1026
1164
  outputValues: opts.outputs?.[n.nodeId],
1027
1165
  status: opts.nodeStatus?.[n.nodeId],
@@ -1038,6 +1176,11 @@ function toReactFlow(def, positions, registry, opts) {
1038
1176
  selected: opts.selectedNodeIds
1039
1177
  ? opts.selectedNodeIds.has(n.nodeId)
1040
1178
  : undefined,
1179
+ initialWidth,
1180
+ initialHeight,
1181
+ handles,
1182
+ width: initialWidth,
1183
+ height: initialHeight,
1041
1184
  };
1042
1185
  });
1043
1186
  const edges = def.edges
@@ -1054,9 +1197,9 @@ function toReactFlow(def, positions, registry, opts) {
1054
1197
  const hasError = !!st?.lastError;
1055
1198
  const isInvalidEdge = !!opts.edgeValidation?.[e.id];
1056
1199
  const style = hasError || isInvalidEdge
1057
- ? { stroke: "#ef4444", strokeWidth: 2 }
1200
+ ? EDGE_STYLE_ERROR
1058
1201
  : isRunning
1059
- ? { stroke: "#3b82f6" }
1202
+ ? EDGE_STYLE_RUNNING
1060
1203
  : undefined;
1061
1204
  return {
1062
1205
  id: e.id,
@@ -1084,17 +1227,35 @@ function getNodeBorderClassNames(args) {
1084
1227
  const hasValidationWarning = !hasValidationError && issues.length > 0;
1085
1228
  const isRunning = !!status.activeRuns;
1086
1229
  const isInvalid = !!status.invalidated && !isRunning && !hasError;
1087
- const borderWidth = selected ? "border-2" : "border";
1230
+ // Keep border width constant to avoid layout reflow on selection toggles
1231
+ const borderWidth = "border";
1088
1232
  const borderStyle = isInvalid ? "border-dashed" : "border-solid";
1089
- const borderColor = hasError || hasValidationError
1090
- ? "border-red-500"
1233
+ const severity = hasError || hasValidationError
1234
+ ? "red"
1091
1235
  : hasValidationWarning
1092
- ? "border-amber-500"
1236
+ ? "amber"
1093
1237
  : isRunning
1094
- ? "border-blue-500"
1095
- : "border-gray-500 dark:border-gray-400";
1096
- const ring = isRunning ? " ring-2 ring-blue-200 dark:ring-blue-900" : "";
1097
- return `${borderWidth} ${borderStyle} ${borderColor}${ring}`.trim();
1238
+ ? "blue"
1239
+ : "gray";
1240
+ const borderBySeverity = {
1241
+ red: "border-red-500",
1242
+ amber: "border-amber-500",
1243
+ blue: "border-blue-500",
1244
+ gray: "border-gray-500 dark:border-gray-400",
1245
+ };
1246
+ const ringBySeverity = {
1247
+ red: "ring-2 ring-red-300 dark:ring-red-900",
1248
+ amber: "ring-2 ring-amber-300 dark:ring-amber-900",
1249
+ blue: "ring-2 ring-blue-200 dark:ring-blue-900",
1250
+ gray: "ring-2 ring-gray-300 dark:ring-gray-500",
1251
+ };
1252
+ const borderColor = borderBySeverity[severity];
1253
+ const ring = isRunning
1254
+ ? ringBySeverity.blue
1255
+ : selected
1256
+ ? ringBySeverity[severity === "blue" ? "gray" : severity]
1257
+ : "";
1258
+ return [borderWidth, borderStyle, borderColor, ring].join(" ").trim();
1098
1259
  }
1099
1260
 
1100
1261
  const WorkbenchContext = React.createContext(null);
@@ -1230,6 +1391,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1230
1391
  });
1231
1392
  wb.setPositions(pos);
1232
1393
  }, [wb]);
1394
+ const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
1233
1395
  // Subscribe to runner/workbench events
1234
1396
  React.useEffect(() => {
1235
1397
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -1240,8 +1402,15 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1240
1402
  if (changeType === "moveNode" || changeType === "moveNodes")
1241
1403
  return prev;
1242
1404
  }
1405
+ const nextNo = prev.length > 0 ? (prev[0]?.no ?? 0) + 1 : 1;
1243
1406
  const next = [
1244
- { at: Date.now(), source, type, payload: structuredClone(payload) },
1407
+ {
1408
+ no: nextNo,
1409
+ at: Date.now(),
1410
+ source,
1411
+ type,
1412
+ payload: structuredClone(payload),
1413
+ },
1245
1414
  ...prev,
1246
1415
  ];
1247
1416
  return next.length > 200 ? next.slice(0, 200) : next;
@@ -1556,6 +1725,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1556
1725
  step,
1557
1726
  flush,
1558
1727
  runAutoLayout,
1728
+ updateEdgeType,
1559
1729
  }), [
1560
1730
  wb,
1561
1731
  runner,
@@ -1582,13 +1752,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1582
1752
  step,
1583
1753
  flush,
1584
1754
  runAutoLayout,
1755
+ wb,
1585
1756
  ]);
1586
1757
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
1587
1758
  }
1588
1759
 
1589
1760
  function IssueBadge({ level, title, size = 12, className, }) {
1590
1761
  const colorClass = level === "error" ? "text-red-600" : "text-amber-600";
1591
- return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react.WarningCircleIcon, { size: size, weight: "fill" })) }));
1762
+ return (jsxRuntime.jsx("button", { type: "button", className: `inline-flex items-center justify-center shrink-0 ${colorClass} ${className ?? ""}`, title: title, style: { width: size, height: size }, children: level === "error" ? (jsxRuntime.jsx(react$1.XCircleIcon, { size: size, weight: "fill" })) : (jsxRuntime.jsx(react$1.WarningCircleIcon, { size: size, weight: "fill" })) }));
1592
1763
  }
1593
1764
 
1594
1765
  function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
@@ -1617,7 +1788,7 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
1617
1788
  return String(v);
1618
1789
  }
1619
1790
  };
1620
- return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev, idx) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-8 shrink-0 text-right text-gray-500 select-none", children: idx + 1 }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-10", children: renderPayload(ev.payload) })] }, `${ev.at}:${idx}`))) })] }));
1791
+ return (jsxRuntime.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Events" }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: hideWorkbench, onChange: (e) => onHideWorkbenchChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Hide workbench" })] }), jsxRuntime.jsxs("label", { className: "flex items-center gap-1 text-xs text-gray-700", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: autoScroll, onChange: (e) => onAutoScrollChange?.(e.target.checked) }), jsxRuntime.jsx("span", { children: "Auto scroll" })] }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "text-xs px-2 py-0.5 border border-gray-300 rounded", children: "Clear" })] })] }), jsxRuntime.jsx("div", { ref: scrollRef, className: "flex-1 overflow-auto text-[11px] leading-4 divide-y divide-gray-200", children: rows.map((ev) => (jsxRuntime.jsxs("div", { className: "opacity-85 odd:bg-gray-50 px-2 py-1", children: [jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [jsxRuntime.jsx("span", { className: "w-12 shrink-0 text-right text-gray-500 select-none", children: ev.no }), jsxRuntime.jsxs("span", { className: "text-gray-500", children: [new Date(ev.at).toLocaleTimeString(), " \u00B7 ", ev.source, ":", ev.type] })] }), jsxRuntime.jsx("pre", { className: "m-0 whitespace-pre-wrap ml-12", children: renderPayload(ev.payload) })] }, `${ev.at}:${ev.no}`))) })] }));
1621
1792
  }
1622
1793
 
1623
1794
  function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
@@ -1632,7 +1803,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1632
1803
  return String(value ?? "");
1633
1804
  }
1634
1805
  };
1635
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, } = useWorkbenchContext();
1806
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, } = useWorkbenchContext();
1636
1807
  const nodeValidationIssues = validationByNode.issues;
1637
1808
  const edgeValidationIssues = validationByEdge.issues;
1638
1809
  const nodeValidationHandles = validationByNode;
@@ -1709,7 +1880,11 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1709
1880
  setOriginals(nextOriginals);
1710
1881
  }, [selectedNodeId, selectedDesc, valuesTick]);
1711
1882
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
1712
- return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && (jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel })), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1883
+ return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [contextPanel && jsxRuntime.jsx("div", { className: "mb-2", children: contextPanel }), jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsxs("div", { className: "text-xs text-gray-500 mb-2", children: ["valuesTick: ", valuesTick] }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: "Type" }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: "DataTypeId" })] }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500 w-full", value: selectedEdge.typeId ?? "", onChange: (e) => {
1884
+ const v = e.target.value;
1885
+ const next = v === "" ? undefined : v;
1886
+ updateEdgeType(selectedEdge.id, next);
1887
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "(infer from source)" }), Array.from(registry.types.keys()).map((tid) => (jsxRuntime.jsx("option", { value: tid, children: tid }, tid)))] })] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1713
1888
  selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
1714
1889
  const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
1715
1890
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
@@ -1770,79 +1945,120 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1770
1945
  })()] }, h))))] }), selectedNodeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedNodeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
1771
1946
  }
1772
1947
 
1948
+ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
1949
+ const layout = data.handleLayout ?? [];
1950
+ const byId = React.useMemo(() => {
1951
+ const m = new Map();
1952
+ for (const h of layout) {
1953
+ m.set(h.id, { position: h.position, y: h.y, type: h.type });
1954
+ }
1955
+ return m;
1956
+ }, [layout]);
1957
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
1958
+ const placed = byId.get(h.id);
1959
+ const position = placed?.position ?? react.Position.Left;
1960
+ const y = placed?.y;
1961
+ const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
1962
+ inputClassName;
1963
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "target", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " left-2", style: {
1964
+ top: (y ?? 0) - 8,
1965
+ right: "50%",
1966
+ whiteSpace: "nowrap",
1967
+ overflow: "hidden",
1968
+ textOverflow: "ellipsis",
1969
+ }, children: renderLabel({ kind: "input", id: h.id }) }))] }, h.id));
1970
+ }), (data.outputHandles ?? []).map((h) => {
1971
+ const placed = byId.get(h.id);
1972
+ const position = placed?.position ?? react.Position.Right;
1973
+ const y = placed?.y;
1974
+ const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
1975
+ outputClassName;
1976
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: h.id, type: "source", position: position, isConnectable: isConnectable, className: cls, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + " right-2", style: {
1977
+ top: (y ?? 0) - 8,
1978
+ left: "50%",
1979
+ textAlign: "right",
1980
+ whiteSpace: "nowrap",
1981
+ overflow: "hidden",
1982
+ textOverflow: "ellipsis",
1983
+ }, children: renderLabel({ kind: "output", id: h.id }) }))] }, h.id));
1984
+ })] }));
1985
+ }
1986
+
1773
1987
  const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
1988
+ const updateNodeInternals = react.useUpdateNodeInternals();
1774
1989
  const { typeId, showValues, inputValues, outputValues, toString } = data;
1775
1990
  const inputEntries = data.inputHandles ?? [];
1776
1991
  const outputEntries = data.outputHandles ?? [];
1992
+ React.useEffect(() => {
1993
+ updateNodeInternals(id);
1994
+ }, [
1995
+ id,
1996
+ inputEntries.length,
1997
+ outputEntries.length,
1998
+ showValues,
1999
+ updateNodeInternals,
2000
+ ]);
1777
2001
  const status = data.status ?? { activeRuns: 0 };
1778
2002
  const validation = data.validation ?? {
1779
2003
  inputs: [],
1780
2004
  outputs: [],
1781
2005
  issues: [],
1782
2006
  };
1783
- const HEADER_SIZE = 24;
1784
- const ROW_SIZE = 22;
1785
- const maxRows = Math.max(inputEntries.length, outputEntries.length);
1786
- const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1787
- const minWidth = data.showValues ? 320 : 240;
1788
- const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1789
2007
  const hasError = !!status.lastError;
1790
- const hasValidationError = validation.issues.some((i) => i.level === "error");
1791
- const hasValidationWarning = !hasValidationError && validation.issues.length > 0;
1792
2008
  const isRunning = !!status.activeRuns;
1793
- const isInvalid = !!status.invalidated && !isRunning && !hasError;
1794
- // Border color encodes severity; thickness encodes selection; style (dashed) encodes invalidated
1795
- const borderWidth = selected ? "border-2" : "border";
1796
- const borderStyle = isInvalid ? "border-dashed" : "border-solid";
1797
- const borderColor = hasError || hasValidationError
1798
- ? "border-red-500"
1799
- : hasValidationWarning
1800
- ? "border-amber-500"
1801
- : isRunning
1802
- ? "border-blue-500"
1803
- : "border-gray-500 dark:border-gray-400";
1804
- const ringClasses = isRunning
1805
- ? "ring-2 ring-blue-200 dark:ring-blue-900"
1806
- : undefined;
1807
- const borderClasses = cx(borderWidth, borderStyle, borderColor, ringClasses);
2009
+ const containerBorder = getNodeBorderClassNames({
2010
+ selected,
2011
+ status,
2012
+ validation,
2013
+ });
1808
2014
  const pct = Math.round(Math.max(0, Math.min(1, Number(status.progress) || 0)) * 100);
1809
- return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", borderClasses), style: { position: "relative", minHeight: minHeight, minWidth }, children: [jsxRuntime.jsxs("div", { className: "flex h-6 items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full leading-6 text-xs", children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
2015
+ return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", containerBorder), style: {
2016
+ position: "relative",
2017
+ minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
2018
+ minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
2019
+ }, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
2020
+ maxHeight: NODE_HEADER_HEIGHT_PX,
2021
+ minHeight: NODE_HEADER_HEIGHT_PX,
2022
+ }, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: typeId }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [hasError && (jsxRuntime.jsx("span", { title: String(status.lastError?.message ?? status.lastError), children: jsxRuntime.jsx(react$1.XCircleIcon, { size: 12, weight: "fill", className: "text-red-500" }) })), validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
1810
2023
  ? "error"
1811
2024
  : "warning", size: 12, className: "w-3 h-3", title: validation.issues
1812
2025
  .map((v) => `${v.code}: ${v.message}`)
1813
- .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), (isRunning || pct > 0) && (jsxRuntime.jsx("div", { className: "h-1 bg-blue-200 dark:bg-blue-900", children: jsxRuntime.jsx("div", { className: "h-1 bg-blue-500 transition-all", style: { width: `${pct}%` } }) })), inputEntries.map((entry, i) => {
1814
- const vIssues = validation.inputs.filter((v) => v.handle === entry.id);
1815
- const hasAny = vIssues.length > 0;
1816
- const hasErr = vIssues.some((v) => v.level === "error");
1817
- const title = vIssues.map((v) => `${v.code}: ${v.message}`).join("; ");
1818
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react$1.Handle, { id: entry.id, type: "target", position: react$1.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1819
- top: topFor(i) - 8,
1820
- right: "50%",
1821
- whiteSpace: "nowrap",
1822
- overflow: "hidden",
1823
- textOverflow: "ellipsis",
1824
- }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1825
- }), outputEntries.map((entry, i) => {
1826
- const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1827
- const hasAny = vIssues.length > 0;
1828
- const hasErr = vIssues.some((v) => v.level === "error");
1829
- const title = vIssues.map((v) => `${v.code}: ${v.message}`).join("; ");
1830
- const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
1831
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(react$1.Handle, { id: entry.id, type: "source", position: react$1.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1832
- top: topFor(i) - 8,
1833
- textAlign: "right",
1834
- left: "50%",
1835
- whiteSpace: "nowrap",
1836
- overflow: "hidden",
1837
- textOverflow: "ellipsis",
1838
- }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1839
- })] }));
2026
+ .join("; ") })), jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }), jsxRuntime.jsx("div", { className: cx("h-px", (isRunning || pct > 0) && "bg-blue-200 dark:bg-blue-900"), children: jsxRuntime.jsx("div", { className: cx("h-px transition-all", (isRunning || pct > 0) && "bg-blue-500"), style: { width: isRunning || pct > 0 ? `${pct}%` : 0 } }) }), jsxRuntime.jsx(NodeHandles, { data: data, isConnectable: isConnectable, getClassName: ({ kind, id }) => {
2027
+ const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2028
+ const hasAny = vIssues.length > 0;
2029
+ const hasErr = vIssues.some((v) => v.level === "error");
2030
+ return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", kind === "output" && "!rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500"));
2031
+ }, renderLabel: ({ kind, id }) => {
2032
+ const entries = kind === "input" ? inputEntries : outputEntries;
2033
+ const entry = entries.find((e) => e.id === id);
2034
+ if (!entry)
2035
+ return id;
2036
+ const vIssues = (kind === "input" ? validation.inputs : validation.outputs).filter((v) => v.handle === id);
2037
+ const hasAny = vIssues.length > 0;
2038
+ const hasErr = vIssues.some((v) => v.level === "error");
2039
+ const title = vIssues
2040
+ .map((v) => `${v.code}: ${v.message}`)
2041
+ .join("; ");
2042
+ // Compose label with truncated value to prevent layout growth
2043
+ const valueText = (() => {
2044
+ if (!showValues)
2045
+ return undefined;
2046
+ if (kind === "input") {
2047
+ const txt = toString(entry.typeId, inputValues?.[entry.id]);
2048
+ return typeof txt === "string" ? txt : String(txt);
2049
+ }
2050
+ const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
2051
+ const txt = toString(resolved.typeId, resolved.value);
2052
+ return typeof txt === "string" ? txt : String(txt);
2053
+ })();
2054
+ return (jsxRuntime.jsxs("span", { className: "flex items-center gap-1 w-full", children: [kind === "output" ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pl-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText })), jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "truncate shrink-0", style: { maxWidth: "40%" }, children: id }), valueText !== undefined && (jsxRuntime.jsx("span", { className: "opacity-60 truncate pr-1", style: { flex: 1, minWidth: 0, maxWidth: "100%" }, children: valueText }))] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "shrink-0", title: title }))] }));
2055
+ } })] }));
1840
2056
  });
1841
2057
  DefaultNode.displayName = "DefaultNode";
1842
2058
 
1843
2059
  function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
1844
2060
  const { registry } = useWorkbenchContext();
1845
- const rf = react$1.useReactFlow();
2061
+ const rf = react.useReactFlow();
1846
2062
  const ids = Array.from(registry.nodes.keys());
1847
2063
  const [query, setQuery] = React.useState("");
1848
2064
  const q = query.trim().toLowerCase();
@@ -2001,6 +2217,58 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
2001
2217
  const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
2002
2218
  const nodeValidation = validationByNode;
2003
2219
  const edgeValidation = validationByEdge.errors;
2220
+ // Keep stable references for nodes/edges to avoid unnecessary updates
2221
+ const prevNodesRef = React.useRef([]);
2222
+ const prevEdgesRef = React.useRef([]);
2223
+ function retainStabilityById(prev, next, isSame) {
2224
+ if (prev.length === 0)
2225
+ return next;
2226
+ const map = new Map();
2227
+ for (const p of prev)
2228
+ map.set(p.id, p);
2229
+ const out = new Array(next.length);
2230
+ for (let i = 0; i < next.length; i++) {
2231
+ const n = next[i];
2232
+ const p = map.get(n.id);
2233
+ out[i] = p && isSame(p, n) ? p : n;
2234
+ }
2235
+ return out;
2236
+ }
2237
+ const isSameNode = (a, b) => {
2238
+ // Compare the parts that affect rendering
2239
+ const pick = (n) => ({
2240
+ position: n.position,
2241
+ type: n.type,
2242
+ selected: n.selected,
2243
+ initialWidth: n.initialWidth,
2244
+ initialHeight: n.initialHeight,
2245
+ data: n.data && {
2246
+ typeId: n.data.typeId,
2247
+ inputHandles: n.data.inputHandles,
2248
+ outputHandles: n.data.outputHandles,
2249
+ showValues: n.data.showValues,
2250
+ inputValues: n.data.inputValues,
2251
+ outputValues: n.data.outputValues,
2252
+ status: n.data.status,
2253
+ validation: n.data.validation,
2254
+ },
2255
+ });
2256
+ return isEqual(pick(a), pick(b));
2257
+ };
2258
+ const isSameEdge = (a, b) => {
2259
+ const pick = (e) => ({
2260
+ source: e.source,
2261
+ target: e.target,
2262
+ sourceHandle: e.sourceHandle,
2263
+ targetHandle: e.targetHandle,
2264
+ selected: e.selected,
2265
+ animated: e.animated,
2266
+ style: e.style,
2267
+ label: e.label,
2268
+ type: e.type,
2269
+ });
2270
+ return isEqual(pick(a), pick(b));
2271
+ };
2004
2272
  // Expose imperative API
2005
2273
  const rfInstanceRef = React.useRef(null);
2006
2274
  React.useImperativeHandle(ref, () => ({
@@ -2011,7 +2279,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
2011
2279
  catch { }
2012
2280
  },
2013
2281
  }));
2014
- const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
2282
+ const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
2015
2283
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
2016
2284
  // Build nodeTypes map using UI extension registry
2017
2285
  const ui = wb.getUI();
@@ -2049,8 +2317,91 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
2049
2317
  selectedNodeIds: new Set(sel.nodes),
2050
2318
  selectedEdgeIds: new Set(sel.edges),
2051
2319
  });
2052
- // console.info(nodeTypes, out);
2053
- return out;
2320
+ // Retain references for unchanged items
2321
+ const stableNodes = retainStabilityById(prevNodesRef.current, out.nodes, isSameNode);
2322
+ const stableEdges = retainStabilityById(prevEdgesRef.current, out.edges, isSameEdge);
2323
+ // Debug: log updates/additions/removals (use value equality, not reference)
2324
+ try {
2325
+ const prevNodeIds = new Set(prevNodesRef.current.map((n) => n.id));
2326
+ const nextNodeIds = new Set(out.nodes.map((n) => n.id));
2327
+ const addedNodeIds = out.nodes
2328
+ .filter((n) => !prevNodeIds.has(n.id))
2329
+ .map((n) => n.id);
2330
+ const removedNodeIds = prevNodesRef.current
2331
+ .filter((n) => !nextNodeIds.has(n.id))
2332
+ .map((n) => n.id);
2333
+ const prevNodeMap = new Map(prevNodesRef.current.map((n) => [n.id, n]));
2334
+ const changedNodeIds = out.nodes
2335
+ .filter((n) => {
2336
+ const p = prevNodeMap.get(n.id);
2337
+ return p ? !isSameNode(p, n) : false;
2338
+ })
2339
+ .map((n) => n.id);
2340
+ // Detect handle updates (ids/length changes) for targeted debug
2341
+ const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => h?.id) : [];
2342
+ const handlesEqual = (a, b) => {
2343
+ const aIds = toIds(a);
2344
+ const bIds = toIds(b);
2345
+ if (aIds.length !== bIds.length)
2346
+ return false;
2347
+ for (let i = 0; i < aIds.length; i++) {
2348
+ if (aIds[i] !== bIds[i])
2349
+ return false;
2350
+ }
2351
+ return true;
2352
+ };
2353
+ const handleChanged = out.nodes
2354
+ .filter((n) => {
2355
+ const p = prevNodeMap.get(n.id);
2356
+ if (!p)
2357
+ return false;
2358
+ const inChanged = !handlesEqual(p.data?.inputHandles, n.data?.inputHandles);
2359
+ const outChanged = !handlesEqual(p.data?.outputHandles, n.data?.outputHandles);
2360
+ return inChanged || outChanged;
2361
+ })
2362
+ .map((n) => n.id);
2363
+ const prevEdgeIds = new Set(prevEdgesRef.current.map((e) => e.id));
2364
+ const nextEdgeIds = new Set(out.edges.map((e) => e.id));
2365
+ const addedEdgeIds = out.edges
2366
+ .filter((e) => !prevEdgeIds.has(e.id))
2367
+ .map((e) => e.id);
2368
+ const removedEdgeIds = prevEdgesRef.current
2369
+ .filter((e) => !nextEdgeIds.has(e.id))
2370
+ .map((e) => e.id);
2371
+ const prevEdgeMap = new Map(prevEdgesRef.current.map((e) => [e.id, e]));
2372
+ const changedEdgeIds = out.edges
2373
+ .filter((e) => {
2374
+ const p = prevEdgeMap.get(e.id);
2375
+ return p ? !isSameEdge(p, e) : false;
2376
+ })
2377
+ .map((e) => e.id);
2378
+ if (addedNodeIds.length ||
2379
+ removedNodeIds.length ||
2380
+ changedNodeIds.length ||
2381
+ handleChanged.length) {
2382
+ // eslint-disable-next-line no-console
2383
+ console.debug("[WorkbenchCanvas] node updates", {
2384
+ added: addedNodeIds,
2385
+ removed: removedNodeIds,
2386
+ changed: changedNodeIds,
2387
+ handleChanged,
2388
+ });
2389
+ }
2390
+ if (addedEdgeIds.length ||
2391
+ removedEdgeIds.length ||
2392
+ changedEdgeIds.length) {
2393
+ // eslint-disable-next-line no-console
2394
+ console.debug("[WorkbenchCanvas] edge updates", {
2395
+ added: addedEdgeIds,
2396
+ removed: removedEdgeIds,
2397
+ changed: changedEdgeIds,
2398
+ });
2399
+ }
2400
+ }
2401
+ catch { }
2402
+ prevNodesRef.current = stableNodes;
2403
+ prevEdgesRef.current = stableEdges;
2404
+ return { nodes: stableNodes, edges: stableEdges };
2054
2405
  }, [
2055
2406
  showValues,
2056
2407
  inputsMap,
@@ -2062,9 +2413,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
2062
2413
  edgeStatus,
2063
2414
  nodeValidation,
2064
2415
  edgeValidation,
2065
- nodeTypes,
2066
2416
  resolveNodeType,
2067
2417
  ]);
2418
+ const throttled = useThrottledValue({ nodes, edges }, 100);
2068
2419
  const [menuOpen, setMenuOpen] = React.useState(false);
2069
2420
  const [menuPos, setMenuPos] = React.useState(null);
2070
2421
  const [nodeMenuOpen, setNodeMenuOpen] = React.useState(false);
@@ -2091,7 +2442,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
2091
2442
  const addNodeAt = (typeId, pos) => {
2092
2443
  wb.addNode({ typeId, position: pos });
2093
2444
  };
2094
- return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(react$1.ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onSelectionChange: onSelectionChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, onInit: (inst) => (rfInstanceRef.current = inst), children: [jsxRuntime.jsx(react$1.Background, {}), jsxRuntime.jsx(react$1.MiniMap, {}), jsxRuntime.jsx(react$1.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
2445
+ return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.jsxs(react.ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, onlyRenderVisibleElements: true, selectionOnDrag: true, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, deleteKeyCode: ["Backspace", "Delete"], fitView: true, onInit: (inst) => (rfInstanceRef.current = inst), children: [jsxRuntime.jsx(react.Background, {}), jsxRuntime.jsx(react.MiniMap, {}), jsxRuntime.jsx(react.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
2095
2446
  });
2096
2447
 
2097
2448
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -2203,9 +2554,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2203
2554
  if (!ex)
2204
2555
  return;
2205
2556
  const { registry: r, def } = await ex.load();
2206
- if (r) {
2207
- setRegistry(r);
2208
- wb.setRegistry(r);
2557
+ // Keep registry consistent with backend:
2558
+ // - For local backend, allow example to provide its own registry
2559
+ // - For remote backend, NEVER overwrite the hydrated remote registry
2560
+ if (backendKind === "local") {
2561
+ if (r) {
2562
+ setRegistry(r);
2563
+ wb.setRegistry(r);
2564
+ }
2209
2565
  }
2210
2566
  await wb.load(def);
2211
2567
  // Build a local runtime so seeded defaults are visible pre-run
@@ -2213,7 +2569,15 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2213
2569
  runAutoLayout();
2214
2570
  setExampleState(key);
2215
2571
  onExampleChange?.(key);
2216
- }, [runner, wb, onExampleChange, runAutoLayout, examples, setRegistry]);
2572
+ }, [
2573
+ runner,
2574
+ wb,
2575
+ onExampleChange,
2576
+ runAutoLayout,
2577
+ examples,
2578
+ setRegistry,
2579
+ backendKind,
2580
+ ]);
2217
2581
  const downloadGraph = React.useCallback(() => {
2218
2582
  try {
2219
2583
  const def = wb.export();
@@ -2515,11 +2879,11 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2515
2879
  return overrides.toElement(baseToElement, { registry });
2516
2880
  return baseToElement;
2517
2881
  }, [overrides, baseToElement, registry]);
2518
- return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2882
+ return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsxs("span", { className: "ml-2 flex items-center gap-1 text-xs", title: transportStatus.kind || undefined, children: [transportStatus.state === "local" && (jsxRuntime.jsx(react$1.PlugsConnectedIcon, { size: 14, className: "text-gray-500" })), transportStatus.state === "connecting" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-600 animate-pulse" })), transportStatus.state === "connected" && (jsxRuntime.jsx(react$1.WifiHighIcon, { size: 14, className: "text-green-600" })), transportStatus.state === "disconnected" && (jsxRuntime.jsx(react$1.WifiSlashIcon, { size: 14, className: "text-red-600" })), transportStatus.state === "retrying" && (jsxRuntime.jsx(react$1.ClockClockwiseIcon, { size: 14, className: "text-amber-700 animate-pulse" }))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2519
2883
  ? "Stop engine before switching example"
2520
2884
  : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2521
2885
  ? "Stop engine before switching backend"
2522
- : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
2886
+ : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && !!onHttpBaseUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && !!onWsUrlChange && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
2523
2887
  const kind = e.target.value || undefined;
2524
2888
  onEngineChange?.(kind);
2525
2889
  }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => {
@@ -2568,6 +2932,7 @@ exports.DefaultUIExtensionRegistry = DefaultUIExtensionRegistry;
2568
2932
  exports.InMemoryWorkbench = InMemoryWorkbench;
2569
2933
  exports.Inspector = Inspector;
2570
2934
  exports.LocalGraphRunner = LocalGraphRunner;
2935
+ exports.NodeHandles = NodeHandles;
2571
2936
  exports.RemoteGraphRunner = RemoteGraphRunner;
2572
2937
  exports.WorkbenchCanvas = WorkbenchCanvas;
2573
2938
  exports.WorkbenchContext = WorkbenchContext;
@@ -2582,6 +2947,7 @@ exports.summarizeDeep = summarizeDeep;
2582
2947
  exports.toReactFlow = toReactFlow;
2583
2948
  exports.useQueryParamBoolean = useQueryParamBoolean;
2584
2949
  exports.useQueryParamString = useQueryParamString;
2950
+ exports.useThrottledValue = useThrottledValue;
2585
2951
  exports.useWorkbenchBridge = useWorkbenchBridge;
2586
2952
  exports.useWorkbenchContext = useWorkbenchContext;
2587
2953
  exports.useWorkbenchGraphTick = useWorkbenchGraphTick;