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