@bian-womp/spark-workbench 0.1.18 → 0.1.20

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
@@ -659,6 +659,14 @@ function useWorkbenchBridge(wb) {
659
659
  return;
660
660
  if (!params.sourceHandle || !params.targetHandle)
661
661
  return;
662
+ // Prevent duplicate edges between the same endpoints
663
+ const def = wb.export();
664
+ const exists = def.edges.some((e) => e.source.nodeId === params.source &&
665
+ e.source.handle === params.sourceHandle &&
666
+ e.target.nodeId === params.target &&
667
+ e.target.handle === params.targetHandle);
668
+ if (exists)
669
+ return;
662
670
  wb.connect({
663
671
  source: { nodeId: params.source, handle: params.sourceHandle },
664
672
  target: { nodeId: params.target, handle: params.targetHandle },
@@ -916,7 +924,9 @@ function toReactFlow(def, positions, registry, opts) {
916
924
  const nodeHandleMap = {};
917
925
  const nodes = def.nodes.map((n) => {
918
926
  const desc = registry.nodes.get(n.typeId);
919
- const inputHandles = Object.entries(desc?.inputs ?? {}).map(([id, typeId]) => ({ id, typeId }));
927
+ const inputHandles = Object.entries(desc?.inputs ?? {})
928
+ .filter(([id]) => !sparkGraph.isInputPrivate(desc?.inputs, id))
929
+ .map(([id, v]) => ({ id, typeId: sparkGraph.getInputTypeId(desc?.inputs, id) }));
920
930
  const outputHandles = Object.entries(desc?.outputs ?? {}).map(([id, typeId]) => ({ id, typeId: formatDeclaredTypeSignature(typeId) }));
921
931
  nodeHandleMap[n.nodeId] = {
922
932
  inputs: new Set(inputHandles.map((h) => h.id)),
@@ -1110,8 +1120,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, children, }) {
1110
1120
  layers.push(layer);
1111
1121
  q.splice(0, q.length, ...next);
1112
1122
  }
1113
- const X = 480;
1114
- const Y = 240;
1123
+ const X = 960;
1124
+ const Y = 480;
1115
1125
  const pos = {};
1116
1126
  layers.forEach((layer, layerIndex) => {
1117
1127
  layer.forEach((id, itemIndex) => {
@@ -1521,7 +1531,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1521
1531
  const selectedDesc = selectedNode
1522
1532
  ? registry.nodes.get(selectedNode.typeId)
1523
1533
  : undefined;
1524
- const inputHandles = Object.keys(selectedDesc?.inputs ?? {});
1534
+ const inputHandles = Object.entries(selectedDesc?.inputs ?? {})
1535
+ .filter(([k]) => !sparkGraph.isInputPrivate(selectedDesc?.inputs, k))
1536
+ .map(([k]) => k);
1525
1537
  const outputHandles = Object.keys(selectedDesc?.outputs ?? {});
1526
1538
  const nodeInputs = selectedNodeId ? inputsMap[selectedNodeId] ?? {} : {};
1527
1539
  const nodeOutputs = selectedNodeId ? outputsMap[selectedNodeId] ?? {} : {};
@@ -1568,7 +1580,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1568
1580
  const nextDrafts = { ...drafts };
1569
1581
  const nextOriginals = { ...originals };
1570
1582
  for (const h of handles) {
1571
- const typeId = desc?.inputs?.[h];
1583
+ const typeId = sparkGraph.getInputTypeId(desc?.inputs, h);
1572
1584
  const current = nodeInputs[h];
1573
1585
  const display = safeToString(typeId, current);
1574
1586
  const wasOriginal = originals[h];
@@ -1588,7 +1600,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1588
1600
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
1589
1601
  return (jsxRuntime.jsxs("div", { className: `${widthClass} border-l border-gray-300 p-3 flex flex-col h-full min-h-0 overflow-hidden`, children: [jsxRuntime.jsx("div", { className: "font-semibold mb-2", children: "Inspector" }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: !selectedNode && !selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "text-gray-500", children: "Select a node or edge." }), globalValidationIssues && globalValidationIssues.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: globalValidationIssues.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : selectedEdge ? (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Edge: ", selectedEdge.id] }), jsxRuntime.jsxs("div", { children: [selectedEdge.source.nodeId, ".", selectedEdge.source.handle, " \u2192", " ", selectedEdge.target.nodeId, ".", selectedEdge.target.handle] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedEdge.typeId] })] }), selectedEdgeValidation.length > 0 && (jsxRuntime.jsxs("div", { className: "mt-2 text-xs bg-red-50 border border-red-200 rounded px-2 py-1", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Validation" }), jsxRuntime.jsx("ul", { className: "list-disc ml-4", children: selectedEdgeValidation.map((m, i) => (jsxRuntime.jsxs("li", { className: "flex items-center gap-1", children: [jsxRuntime.jsx(IssueBadge, { level: m.level, size: 24, className: "w-6 h-6" }), jsxRuntime.jsx("span", { children: `${m.code}: ${m.message}` })] }, i))) })] }))] })) : (jsxRuntime.jsxs("div", { children: [selectedNode && (jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsxs("div", { children: ["Node: ", selectedNode.nodeId] }), jsxRuntime.jsxs("div", { children: ["Type: ", selectedNode.typeId] }), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
1590
1602
  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) => {
1591
- const typeId = (selectedDesc?.inputs ?? {})[h];
1603
+ const typeId = sparkGraph.getInputTypeId(selectedDesc?.inputs, h);
1592
1604
  const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
1593
1605
  e.target.handle === h);
1594
1606
  const commonProps = {
@@ -1616,7 +1628,7 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
1616
1628
  const title = inIssues
1617
1629
  .map((v) => `${v.code}: ${v.message}`)
1618
1630
  .join("; ");
1619
- return (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsxRuntime.jsxs("label", { className: "w-32", children: [h, jsxRuntime.jsx("span", { className: "text-gray-500 ml-1 text-[11px]", children: selectedDesc?.inputs?.[h] })] }), 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
1631
+ 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
1620
1632
  ? String(current)
1621
1633
  : "", onChange: (e) => {
1622
1634
  const val = e.target.value;
@@ -1948,13 +1960,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, r
1948
1960
  return (jsxRuntime.jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsxRuntime.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: [jsxRuntime.jsx(ReactFlow.Background, {}), jsxRuntime.jsx(ReactFlow.MiniMap, {}), jsxRuntime.jsx(ReactFlow.Controls, {}), jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: () => setMenuOpen(false) }), jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: () => setNodeMenuOpen(false) })] }) }));
1949
1961
  });
1950
1962
 
1951
- function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
1963
+ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
1952
1964
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
1953
1965
  const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
1954
1966
  const selectedDesc = selectedNode
1955
1967
  ? registry.nodes.get(selectedNode.typeId)
1956
1968
  : undefined;
1957
- const [exampleState, setExampleState] = React.useState(example ?? "simple");
1969
+ const [exampleState, setExampleState] = React.useState(example ?? "");
1958
1970
  const defaultExamples = React.useMemo(() => [
1959
1971
  {
1960
1972
  id: "simple",
@@ -1997,6 +2009,54 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1997
2009
  const lastAutoLaunched = React.useRef(undefined);
1998
2010
  const autoLayoutRan = React.useRef(false);
1999
2011
  const canvasRef = React.useRef(null);
2012
+ // Expose init callback with setInitialGraph helper
2013
+ const initCalled = React.useRef(false);
2014
+ React.useEffect(() => {
2015
+ if (initCalled.current)
2016
+ return;
2017
+ initCalled.current = true;
2018
+ if (!onInit)
2019
+ return;
2020
+ const setInitialGraph = async (d, inputs) => {
2021
+ await wb.load(d);
2022
+ try {
2023
+ runner.build(wb.export());
2024
+ }
2025
+ catch { }
2026
+ if (inputs) {
2027
+ for (const [nodeId, map] of Object.entries(inputs)) {
2028
+ runner.setInputs(nodeId, map);
2029
+ }
2030
+ }
2031
+ runAutoLayout();
2032
+ };
2033
+ onInit({ wb, runner, setInitialGraph });
2034
+ }, [onInit, wb, runner, runAutoLayout]);
2035
+ // Expose change callback on graph/value changes
2036
+ React.useEffect(() => {
2037
+ if (!onChange)
2038
+ return;
2039
+ const off1 = wb.on("graphChanged", () => {
2040
+ try {
2041
+ const cur = wb.export();
2042
+ const inputs = runner.getInputs(cur);
2043
+ onChange({ def: cur, inputs });
2044
+ }
2045
+ catch { }
2046
+ });
2047
+ const off2 = runner.on("value", () => {
2048
+ try {
2049
+ const cur = wb.export();
2050
+ const inputs = runner.getInputs(cur);
2051
+ onChange({ def: cur, inputs });
2052
+ }
2053
+ catch { }
2054
+ });
2055
+ return () => {
2056
+ off1();
2057
+ off2();
2058
+ };
2059
+ }, [wb, runner, onChange]);
2000
2060
  const applyExample = React.useCallback(async (key) => {
2001
2061
  if (runner.isRunning()) {
2002
2062
  alert(`Stop engine before switching example.`);
@@ -2111,7 +2171,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2111
2171
  }, [setRegistry, wb]);
2112
2172
  // Ensure initial example is loaded (and sync when example prop changes)
2113
2173
  React.useEffect(() => {
2114
- applyExample(example ?? "simple");
2174
+ if (!example)
2175
+ return;
2176
+ applyExample(example);
2115
2177
  }, [example, wb]);
2116
2178
  React.useEffect(() => {
2117
2179
  if (!engine)
@@ -2182,7 +2244,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2182
2244
  break;
2183
2245
  }
2184
2246
  case "base.bool": {
2185
- value = Boolean(raw);
2247
+ value = raw === "true" || raw === "1" ? true : false;
2186
2248
  break;
2187
2249
  }
2188
2250
  case "base.string": {
@@ -2319,14 +2381,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2319
2381
  return overrides.toElement(baseToElement, { registry });
2320
2382
  return baseToElement;
2321
2383
  }, [overrides, baseToElement, registry]);
2322
- return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsx("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2384
+ return (jsxRuntime.jsxs("div", { className: "w-full h-screen flex flex-col", children: [jsxRuntime.jsxs("div", { className: "p-2 border-b border-gray-300 flex gap-2 items-center", children: [runner.isRunning() ? (jsxRuntime.jsxs("span", { className: "ml-2 text-sm text-green-700", children: ["Running: ", runner.getRunningEngine()] })) : (jsxRuntime.jsx("span", { className: "ml-2 text-sm text-gray-500", children: "Stopped" })), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Example:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: exampleState, onChange: (e) => applyExample(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2323
2385
  ? "Stop engine before switching example"
2324
- : undefined, children: examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id))) }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2386
+ : undefined, children: [jsxRuntime.jsx("option", { value: "", children: "Select Example\u2026" }), examples.map((ex) => (jsxRuntime.jsx("option", { value: ex.id, children: ex.label }, ex.id)))] }), jsxRuntime.jsx("label", { className: "ml-2 text-sm", children: "Backend:" }), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: backendKind, onChange: (e) => onBackendKindChange(e.target.value), disabled: runner.isRunning(), title: runner.isRunning()
2325
2387
  ? "Stop engine before switching backend"
2326
2388
  : undefined, children: [jsxRuntime.jsx("option", { value: "local", children: "Local" }), jsxRuntime.jsx("option", { value: "remote-http", children: "Remote (HTTP)" }), jsxRuntime.jsx("option", { value: "remote-ws", children: "Remote (WebSocket)" })] }), backendKind === "remote-http" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "http://127.0.0.1:18080", value: httpBaseUrl, onChange: (e) => onHttpBaseUrlChange(e.target.value) })), backendKind === "remote-ws" && (jsxRuntime.jsx("input", { className: "ml-2 border border-gray-300 rounded px-2 py-1 w-72", placeholder: "ws://127.0.0.1:18081", value: wsUrl, onChange: (e) => onWsUrlChange(e.target.value) })), jsxRuntime.jsxs("select", { className: "border border-gray-300 rounded px-2 py-1", value: runner.getRunningEngine() ?? engine ?? "", onChange: (e) => {
2327
2389
  const kind = e.target.value || undefined;
2328
2390
  onEngineChange?.(kind);
2329
- }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { onClick: () => {
2391
+ }, children: [jsxRuntime.jsx("option", { value: "", children: "Select Engine\u2026" }), jsxRuntime.jsx("option", { value: "push", children: "Push" }), jsxRuntime.jsx("option", { value: "batched", children: "Batched" }), jsxRuntime.jsx("option", { value: "pull", children: "Pull" }), jsxRuntime.jsx("option", { value: "hybrid", children: "Hybrid" }), jsxRuntime.jsx("option", { value: "step", children: "Step" })] }), runner.getRunningEngine() === "step" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.step(), disabled: !runner.isRunning(), children: "Step" })), runner.getRunningEngine() === "batched" && (jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.flush(), disabled: !runner.isRunning(), children: "Flush" })), runner.isRunning() ? (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => runner.dispose(), disabled: !runner.isRunning(), children: "Stop" })) : (jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: () => {
2330
2392
  const kind = engine;
2331
2393
  if (!kind)
2332
2394
  return alert("Select an engine first.");
@@ -2336,9 +2398,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2336
2398
  catch (err) {
2337
2399
  alert(String(err?.message ?? err));
2338
2400
  }
2339
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2401
+ }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { className: "border border-gray-300 rounded px-2 py-1.5", onClick: runAutoLayout, children: "Auto Layout" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: () => canvasRef.current?.fitView?.(), title: "Fit View", children: "Fit View" }), jsxRuntime.jsx("button", { className: "ml-2 border border-gray-300 rounded px-2 py-1.5", onClick: downloadGraph, children: "Download Graph" }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: debug, onChange: (e) => onDebugChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Debug events" })] }), jsxRuntime.jsxs("label", { className: "ml-2 flex items-center gap-1", children: [jsxRuntime.jsx("input", { type: "checkbox", checked: showValues, onChange: (e) => onShowValuesChange(e.target.checked) }), jsxRuntime.jsx("span", { children: "Show values in nodes" })] })] }), jsxRuntime.jsxs("div", { className: "flex flex-1 min-h-0", children: [jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntime.jsx(WorkbenchCanvas, { ref: canvasRef, showValues: showValues, toString: toString, toElement: toElement }) }), jsxRuntime.jsx(Inspector, { setInput: setInput, debug: debug, autoScroll: autoScroll, hideWorkbench: hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange, toString: toString, toElement: toElement })] })] }));
2340
2402
  }
2341
- function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
2403
+ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, onInit, onChange, }) {
2342
2404
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
2343
2405
  const [wb] = React.useState(() => new InMemoryWorkbench({ ui: new DefaultUIExtensionRegistry() }));
2344
2406
  const runner = React.useMemo(() => {
@@ -2352,14 +2414,14 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
2352
2414
  // Allow external UI registration (e.g., node renderers) with access to wb
2353
2415
  React.useEffect(() => {
2354
2416
  const baseRegisterUI = (_wb) => { };
2355
- overrides?.registerUI?.(baseRegisterUI, { wb });
2417
+ overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
2356
2418
  // eslint-disable-next-line react-hooks/exhaustive-deps
2357
- }, [wb, overrides]);
2419
+ }, [wb, runner, overrides]);
2358
2420
  return (jsxRuntime.jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, children: jsxRuntime.jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: (v) => {
2359
2421
  if (runner.isRunning())
2360
2422
  runner.dispose();
2361
2423
  onBackendKindChange(v);
2362
- }, httpBaseUrl: httpBaseUrl, onHttpBaseUrlChange: onHttpBaseUrlChange, wsUrl: wsUrl, onWsUrlChange: onWsUrlChange, debug: debug, onDebugChange: onDebugChange, showValues: showValues, onShowValuesChange: onShowValuesChange, hideWorkbench: hideWorkbench, onHideWorkbenchChange: onHideWorkbenchChange, overrides: overrides }) }));
2424
+ }, 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 }) }));
2363
2425
  }
2364
2426
 
2365
2427
  exports.AbstractWorkbench = AbstractWorkbench;