@bian-womp/spark-workbench 0.2.26 → 0.2.28

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.
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)
@@ -1647,6 +1682,16 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1647
1682
  const [edgeStatus, setEdgeStatus] = React.useState({});
1648
1683
  const [events, setEvents] = React.useState([]);
1649
1684
  const clearEvents = React.useCallback(() => setEvents([]), []);
1685
+ const [systemErrors, setSystemErrors] = React.useState([]);
1686
+ const [registryErrors, setRegistryErrors] = React.useState([]);
1687
+ const clearSystemErrors = React.useCallback(() => setSystemErrors([]), []);
1688
+ const clearRegistryErrors = React.useCallback(() => setRegistryErrors([]), []);
1689
+ const removeSystemError = React.useCallback((index) => {
1690
+ setSystemErrors((prev) => prev.filter((_, idx) => idx !== index));
1691
+ }, []);
1692
+ const removeRegistryError = React.useCallback((index) => {
1693
+ setRegistryErrors((prev) => prev.filter((_, idx) => idx !== index));
1694
+ }, []);
1650
1695
  // Fallback progress animation: drive progress to 100% over ~2 minutes
1651
1696
  const FALLBACK_TOTAL_MS = 2 * 60 * 1000;
1652
1697
  const [fallbackStarts, setFallbackStarts] = React.useState({});
@@ -1829,7 +1874,10 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1829
1874
  if (remoteDef && Array.isArray(remoteDef.nodes)) {
1830
1875
  // Mutate current def in-place to avoid emitting graphChanged and causing update loop
1831
1876
  const cur = wb.export();
1832
- const byId = new Map((remoteDef.nodes || []).map((n) => [n.nodeId, n]));
1877
+ const byId = new Map((remoteDef.nodes || []).map((n) => [
1878
+ n.nodeId,
1879
+ n,
1880
+ ]));
1833
1881
  let changed = false;
1834
1882
  for (const n of cur.nodes) {
1835
1883
  const rn = byId.get(n.nodeId);
@@ -1861,6 +1909,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1861
1909
  const off2 = runner.on("error", (e) => {
1862
1910
  const edgeError = e;
1863
1911
  const nodeError = e;
1912
+ const registryError = e;
1913
+ const systemError = e;
1864
1914
  if (edgeError.kind === "edge-convert") {
1865
1915
  const edgeId = edgeError.edgeId;
1866
1916
  setEdgeStatus((s) => ({
@@ -1868,7 +1918,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1868
1918
  [edgeId]: { ...s[edgeId], lastError: edgeError.err },
1869
1919
  }));
1870
1920
  }
1871
- else if (nodeError.nodeId) {
1921
+ else if (nodeError.kind === "node-run" && nodeError.nodeId) {
1872
1922
  const nodeId = nodeError.nodeId;
1873
1923
  const runId = nodeError.runId;
1874
1924
  setNodeStatus((s) => ({
@@ -1886,6 +1936,27 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
1886
1936
  };
1887
1937
  }
1888
1938
  }
1939
+ else if (registryError.kind === "registry") {
1940
+ // Track registry errors for UI display
1941
+ setRegistryErrors((prev) => {
1942
+ // Avoid duplicates by checking message
1943
+ if (prev.some((err) => err.message === registryError.message)) {
1944
+ return prev;
1945
+ }
1946
+ return [...prev, registryError];
1947
+ });
1948
+ }
1949
+ else if (systemError.kind === "system") {
1950
+ // Track custom errors for UI display
1951
+ setSystemErrors((prev) => {
1952
+ // Avoid duplicates by checking message and code
1953
+ if (prev.some((err) => err.message === systemError.message &&
1954
+ err.code === systemError.code)) {
1955
+ return prev;
1956
+ }
1957
+ return [...prev, systemError];
1958
+ });
1959
+ }
1889
1960
  return add("runner", "error")(e);
1890
1961
  });
1891
1962
  const off3 = runner.on("invalidate", (e) => {
@@ -2133,6 +2204,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2133
2204
  validationGlobal,
2134
2205
  events,
2135
2206
  clearEvents,
2207
+ systemErrors,
2208
+ registryErrors,
2209
+ clearSystemErrors,
2210
+ clearRegistryErrors,
2211
+ removeSystemError,
2212
+ removeRegistryError,
2136
2213
  isRunning,
2137
2214
  engineKind,
2138
2215
  start,
@@ -2154,6 +2231,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, child
2154
2231
  nodeStatus,
2155
2232
  edgeStatus,
2156
2233
  valuesTick,
2234
+ systemErrors,
2235
+ registryErrors,
2236
+ clearSystemErrors,
2237
+ clearRegistryErrors,
2238
+ removeSystemError,
2239
+ removeRegistryError,
2157
2240
  inputsMap,
2158
2241
  outputsMap,
2159
2242
  validationByNode,
@@ -2182,6 +2265,7 @@ function IssueBadge({ level, title, size = 12, className, }) {
2182
2265
  function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWorkbenchChange, }) {
2183
2266
  const { events, clearEvents } = useWorkbenchContext();
2184
2267
  const scrollRef = React.useRef(null);
2268
+ const [copied, setCopied] = React.useState(false);
2185
2269
  const rows = React.useMemo(() => {
2186
2270
  const filtered = hideWorkbench
2187
2271
  ? events.filter((e) => e.source !== "workbench")
@@ -2205,7 +2289,25 @@ function DebugEvents({ autoScroll, onAutoScrollChange, hideWorkbench, onHideWork
2205
2289
  return String(v);
2206
2290
  }
2207
2291
  };
2208
- 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}`))) })] }));
2292
+ const handleCopyLogs = async () => {
2293
+ try {
2294
+ const formattedEvents = rows.map((ev) => ({
2295
+ no: ev.no,
2296
+ at: ev.at,
2297
+ source: ev.source,
2298
+ type: ev.type,
2299
+ payload: summarizeDeep(ev.payload),
2300
+ }));
2301
+ const jsonString = JSON.stringify(formattedEvents, null, 2);
2302
+ await navigator.clipboard.writeText(jsonString);
2303
+ setCopied(true);
2304
+ setTimeout(() => setCopied(false), 2000);
2305
+ }
2306
+ catch (err) {
2307
+ console.error("Failed to copy logs:", err);
2308
+ }
2309
+ };
2310
+ 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}`))) })] }));
2209
2311
  }
2210
2312
 
2211
2313
  function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHideWorkbenchChange, toString, toElement, contextPanel, setInput, }) {
@@ -2220,20 +2322,24 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2220
2322
  return String(value ?? "");
2221
2323
  }
2222
2324
  };
2223
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, } = useWorkbenchContext();
2325
+ const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, outputsMap, outputTypesMap, nodeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, clearSystemErrors, clearRegistryErrors, removeSystemError, removeRegistryError, } = useWorkbenchContext();
2224
2326
  const nodeValidationIssues = validationByNode.issues;
2225
2327
  const edgeValidationIssues = validationByEdge.issues;
2226
2328
  const nodeValidationHandles = validationByNode;
2227
2329
  const globalValidationIssues = validationGlobal;
2228
2330
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
2229
2331
  const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
2230
- const selectedDesc = selectedNode
2332
+ selectedNode
2231
2333
  ? registry.nodes.get(selectedNode.typeId)
2232
2334
  : undefined;
2233
- const inputHandles = Object.entries(selectedDesc?.inputs ?? {})
2234
- .filter(([k]) => !sparkGraph.isInputPrivate(selectedDesc?.inputs, k))
2335
+ // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
2336
+ const effectiveHandles = selectedNode
2337
+ ? computeEffectiveHandles(selectedNode, registry)
2338
+ : { inputs: {}, outputs: {}};
2339
+ const inputHandles = Object.entries(effectiveHandles.inputs)
2340
+ .filter(([k]) => !sparkGraph.isInputPrivate(effectiveHandles.inputs, k))
2235
2341
  .map(([k]) => k);
2236
- const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
2342
+ const outputHandles = Object.keys(effectiveHandles.outputs);
2237
2343
  const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
2238
2344
  const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
2239
2345
  const selectedNodeStatus = selectedNodeId
@@ -2274,12 +2380,11 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2274
2380
  }
2275
2381
  return;
2276
2382
  }
2277
- const desc = selectedDesc;
2278
- const handles = Object.keys(desc?.inputs ?? {});
2383
+ const handles = Object.keys(effectiveHandles.inputs);
2279
2384
  const nextDrafts = { ...drafts };
2280
2385
  const nextOriginals = { ...originals };
2281
2386
  for (const h of handles) {
2282
- const typeId = sparkGraph.getInputTypeId(desc?.inputs, h);
2387
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2283
2388
  const current = nodeInputs[h];
2284
2389
  const display = safeToString(typeId, current);
2285
2390
  const wasOriginal = originals[h];
@@ -2295,7 +2400,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2295
2400
  setDrafts(nextDrafts);
2296
2401
  if (!shallowEqual(originals, nextOriginals))
2297
2402
  setOriginals(nextOriginals);
2298
- }, [selectedNodeId, selectedDesc, valuesTick]);
2403
+ }, [selectedNodeId, selectedNode, registry, valuesTick]);
2299
2404
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
2300
2405
  const { wb } = useWorkbenchContext();
2301
2406
  const deleteEdgeById = (edgeId) => {
@@ -2306,7 +2411,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2306
2411
  }
2307
2412
  catch { }
2308
2413
  };
2309
- 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}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2414
+ 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 }), systemErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [systemErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: err.code ? `Error ${err.code}` : "Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message })] }), jsxRuntime.jsx("button", { className: "text-red-500 hover:text-red-700 text-[10px] px-1", onClick: () => removeSystemError(i), title: "Dismiss", children: "\u00D7" })] }, i))), systemErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-red-600 hover:text-red-800 underline", onClick: clearSystemErrors, children: "Clear all" }))] })), registryErrors.length > 0 && (jsxRuntime.jsxs("div", { className: "mb-2 space-y-1", children: [registryErrors.map((err, i) => (jsxRuntime.jsxs("div", { className: "text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 flex items-start justify-between gap-2", children: [jsxRuntime.jsxs("div", { className: "flex-1", children: [jsxRuntime.jsx("div", { className: "font-semibold", children: "Registry Error" }), jsxRuntime.jsx("div", { className: "break-words", children: err.message }), err.attempt && err.maxAttempts && (jsxRuntime.jsxs("div", { className: "text-[10px] text-amber-600 mt-1", children: ["Attempt ", err.attempt, " of ", err.maxAttempts] }))] }), jsxRuntime.jsx("button", { className: "text-amber-500 hover:text-amber-700 text-[10px] px-1", onClick: () => removeRegistryError(i), title: "Dismiss", children: "\u00D7" })] }, i))), registryErrors.length > 1 && (jsxRuntime.jsx("button", { className: "text-xs text-amber-600 hover:text-amber-800 underline", onClick: clearRegistryErrors, children: "Clear all" }))] })), 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}` }), !!m.data?.edgeId && (jsxRuntime.jsx("button", { className: "ml-2 text-[10px] px-1 py-[2px] border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
2310
2415
  e.stopPropagation();
2311
2416
  deleteEdgeById(m.data?.edgeId);
2312
2417
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, 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.jsx("div", { className: "mt-1", children: jsxRuntime.jsx("button", { className: "text-xs px-2 py-1 border border-red-300 rounded text-red-700 hover:bg-red-50", onClick: (e) => {
@@ -2321,7 +2426,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2321
2426
  deleteEdgeById(selectedEdge.id);
2322
2427
  }, 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 ??
2323
2428
  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) => {
2324
- const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
2429
+ const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
2325
2430
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
2326
2431
  e.target.handle === h);
2327
2432
  const commonProps = {
@@ -2349,7 +2454,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2349
2454
  const title = inIssues
2350
2455
  .map((v) => `${v.code}: ${v.message}`)
2351
2456
  .join("; ");
2352
- 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
2457
+ 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
2353
2458
  ? String(current)
2354
2459
  : "", onChange: (e) => {
2355
2460
  const val = e.target.value;
@@ -2365,8 +2470,8 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
2365
2470
  if (e.key === "Escape")
2366
2471
  revert();
2367
2472
  }, ...commonProps }))] }, h));
2368
- }))] }), 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: (() => {
2369
- const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], selectedDesc?.outputs?.[h]);
2473
+ }))] }), 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: (() => {
2474
+ const { typeId, value } = resolveOutputDisplay(nodeOutputs[h], effectiveHandles.outputs[h]);
2370
2475
  return toElement(typeId, value);
2371
2476
  })() }), (() => {
2372
2477
  const outIssues = selectedNodeHandleValidation.outputs.filter((m) => m.handle === h);
@@ -2510,18 +2615,6 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
2510
2615
  }
2511
2616
  function DefaultNodeContent({ data, isConnectable, }) {
2512
2617
  const { showValues, inputValues, outputValues, toString } = data;
2513
- const prettyHandle = React.useCallback((id) => {
2514
- try {
2515
- const parts = String(id).split(":");
2516
- // If there are exactly 3 colons (4 parts), display only the second part
2517
- if (parts.length === 4)
2518
- return parts[1] || id;
2519
- return id;
2520
- }
2521
- catch {
2522
- return id;
2523
- }
2524
- }, []);
2525
2618
  const inputEntries = data.inputHandles ?? [];
2526
2619
  const outputEntries = data.outputHandles ?? [];
2527
2620
  const status = data.status ?? { activeRuns: 0 };
@@ -3156,11 +3249,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3156
3249
  return backendKind === "local";
3157
3250
  });
3158
3251
  // Expose init callback with setInitialGraph helper
3159
- const initCalled = React.useRef(false);
3252
+ // Note: This runs whenever runner changes (e.g., when Flow is enabled and backendOptions changes)
3160
3253
  React.useEffect(() => {
3161
- if (initCalled.current)
3162
- return;
3163
- initCalled.current = true;
3164
3254
  if (!onInit)
3165
3255
  return;
3166
3256
  const setInitialGraph = async (d, inputs) => {
@@ -3606,7 +3696,19 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
3606
3696
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, backendOptions, overrides, onInit, onChange, }) {
3607
3697
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
3608
3698
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
3699
+ // Store previous runner for cleanup
3700
+ const prevRunnerRef = React.useRef(null);
3609
3701
  const runner = React.useMemo(() => {
3702
+ // Dispose previous runner if it exists
3703
+ if (prevRunnerRef.current) {
3704
+ try {
3705
+ prevRunnerRef.current.dispose();
3706
+ }
3707
+ catch (err) {
3708
+ console.warn("Error disposing previous runner:", err);
3709
+ }
3710
+ }
3711
+ let newRunner;
3610
3712
  if (backendKind === "remote-http") {
3611
3713
  const backend = {
3612
3714
  kind: "remote-http",
@@ -3618,9 +3720,9 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3618
3720
  onCustomEvent: backendOptions.onCustomEvent,
3619
3721
  }),
3620
3722
  };
3621
- return new RemoteGraphRunner(registry, backend);
3723
+ newRunner = new RemoteGraphRunner(registry, backend);
3622
3724
  }
3623
- if (backendKind === "remote-ws") {
3725
+ else if (backendKind === "remote-ws") {
3624
3726
  const backend = {
3625
3727
  kind: "remote-ws",
3626
3728
  url: wsUrl,
@@ -3631,10 +3733,27 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
3631
3733
  onCustomEvent: backendOptions.onCustomEvent,
3632
3734
  }),
3633
3735
  };
3634
- return new RemoteGraphRunner(registry, backend);
3736
+ newRunner = new RemoteGraphRunner(registry, backend);
3635
3737
  }
3636
- return new LocalGraphRunner(registry);
3738
+ else {
3739
+ newRunner = new LocalGraphRunner(registry);
3740
+ }
3741
+ prevRunnerRef.current = newRunner;
3742
+ return newRunner;
3637
3743
  }, [registry, backendKind, httpBaseUrl, wsUrl, backendOptions]);
3744
+ // Cleanup runner on unmount
3745
+ React.useEffect(() => {
3746
+ return () => {
3747
+ if (prevRunnerRef.current) {
3748
+ try {
3749
+ prevRunnerRef.current.dispose();
3750
+ }
3751
+ catch (err) {
3752
+ console.warn("Error disposing runner on unmount:", err);
3753
+ }
3754
+ }
3755
+ };
3756
+ }, []);
3638
3757
  // Allow external UI registration (e.g., node renderers) with access to wb
3639
3758
  React.useEffect(() => {
3640
3759
  const baseRegisterUI = (_wb) => { };
@@ -3667,6 +3786,7 @@ exports.formatDataUrlAsLabel = formatDataUrlAsLabel;
3667
3786
  exports.formatDeclaredTypeSignature = formatDeclaredTypeSignature;
3668
3787
  exports.getNodeBorderClassNames = getNodeBorderClassNames;
3669
3788
  exports.preformatValueForDisplay = preformatValueForDisplay;
3789
+ exports.prettyHandle = prettyHandle;
3670
3790
  exports.resolveOutputDisplay = resolveOutputDisplay;
3671
3791
  exports.summarizeDeep = summarizeDeep;
3672
3792
  exports.toReactFlow = toReactFlow;