@bian-womp/spark-workbench 0.2.27 → 0.2.29

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 (31) hide show
  1. package/lib/cjs/index.cjs +111 -49
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/misc/DebugEvents.d.ts.map +1 -1
  4. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  6. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +1 -0
  9. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts +2 -1
  11. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/value.d.ts +6 -0
  13. package/lib/cjs/src/misc/value.d.ts.map +1 -1
  14. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  15. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  16. package/lib/esm/index.js +112 -51
  17. package/lib/esm/index.js.map +1 -1
  18. package/lib/esm/src/misc/DebugEvents.d.ts.map +1 -1
  19. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  20. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  21. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  22. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  23. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +1 -0
  24. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  25. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts +2 -1
  26. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  27. package/lib/esm/src/misc/value.d.ts +6 -0
  28. package/lib/esm/src/misc/value.d.ts.map +1 -1
  29. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  30. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  31. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -524,7 +524,16 @@ class LocalGraphRunner extends AbstractGraphRunner {
524
524
  const runtimeInputs = this.runtime
525
525
  ? this.runtime.getNodeData?.(n.nodeId)?.inputs ?? {}
526
526
  : {};
527
- const merged = { ...runtimeInputs, ...staged };
527
+ // Build inbound handle set for this node from current def
528
+ const inbound = new Set(def.edges
529
+ .filter((e) => e.target.nodeId === n.nodeId)
530
+ .map((e) => e.target.handle));
531
+ // Merge staged only for non-inbound handles so UI reflects runtime values for wired inputs
532
+ const merged = { ...runtimeInputs };
533
+ for (const [h, v] of Object.entries(staged)) {
534
+ if (!inbound.has(h))
535
+ merged[h] = v;
536
+ }
528
537
  if (Object.keys(merged).length > 0)
529
538
  out[n.nodeId] = merged;
530
539
  }
@@ -795,12 +804,21 @@ class RemoteGraphRunner extends AbstractGraphRunner {
795
804
  const desc = this.registry.nodes.get(n.typeId);
796
805
  const handles = Object.keys(resolved ?? desc?.inputs ?? {});
797
806
  const cur = {};
807
+ // Build inbound handle set for this node to honor wiring precedence
808
+ const inbound = new Set(def.edges
809
+ .filter((e) => e.target.nodeId === n.nodeId)
810
+ .map((e) => e.target.handle));
798
811
  for (const h of handles) {
799
812
  const rec = cache.get(`${n.nodeId}.${h}`);
800
813
  if (rec && rec.io === "input")
801
814
  cur[h] = rec.value;
802
815
  }
803
- const merged = { ...cur, ...staged };
816
+ // Merge staged only for non-inbound handles so UI doesn't override runtime values
817
+ const merged = { ...cur };
818
+ for (const [h, v] of Object.entries(staged)) {
819
+ if (!inbound.has(h))
820
+ merged[h] = v;
821
+ }
804
822
  if (Object.keys(merged).length > 0)
805
823
  out[n.nodeId] = merged;
806
824
  }
@@ -1264,6 +1282,23 @@ function formatDeclaredTypeSignature(declared) {
1264
1282
  return declared.join(" | ");
1265
1283
  return declared ?? "";
1266
1284
  }
1285
+ /**
1286
+ * Formats a handle ID for display in the UI.
1287
+ * For handles with format "prefix:middle:suffix:extra" (4 parts), displays only the middle part.
1288
+ * Otherwise returns the handle ID as-is.
1289
+ */
1290
+ function prettyHandle(id) {
1291
+ try {
1292
+ const parts = String(id).split(":");
1293
+ // If there are exactly 3 colons (4 parts), display only the second part
1294
+ if (parts.length === 4)
1295
+ return parts[1] || id;
1296
+ return id;
1297
+ }
1298
+ catch {
1299
+ return id;
1300
+ }
1301
+ }
1267
1302
  // Pre-format common structures for display; return undefined to defer to caller
1268
1303
  function preformatValueForDisplay(typeId, value, registry) {
1269
1304
  if (value === undefined || value === null)
@@ -1642,7 +1677,7 @@ function useWorkbenchContext() {
1642
1677
  return ctx;
1643
1678
  }
1644
1679
 
1645
- function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, children, }) {
1680
+ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVersion, children, }) {
1646
1681
  const [nodeStatus, setNodeStatus] = React.useState({});
1647
1682
  const [edgeStatus, setEdgeStatus] = React.useState({});
1648
1683
  const [events, setEvents] = React.useState([]);
@@ -2184,6 +2219,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2184
2219
  runAutoLayout,
2185
2220
  updateEdgeType,
2186
2221
  triggerExternal,
2222
+ uiVersion,
2187
2223
  }), [
2188
2224
  wb,
2189
2225
  runner,
@@ -2218,6 +2254,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2218
2254
  runAutoLayout,
2219
2255
  wb,
2220
2256
  runner,
2257
+ uiVersion,
2221
2258
  ]);
2222
2259
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
2223
2260
  }
@@ -2230,6 +2267,7 @@ function IssueBadge({ level, title, size = 12, className, }) {
2230
2267
  function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
2231
2268
  const { events, clearEvents } = useWorkbenchContext();
2232
2269
  const scrollRef = React.useRef(null);
2270
+ const [copied, setCopied] = React.useState(false);
2233
2271
  const rows = React.useMemo(() => {
2234
2272
  const filtered = hideWorkbench
2235
2273
  ? events.filter((e) => e.source !== "workbench")
@@ -2253,7 +2291,25 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
2253
2291
  return String(v);
2254
2292
  }
2255
2293
  };
2256
- 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}`))) })] }));
2294
+ const handleCopyLogs = async () => {
2295
+ try {
2296
+ const formattedEvents = rows.map((ev) => ({
2297
+ no: ev.no,
2298
+ at: ev.at,
2299
+ source: ev.source,
2300
+ type: ev.type,
2301
+ payload: summarizeDeep(ev.payload),
2302
+ }));
2303
+ const jsonString = JSON.stringify(formattedEvents, null, 2);
2304
+ await navigator.clipboard.writeText(jsonString);
2305
+ setCopied(true);
2306
+ setTimeout(() => setCopied(false), 2000);
2307
+ }
2308
+ catch (err) {
2309
+ console.error("Failed to copy logs:", err);
2310
+ }
2311
+ };
2312
+ 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: handleCopyLogs, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: copied ? "Copied!" : "Copy logs as formatted JSON", children: jsxRuntime.jsx(react$1.CopyIcon, { size: 14 }) }), jsxRuntime.jsx("button", { onClick: clearEvents, className: "p-2 border border-gray-300 rounded flex items-center justify-center", title: "Clear all events", children: jsxRuntime.jsx(react$1.TrashIcon, { size: 14 }) })] })] }), 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}`))) })] }));
2257
2313
  }
2258
2314
 
2259
2315
  function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
@@ -2275,13 +2331,17 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2275
2331
  const globalValidationIssues = validationGlobal;
2276
2332
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
2277
2333
  const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
2278
- const selectedDesc = selectedNode
2334
+ selectedNode
2279
2335
  ? registry.nodes.get(selectedNode.typeId)
2280
2336
  : undefined;
2281
- const inputHandles = Object.entries(selectedDesc?.inputs ?? {})
2282
- .filter(([k]) => !sparkGraph.isInputPrivate(selectedDesc?.inputs, k))
2337
+ // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
2338
+ const effectiveHandles = selectedNode
2339
+ ? computeEffectiveHandles(selectedNode, registry)
2340
+ : { inputs: {}, outputs: {}};
2341
+ const inputHandles = Object.entries(effectiveHandles.inputs)
2342
+ .filter(([k]) => !sparkGraph.isInputPrivate(effectiveHandles.inputs, k))
2283
2343
  .map(([k]) => k);
2284
- const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
2344
+ const outputHandles = Object.keys(effectiveHandles.outputs);
2285
2345
  const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
2286
2346
  const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
2287
2347
  const selectedNodeStatus = selectedNodeId
@@ -2322,12 +2382,11 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2322
2382
  }
2323
2383
  return;
2324
2384
  }
2325
- const desc = selectedDesc;
2326
- const handles = Object.keys(desc?.inputs ?? {});
2385
+ const handles = Object.keys(effectiveHandles.inputs);
2327
2386
  const nextDrafts = { ...drafts };
2328
2387
  const nextOriginals = { ...originals };
2329
2388
  for (const h of handles) {
2330
- const typeId = sparkGraph.getInputTypeId(desc?.inputs, h);
2389
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2331
2390
  const current = nodeInputs[h];
2332
2391
  const display = safeToString(typeId, current);
2333
2392
  const wasOriginal = originals[h];
@@ -2343,7 +2402,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2343
2402
  setDrafts(nextDrafts);
2344
2403
  if (!shallowEqual(originals, nextOriginals))
2345
2404
  setOriginals(nextOriginals);
2346
- }, [selectedNodeId, selectedDesc, valuesTick]);
2405
+ }, [selectedNodeId, selectedNode, registry, valuesTick]);
2347
2406
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
2348
2407
  const { wb } = useWorkbenchContext();
2349
2408
  const deleteEdgeById = (edgeId) => {
@@ -2369,7 +2428,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2369
2428
  deleteEdgeById(selectedEdge.id);
2370
2429
  }, title: "Delete this edge", children: "Delete edge" })] }, 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 ??
2371
2430
  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) => {
2372
- const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
2431
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2373
2432
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
2374
2433
  e.target.handle === h);
2375
2434
  const commonProps = {
@@ -2397,7 +2456,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2397
2456
  const title = inIssues
2398
2457
  .map((v) => `${v.code}: ${v.message}`)
2399
2458
  .join("; ");
2400
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: h }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (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: current !== undefined && current !== null
2459
+ return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: typeId })] }), hasValidation && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 24, className: "ml-1 w-6 h-6", title: title })), isEnum ? (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: current !== undefined && current !== null
2401
2460
  ? String(current)
2402
2461
  : "", onChange: (e) => {
2403
2462
  const val = e.target.value;
@@ -2413,8 +2472,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2413
2472
  if (e.key === "Escape")
2414
2473
  revert();
2415
2474
  }, ...commonProps }))] }, h));
2416
- }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: h }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1", children: (() => {
2417
- const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], selectedDesc?.outputs?.[h]);
2475
+ }))] }), jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Outputs" }), outputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No outputs" })) : (outputHandles.map((h) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-20 flex flex-col", children: [jsxRuntime.jsx("span", { children: prettyHandle(h) }), jsxRuntime.jsx("span", { className: "text-gray-500 text-[11px]", children: outputTypesMap[selectedNodeId]?.[h] ?? "" })] }), jsxRuntime.jsx("div", { className: "flex-1", children: (() => {
2476
+ const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2418
2477
  return toElement(typeId, value);
2419
2478
  })() }), (() => {
2420
2479
  const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
@@ -2558,18 +2617,6 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
2558
2617
  }
2559
2618
  function DefaultNodeContent({ data, isConnectable, }) {
2560
2619
  const { showValues, inputValues, outputValues, toString } = data;
2561
- const prettyHandle = React.useCallback((id) => {
2562
- try {
2563
- const parts = String(id).split(":");
2564
- // If there are exactly 3 colons (4 parts), display only the second part
2565
- if (parts.length === 4)
2566
- return parts[1] || id;
2567
- return id;
2568
- }
2569
- catch {
2570
- return id;
2571
- }
2572
- }, []);
2573
2620
  const inputEntries = data.inputHandles ?? [];
2574
2621
  const outputEntries = data.outputHandles ?? [];
2575
2622
  const status = data.status ?? { activeRuns: 0 };
@@ -2902,7 +2949,7 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
2902
2949
  }
2903
2950
 
2904
2951
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
2905
- const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
2952
+ const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, } = useWorkbenchContext();
2906
2953
  const nodeValidation = validationByNode;
2907
2954
  const edgeValidation = validationByEdge.errors;
2908
2955
  // Keep stable references for nodes/edges to avoid unnecessary updates
@@ -2972,8 +3019,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
2972
3019
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
2973
3020
  // Build nodeTypes map using UI extension registry
2974
3021
  const ui = wb.getUI();
2975
- const custom = new Map();
2976
- for (const typeId of Array.from(registry.nodes.keys())) {
3022
+ const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
3023
+ const def = wb.export();
3024
+ const ids = new Set([
3025
+ ...Array.from(registry.nodes.keys()),
3026
+ ...def.nodes.map((n) => n.typeId),
3027
+ ]);
3028
+ for (const typeId of ids) {
2977
3029
  const renderer = ui.getNodeRenderer(typeId);
2978
3030
  if (renderer)
2979
3031
  custom.set(typeId, renderer);
@@ -2987,8 +3039,8 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
2987
3039
  }
2988
3040
  const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
2989
3041
  return { nodeTypes: types, resolveNodeType: resolver };
2990
- // registry is stable; ui renderers expected to be set up before mount
2991
- }, [wb, registry]);
3042
+ // Include uiVersion to recompute when custom renderers are registered
3043
+ }, [wb, registry, uiVersion]);
2992
3044
  const { nodes, edges } = React.useMemo(() => {
2993
3045
  const def = wb.export();
2994
3046
  const sel = wb.getSelection();
@@ -3028,7 +3080,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3028
3080
  })
3029
3081
  .map((n) => n.id);
3030
3082
  // Detect handle updates (ids/length changes) for targeted debug
3031
- const toIds = (arr) => Array.isArray(arr) ? arr.map((h) => (h && typeof h === "object" && "id" in h ? String(h.id) : "")).filter(Boolean) : [];
3083
+ const toIds = (arr) => Array.isArray(arr)
3084
+ ? arr
3085
+ .map((h) => h && typeof h === "object" && "id" in h ? String(h.id) : "")
3086
+ .filter(Boolean)
3087
+ : [];
3032
3088
  const handlesEqual = (a, b) => {
3033
3089
  const aIds = toIds(a);
3034
3090
  const bIds = toIds(b);
@@ -3654,15 +3710,6 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3654
3710
  // Store previous runner for cleanup
3655
3711
  const prevRunnerRef = React.useRef(null);
3656
3712
  const runner = React.useMemo(() => {
3657
- // Dispose previous runner if it exists
3658
- if (prevRunnerRef.current) {
3659
- try {
3660
- prevRunnerRef.current.dispose();
3661
- }
3662
- catch (err) {
3663
- console.warn("Error disposing previous runner:", err);
3664
- }
3665
- }
3666
3713
  let newRunner;
3667
3714
  if (backendKind === "remote-http") {
3668
3715
  const backend = {
@@ -3693,29 +3740,43 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3693
3740
  else {
3694
3741
  newRunner = new LocalGraphRunner(registry);
3695
3742
  }
3696
- prevRunnerRef.current = newRunner;
3697
3743
  return newRunner;
3698
3744
  }, [registry, backendKind, httpBaseUrl, wsUrl, backendOptions]);
3699
- // Cleanup runner on unmount
3745
+ // Dispose previous runner after commit; dispose current on unmount
3700
3746
  React.useEffect(() => {
3747
+ const previous = prevRunnerRef.current;
3748
+ prevRunnerRef.current = runner;
3749
+ if (previous && previous !== runner) {
3750
+ try {
3751
+ previous.dispose();
3752
+ }
3753
+ catch (err) {
3754
+ console.warn("Error disposing previous runner:", err);
3755
+ }
3756
+ }
3701
3757
  return () => {
3702
- if (prevRunnerRef.current) {
3758
+ // Only dispose if this runner is still the current one
3759
+ if (prevRunnerRef.current === runner) {
3703
3760
  try {
3704
- prevRunnerRef.current.dispose();
3761
+ runner.dispose();
3705
3762
  }
3706
3763
  catch (err) {
3707
3764
  console.warn("Error disposing runner on unmount:", err);
3708
3765
  }
3709
3766
  }
3710
3767
  };
3711
- }, []);
3768
+ }, [runner]);
3769
+ // Track UI registration version to trigger nodeTypes recomputation
3770
+ const [uiVersion, setUiVersion] = React.useState(0);
3712
3771
  // Allow external UI registration (e.g., node renderers) with access to wb
3713
3772
  React.useEffect(() => {
3714
3773
  const baseRegisterUI = (_wb) => { };
3715
3774
  overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
3775
+ // Increment UI version to trigger nodeTypes recomputation in WorkbenchCanvas
3776
+ setUiVersion((v) => v + 1);
3716
3777
  // eslint-disable-next-line react-hooks/exhaustive-deps
3717
3778
  }, [wb, runner, overrides]);
3718
- return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
3779
+ return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
3719
3780
  if (runner.isRunning())
3720
3781
  runner.dispose();
3721
3782
  onBackendKindChange(v);
@@ -3741,6 +3802,7 @@ exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
3741
3802
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
3742
3803
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
3743
3804
  exports.preformatValueForDisplay = preformatValueForDisplay;
3805
+ exports.prettyHandle = prettyHandle;
3744
3806
  exports.resolveOutputDisplay = resolveOutputDisplay;
3745
3807
  exports.summarizeDeep = summarizeDeep;
3746
3808
  exports.toReactFlow = toReactFlow;