@bian-womp/spark-workbench 0.1.17 → 0.1.19

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 },
@@ -857,7 +865,7 @@ function preformatValueForDisplay(typeId, value, registry) {
857
865
  }
858
866
  return undefined;
859
867
  }
860
- function summarizeDeep(value, registry) {
868
+ function summarizeDeep(value) {
861
869
  // Strings: summarize data URLs and trim extremely long strings
862
870
  if (typeof value === "string") {
863
871
  if (value.startsWith("data:")) {
@@ -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;
@@ -1661,7 +1673,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1661
1673
  const ROW_SIZE = 22;
1662
1674
  const maxRows = Math.max(inputEntries.length, outputEntries.length);
1663
1675
  const minHeight = HEADER_SIZE + maxRows * ROW_SIZE;
1664
- const minWidth = data.showValues ? 320 : 160;
1676
+ const minWidth = data.showValues ? 320 : 240;
1665
1677
  const topFor = (i) => HEADER_SIZE + i * ROW_SIZE + ROW_SIZE / 2;
1666
1678
  const hasError = !!status.lastError;
1667
1679
  const hasValidationError = validation.issues.some((i) => i.level === "error");
@@ -1694,7 +1706,13 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1694
1706
  const title = vIssues
1695
1707
  .map((v) => `${v.code}: ${v.message}`)
1696
1708
  .join("; ");
1697
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8 }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1709
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "target", position: ReactFlow.Position.Left, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { left: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute left-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1710
+ top: topFor(i) - 8,
1711
+ right: "50%",
1712
+ whiteSpace: "nowrap",
1713
+ overflow: "hidden",
1714
+ textOverflow: "ellipsis",
1715
+ }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(entry.typeId, inputValues?.[entry.id]) }))] })] }, `in-${entry.id}`));
1698
1716
  }), outputEntries.map((entry, i) => {
1699
1717
  const vIssues = validation.outputs.filter((v) => v.handle === entry.id);
1700
1718
  const hasAny = vIssues.length > 0;
@@ -1703,7 +1721,14 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
1703
1721
  .map((v) => `${v.code}: ${v.message}`)
1704
1722
  .join("; ");
1705
1723
  const resolved = resolveOutputDisplay(outputValues?.[entry.id], entry.typeId);
1706
- return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: { top: topFor(i) - 8, textAlign: "right" }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, resolved.typeId && (jsxRuntime.jsxs("span", { className: "ml-1 opacity-60", children: ["(", resolved.typeId, ")"] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1724
+ return (jsxRuntime.jsxs(React.Fragment, { children: [jsxRuntime.jsx(ReactFlow.Handle, { id: entry.id, type: "source", position: ReactFlow.Position.Right, isConnectable: isConnectable, className: cx("!w-3 !h-3 !bg-white !dark:bg-stone-900 !border-gray-500 dark:!border-gray-400 !rounded-none", hasAny && (hasErr ? "!border-red-500" : "!border-amber-500")), style: { right: -5, top: topFor(i) } }), jsxRuntime.jsxs("div", { className: "absolute right-2 text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", style: {
1725
+ top: topFor(i) - 8,
1726
+ textAlign: "right",
1727
+ left: "50%",
1728
+ whiteSpace: "nowrap",
1729
+ overflow: "hidden",
1730
+ textOverflow: "ellipsis",
1731
+ }, title: `${entry.id}: ${entry.typeId}`, children: [entry.id, resolved.typeId && (jsxRuntime.jsxs("span", { className: "ml-1 opacity-60", children: ["(", resolved.typeId, ")"] })), hasAny && (jsxRuntime.jsx(IssueBadge, { level: hasErr ? "error" : "warning", size: 12, className: "ml-1", title: title })), showValues && (jsxRuntime.jsx("span", { className: "ml-1 opacity-60", children: toString(resolved.typeId, resolved.value) }))] })] }, `out-${entry.id}`));
1707
1732
  })] }));
1708
1733
  });
1709
1734
  DefaultNode.displayName = "DefaultNode";
@@ -1842,11 +1867,21 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
1842
1867
  }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
1843
1868
  }
1844
1869
 
1845
- function WorkbenchCanvas({ showValues, toString, toElement, }) {
1870
+ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement }, ref) => {
1846
1871
  const { wb, registry, inputsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, } = useWorkbenchContext();
1847
1872
  const ioValues = { inputs: inputsMap, outputs: outputsMap };
1848
1873
  const nodeValidation = validationByNode;
1849
1874
  const edgeValidation = validationByEdge.errors;
1875
+ // Expose imperative API
1876
+ const rfInstanceRef = React.useRef(null);
1877
+ React.useImperativeHandle(ref, () => ({
1878
+ fitView: () => {
1879
+ try {
1880
+ rfInstanceRef.current?.fitView({ padding: 0.2 });
1881
+ }
1882
+ catch { }
1883
+ },
1884
+ }));
1850
1885
  const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, onSelectionChange, } = useWorkbenchBridge(wb);
1851
1886
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
1852
1887
  // Build nodeTypes map using UI extension registry
@@ -1922,8 +1957,8 @@ function WorkbenchCanvas({ showValues, toString, toElement, }) {
1922
1957
  const addNodeAt = (typeId, pos) => {
1923
1958
  wb.addNode({ typeId, position: pos });
1924
1959
  };
1925
- 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, 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) })] }) }));
1926
- }
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) })] }) }));
1961
+ });
1927
1962
 
1928
1963
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, }) {
1929
1964
  const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
@@ -1931,7 +1966,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1931
1966
  const selectedDesc = selectedNode
1932
1967
  ? registry.nodes.get(selectedNode.typeId)
1933
1968
  : undefined;
1934
- const [exampleState, setExampleState] = React.useState(example ?? "simple");
1969
+ const [exampleState, setExampleState] = React.useState(example ?? "");
1935
1970
  const defaultExamples = React.useMemo(() => [
1936
1971
  {
1937
1972
  id: "simple",
@@ -1973,6 +2008,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1973
2008
  }, [overrides, defaultExamples]);
1974
2009
  const lastAutoLaunched = React.useRef(undefined);
1975
2010
  const autoLayoutRan = React.useRef(false);
2011
+ const canvasRef = React.useRef(null);
1976
2012
  const applyExample = React.useCallback(async (key) => {
1977
2013
  if (runner.isRunning()) {
1978
2014
  alert(`Stop engine before switching example.`);
@@ -1993,6 +2029,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
1993
2029
  setExampleState(key);
1994
2030
  onExampleChange?.(key);
1995
2031
  }, [runner, wb, onExampleChange, runAutoLayout, examples, setRegistry]);
2032
+ const downloadGraph = React.useCallback(() => {
2033
+ try {
2034
+ const def = wb.export();
2035
+ const pretty = JSON.stringify(def, null, 2);
2036
+ const blob = new Blob([pretty], { type: "application/json" });
2037
+ const url = URL.createObjectURL(blob);
2038
+ const a = document.createElement("a");
2039
+ const d = new Date();
2040
+ const pad = (n) => String(n).padStart(2, "0");
2041
+ const ts = `${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`;
2042
+ a.href = url;
2043
+ a.download = `spark-graph-${ts}.json`;
2044
+ document.body.appendChild(a);
2045
+ a.click();
2046
+ a.remove();
2047
+ URL.revokeObjectURL(url);
2048
+ }
2049
+ catch (err) {
2050
+ alert(String(err?.message ?? err));
2051
+ }
2052
+ }, [wb]);
1996
2053
  const hydrateFromBackend = React.useCallback(async (kind, base) => {
1997
2054
  try {
1998
2055
  const transport = kind === "remote-http"
@@ -2066,7 +2123,9 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2066
2123
  }, [setRegistry, wb]);
2067
2124
  // Ensure initial example is loaded (and sync when example prop changes)
2068
2125
  React.useEffect(() => {
2069
- applyExample(example ?? "simple");
2126
+ if (!example)
2127
+ return;
2128
+ applyExample(example);
2070
2129
  }, [example, wb]);
2071
2130
  React.useEffect(() => {
2072
2131
  if (!engine)
@@ -2137,7 +2196,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2137
2196
  break;
2138
2197
  }
2139
2198
  case "base.bool": {
2140
- value = Boolean(raw);
2199
+ value = raw === "true" || raw === "1" ? true : false;
2141
2200
  break;
2142
2201
  }
2143
2202
  case "base.string": {
@@ -2274,14 +2333,14 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2274
2333
  return overrides.toElement(baseToElement, { registry });
2275
2334
  return baseToElement;
2276
2335
  }, [overrides, baseToElement, registry]);
2277
- 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()
2336
+ 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()
2278
2337
  ? "Stop engine before switching example"
2279
- : 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()
2338
+ : 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()
2280
2339
  ? "Stop engine before switching backend"
2281
2340
  : 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) => {
2282
2341
  const kind = e.target.value || undefined;
2283
2342
  onEngineChange?.(kind);
2284
- }, 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: () => {
2343
+ }, 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: () => {
2285
2344
  const kind = engine;
2286
2345
  if (!kind)
2287
2346
  return alert("Select an engine first.");
@@ -2291,7 +2350,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
2291
2350
  catch (err) {
2292
2351
  alert(String(err?.message ?? err));
2293
2352
  }
2294
- }, disabled: !engine, children: "Start" })), jsxRuntime.jsx("button", { onClick: runAutoLayout, children: "Auto Layout" }), 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, { 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 })] })] }));
2353
+ }, 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 })] })] }));
2295
2354
  }
2296
2355
  function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, autoScroll, onAutoScrollChange, overrides, }) {
2297
2356
  const [registry, setRegistry] = React.useState(sparkGraph.createSimpleGraphRegistry());
@@ -2307,9 +2366,9 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
2307
2366
  // Allow external UI registration (e.g., node renderers) with access to wb
2308
2367
  React.useEffect(() => {
2309
2368
  const baseRegisterUI = (_wb) => { };
2310
- overrides?.registerUI?.(baseRegisterUI, { wb });
2369
+ overrides?.registerUI?.(baseRegisterUI, { wb, wbRunner: runner });
2311
2370
  // eslint-disable-next-line react-hooks/exhaustive-deps
2312
- }, [wb, overrides]);
2371
+ }, [wb, runner, overrides]);
2313
2372
  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) => {
2314
2373
  if (runner.isRunning())
2315
2374
  runner.dispose();