@bian-womp/spark-workbench 0.2.54 → 0.2.56

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 (65) hide show
  1. package/lib/cjs/index.cjs +340 -232
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/AbstractWorkbench.d.ts +2 -0
  4. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +0 -1
  6. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  7. package/lib/cjs/src/core/ui-extensions.d.ts +44 -47
  8. package/lib/cjs/src/core/ui-extensions.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/DefaultContextMenu.d.ts +2 -12
  10. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/NodeContextMenu.d.ts +2 -9
  12. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/NodeHandles.d.ts +1 -3
  14. package/lib/cjs/src/misc/NodeHandles.d.ts.map +1 -1
  15. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/constants.d.ts +2 -1
  17. package/lib/cjs/src/misc/constants.d.ts.map +1 -1
  18. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +41 -0
  19. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -0
  20. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +7 -0
  21. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -0
  22. package/lib/cjs/src/misc/layout.d.ts +46 -0
  23. package/lib/cjs/src/misc/layout.d.ts.map +1 -1
  24. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  25. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +3 -2
  26. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  27. package/lib/cjs/src/runtime/IGraphRunner.d.ts +1 -0
  28. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  29. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +1 -1
  30. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  31. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +0 -1
  32. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  33. package/lib/esm/index.js +337 -234
  34. package/lib/esm/index.js.map +1 -1
  35. package/lib/esm/src/core/AbstractWorkbench.d.ts +2 -0
  36. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  37. package/lib/esm/src/core/InMemoryWorkbench.d.ts +0 -1
  38. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  39. package/lib/esm/src/core/ui-extensions.d.ts +44 -47
  40. package/lib/esm/src/core/ui-extensions.d.ts.map +1 -1
  41. package/lib/esm/src/misc/DefaultContextMenu.d.ts +2 -12
  42. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  43. package/lib/esm/src/misc/NodeContextMenu.d.ts +2 -9
  44. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
  45. package/lib/esm/src/misc/NodeHandles.d.ts +1 -3
  46. package/lib/esm/src/misc/NodeHandles.d.ts.map +1 -1
  47. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  48. package/lib/esm/src/misc/constants.d.ts +2 -1
  49. package/lib/esm/src/misc/constants.d.ts.map +1 -1
  50. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +41 -0
  51. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -0
  52. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +7 -0
  53. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -0
  54. package/lib/esm/src/misc/layout.d.ts +46 -0
  55. package/lib/esm/src/misc/layout.d.ts.map +1 -1
  56. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  57. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +3 -2
  58. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  59. package/lib/esm/src/runtime/IGraphRunner.d.ts +1 -0
  60. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  61. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +1 -1
  62. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  63. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +0 -1
  64. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  65. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
1
+ import { generateId, GraphBuilder, createEngine, StepEngine, PullEngine, BatchedEngine, isTypedOutput, getTypedOutputValue, getTypedOutputTypeId, isInputPrivate, getInputTypeId, createSimpleGraphRegistry, createSimpleGraphDef, createAsyncGraphDef, createAsyncGraphRegistry, createProgressGraphDef, createProgressGraphRegistry, createValidationGraphDef, createValidationGraphRegistry } from '@bian-womp/spark-graph';
2
2
  import { RuntimeApiClient } from '@bian-womp/spark-remote';
3
3
  import { Position, Handle, useUpdateNodeInternals, useReactFlow, ReactFlowProvider, ReactFlow, Background, BackgroundVariant, MiniMap, Controls } from '@xyflow/react';
4
4
  import React, { useCallback, useState, useRef, useEffect, useMemo, createContext, useContext, useImperativeHandle } from 'react';
@@ -10,71 +10,75 @@ import isEqual from 'lodash/isEqual';
10
10
  class DefaultUIExtensionRegistry {
11
11
  constructor() {
12
12
  this.nodeRenderers = new Map();
13
- this.portRenderers = new Map();
14
- this.edgeRenderers = new Map();
15
13
  }
16
14
  registerNodeRenderer(nodeTypeId, renderer) {
17
- this.nodeRenderers.set(nodeTypeId, renderer);
15
+ if (renderer === undefined) {
16
+ this.nodeRenderers.delete(nodeTypeId);
17
+ }
18
+ else {
19
+ this.nodeRenderers.set(nodeTypeId, renderer);
20
+ }
18
21
  return this;
19
22
  }
20
23
  getNodeRenderer(nodeTypeId) {
21
24
  return this.nodeRenderers.get(nodeTypeId);
22
25
  }
23
- registerPortRenderer(dataTypeId, renderer) {
24
- this.portRenderers.set(dataTypeId, renderer);
25
- return this;
26
- }
27
- getPortRenderer(dataTypeId) {
28
- return this.portRenderers.get(dataTypeId);
26
+ getAllNodeRenderers() {
27
+ const result = {};
28
+ for (const [nodeTypeId, renderer] of this.nodeRenderers.entries()) {
29
+ result[nodeTypeId] = renderer;
30
+ }
31
+ return result;
29
32
  }
30
- registerEdgeRenderer(typeId, renderer) {
31
- this.edgeRenderers.set(typeId, renderer);
33
+ registerIconProvider(provider) {
34
+ this.iconProvider = provider;
32
35
  return this;
33
36
  }
34
- getEdgeRenderer(typeId) {
35
- return this.edgeRenderers.get(typeId);
37
+ getIconProvider() {
38
+ return this.iconProvider;
36
39
  }
37
- setInspector(renderer) {
38
- this.inspector = renderer;
40
+ // React Flow renderers
41
+ registerConnectionLineRenderer(renderer) {
42
+ this.connectionLineRenderer = renderer;
39
43
  return this;
40
44
  }
41
- getInspector() {
42
- return this.inspector;
45
+ getConnectionLineRenderer() {
46
+ return this.connectionLineRenderer;
43
47
  }
44
- setPalette(renderer) {
45
- this.palette = renderer;
48
+ registerMinimapRenderer(renderer) {
49
+ this.minimapRenderer = renderer;
46
50
  return this;
47
51
  }
48
- getPalette() {
49
- return this.palette;
52
+ getMinimapRenderer() {
53
+ return this.minimapRenderer;
50
54
  }
51
- setToolbar(renderer) {
52
- this.toolbar = renderer;
55
+ registerControlsRenderer(renderer) {
56
+ this.controlsRenderer = renderer;
53
57
  return this;
54
58
  }
55
- getToolbar() {
56
- return this.toolbar;
59
+ getControlsRenderer() {
60
+ return this.controlsRenderer;
57
61
  }
58
- setContextMenu(renderer) {
59
- this.contextMenu = renderer;
62
+ registerBackgroundRenderer(renderer) {
63
+ this.backgroundRenderer = renderer;
60
64
  return this;
61
65
  }
62
- getContextMenu() {
63
- return this.contextMenu;
66
+ getBackgroundRenderer() {
67
+ return this.backgroundRenderer;
64
68
  }
65
- setMiniMap(renderer) {
66
- this.miniMap = renderer;
69
+ registerDefaultContextMenuRenderer(renderer) {
70
+ this.defaultContextMenuRenderer = renderer;
67
71
  return this;
68
72
  }
69
- getMiniMap() {
70
- return this.miniMap;
73
+ getDefaultContextMenuRenderer() {
74
+ return this.defaultContextMenuRenderer;
71
75
  }
72
- setIconProvider(provider) {
73
- this.iconProvider = provider;
76
+ registerNodeContextMenuRenderer(renderer) {
77
+ this.nodeContextMenuRenderer = renderer;
74
78
  return this;
75
79
  }
76
- getIconProvider() {
77
- return this.iconProvider;
80
+ getNodeContextMenuRenderer() {
81
+ return this.nodeContextMenuRenderer;
78
82
  }
79
83
  }
80
84
 
@@ -84,6 +88,7 @@ class AbstractWorkbench {
84
88
  this.layout = args.layout;
85
89
  this.storage = args.storage;
86
90
  this.serializer = args.serializer;
91
+ this.genId = args.genId || generateId;
87
92
  }
88
93
  // Expose UI registry to adapters (React Flow, CLI) to allow overrides
89
94
  getUI() {
@@ -162,11 +167,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
162
167
  return { ok: issues.every((i) => i.level !== "error"), issues };
163
168
  }
164
169
  addNode(node) {
165
- const id = node.nodeId ?? this.generateId("n");
170
+ const id = node.nodeId ??
171
+ this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
166
172
  this.def.nodes.push({
167
173
  nodeId: id,
168
174
  typeId: node.typeId,
169
175
  params: node.params,
176
+ initialInputs: node.initialInputs,
177
+ resolvedHandles: node.resolvedHandles,
170
178
  });
171
179
  if (node.position)
172
180
  this.positions[id] = node.position;
@@ -188,7 +196,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
188
196
  this.refreshValidation();
189
197
  }
190
198
  connect(edge) {
191
- const id = edge.id ?? this.generateId("e");
199
+ const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
192
200
  this.def.edges.push({
193
201
  id,
194
202
  source: { ...edge.source },
@@ -329,9 +337,6 @@ class InMemoryWorkbench extends AbstractWorkbench {
329
337
  for (const h of Array.from(set))
330
338
  h(payload);
331
339
  }
332
- generateId(prefix) {
333
- return `${prefix}${Math.random().toString(36).slice(2, 8)}`;
334
- }
335
340
  }
336
341
 
337
342
  class CLIWorkbench {
@@ -463,6 +468,18 @@ class AbstractGraphRunner {
463
468
  }
464
469
  }
465
470
  }
471
+ getInputDefaults(def) {
472
+ const out = {};
473
+ for (const n of def.nodes) {
474
+ const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
475
+ const graphDefaults = n.initialInputs ?? {};
476
+ const merged = { ...dynDefaults, ...graphDefaults };
477
+ if (Object.keys(merged).length > 0) {
478
+ out[n.nodeId] = merged;
479
+ }
480
+ }
481
+ return out;
482
+ }
466
483
  on(event, handler) {
467
484
  if (!this.listeners.has(event))
468
485
  this.listeners.set(event, new Set());
@@ -624,16 +641,6 @@ class LocalGraphRunner extends AbstractGraphRunner {
624
641
  }
625
642
  return out;
626
643
  }
627
- getInputDefaults(def) {
628
- const out = {};
629
- for (const n of def.nodes) {
630
- const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
631
- if (Object.keys(dynDefaults).length > 0) {
632
- out[n.nodeId] = dynDefaults;
633
- }
634
- }
635
- return out;
636
- }
637
644
  async snapshotFull() {
638
645
  const def = undefined; // UI will supply def/positions on download for local
639
646
  const inputs = this.getInputs(this.runtime
@@ -661,11 +668,25 @@ class LocalGraphRunner extends AbstractGraphRunner {
661
668
  if (payload.def)
662
669
  this.build(payload.def);
663
670
  this.setEnvironment?.(payload.environment || {}, { merge: false });
664
- // Hydrate via runtime for exact restore and re-emit
671
+ this.hydrateSnapshotFull(payload);
672
+ }
673
+ hydrateSnapshotFull(snapshot) {
674
+ // Hydrate via runtime for exact restore (this emits events on runtime emitter)
665
675
  this.runtime?.hydrate({
666
- inputs: payload.inputs || {},
667
- outputs: payload.outputs || {},
676
+ inputs: snapshot.inputs || {},
677
+ outputs: snapshot.outputs || {},
668
678
  });
679
+ // Also emit directly from runner to ensure UI gets events even if engine isn't running
680
+ for (const [nodeId, map] of Object.entries(snapshot.inputs || {})) {
681
+ for (const [handle, value] of Object.entries(map || {})) {
682
+ this.emit("value", { nodeId, handle, value, io: "input" });
683
+ }
684
+ }
685
+ for (const [nodeId, map] of Object.entries(snapshot.outputs || {})) {
686
+ for (const [handle, value] of Object.entries(map || {})) {
687
+ this.emit("value", { nodeId, handle, value, io: "output" });
688
+ }
689
+ }
669
690
  }
670
691
  dispose() {
671
692
  super.dispose();
@@ -1222,16 +1243,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1222
1243
  }
1223
1244
  return out;
1224
1245
  }
1225
- getInputDefaults(def) {
1226
- const out = {};
1227
- for (const n of def.nodes) {
1228
- const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
1229
- if (Object.keys(dynDefaults).length > 0) {
1230
- out[n.nodeId] = dynDefaults;
1231
- }
1232
- }
1233
- return out;
1234
- }
1235
1246
  dispose() {
1236
1247
  // Idempotent: allow multiple calls safely
1237
1248
  if (this.disposed)
@@ -1401,7 +1412,8 @@ function summarizeDeep(value) {
1401
1412
 
1402
1413
  // Shared UI constants for node layout to keep mapping and rendering in sync
1403
1414
  const NODE_HEADER_HEIGHT_PX = 24;
1404
- const NODE_ROW_HEIGHT_PX = 22;
1415
+ const NODE_ROW_HEIGHT_PX = 18;
1416
+ const HANDLE_SIZE_PX = 12;
1405
1417
 
1406
1418
  function computeEffectiveHandles(node, registry) {
1407
1419
  const desc = registry.nodes.get(node.typeId);
@@ -1430,6 +1442,61 @@ function estimateNodeSize(args) {
1430
1442
  const height = overrides?.height ?? NODE_HEADER_HEIGHT_PX + rows * NODE_ROW_HEIGHT_PX;
1431
1443
  return { width, height, inputsCount, outputsCount, rowCount: rows };
1432
1444
  }
1445
+ /**
1446
+ * Calculate the Y position for handle layout (center of row).
1447
+ * Used for positioning handles in React Flow.
1448
+ */
1449
+ function getHandleLayoutY(rowIndex) {
1450
+ return (NODE_HEADER_HEIGHT_PX +
1451
+ rowIndex * NODE_ROW_HEIGHT_PX +
1452
+ NODE_ROW_HEIGHT_PX / 2);
1453
+ }
1454
+ /**
1455
+ * Calculate the Y position for handle bounds (top + centering offset).
1456
+ * Used for hit-testing and edge routing.
1457
+ */
1458
+ function getHandleBoundsY(rowIndex) {
1459
+ return (NODE_HEADER_HEIGHT_PX +
1460
+ rowIndex * NODE_ROW_HEIGHT_PX +
1461
+ (NODE_ROW_HEIGHT_PX - HANDLE_SIZE_PX) / 2 +
1462
+ 1);
1463
+ }
1464
+ /**
1465
+ * Calculate the X position for handle bounds based on position and node width.
1466
+ */
1467
+ function getHandleBoundsX(position, nodeWidth) {
1468
+ if (position === Position.Left) {
1469
+ return -HANDLE_SIZE_PX / 2 + 1;
1470
+ }
1471
+ else {
1472
+ return nodeWidth - HANDLE_SIZE_PX / 2 - 1;
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Create handle bounds object for hit-testing/edge routing.
1477
+ */
1478
+ function createHandleBounds(args) {
1479
+ return {
1480
+ id: args.id,
1481
+ type: args.type,
1482
+ position: args.position,
1483
+ x: getHandleBoundsX(args.position, args.nodeWidth),
1484
+ y: getHandleBoundsY(args.rowIndex),
1485
+ width: HANDLE_SIZE_PX,
1486
+ height: HANDLE_SIZE_PX,
1487
+ };
1488
+ }
1489
+ /**
1490
+ * Create handle layout object for React Flow rendering.
1491
+ */
1492
+ function createHandleLayout(args) {
1493
+ return {
1494
+ id: args.id,
1495
+ type: args.type,
1496
+ position: args.position,
1497
+ y: getHandleLayoutY(args.rowIndex),
1498
+ };
1499
+ }
1433
1500
  function layoutNode(args) {
1434
1501
  const { node, registry, showValues, overrides } = args;
1435
1502
  const { inputs, outputs } = computeEffectiveHandles(node, registry);
@@ -1441,40 +1508,34 @@ function layoutNode(args) {
1441
1508
  showValues,
1442
1509
  overrides,
1443
1510
  });
1444
- const HEADER = NODE_HEADER_HEIGHT_PX;
1445
- const ROW = NODE_ROW_HEIGHT_PX;
1446
1511
  const handles = [
1447
- ...inputOrder.map((id, i) => ({
1512
+ ...inputOrder.map((id, i) => createHandleBounds({
1448
1513
  id,
1449
1514
  type: "target",
1450
1515
  position: Position.Left,
1451
- x: 0,
1452
- y: HEADER + i * ROW,
1453
- width: 1,
1454
- height: ROW + 2,
1516
+ rowIndex: i,
1517
+ nodeWidth: width,
1455
1518
  })),
1456
- ...outputOrder.map((id, i) => ({
1519
+ ...outputOrder.map((id, i) => createHandleBounds({
1457
1520
  id,
1458
1521
  type: "source",
1459
1522
  position: Position.Right,
1460
- x: width - 1,
1461
- y: HEADER + i * ROW,
1462
- width: 1,
1463
- height: ROW + 2,
1523
+ rowIndex: i,
1524
+ nodeWidth: width,
1464
1525
  })),
1465
1526
  ];
1466
1527
  const handleLayout = [
1467
- ...inputOrder.map((id, i) => ({
1528
+ ...inputOrder.map((id, i) => createHandleLayout({
1468
1529
  id,
1469
1530
  type: "target",
1470
1531
  position: Position.Left,
1471
- y: HEADER + i * ROW + ROW / 2,
1532
+ rowIndex: i,
1472
1533
  })),
1473
- ...outputOrder.map((id, i) => ({
1534
+ ...outputOrder.map((id, i) => createHandleLayout({
1474
1535
  id,
1475
1536
  type: "source",
1476
1537
  position: Position.Right,
1477
- y: HEADER + i * ROW + ROW / 2,
1538
+ rowIndex: i,
1478
1539
  })),
1479
1540
  ];
1480
1541
  return { width, height, inputOrder, outputOrder, handles, handleLayout };
@@ -1789,20 +1850,22 @@ function toReactFlow(def, positions, registry, opts) {
1789
1850
  const baseRightCount = geom.outputOrder.length;
1790
1851
  const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
1791
1852
  const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
1792
- const HEADER = NODE_HEADER_HEIGHT_PX;
1793
- const ROW = NODE_ROW_HEIGHT_PX;
1794
1853
  const extraHandleLayoutLeft = extraInputs.map((id, i) => ({
1795
- id,
1796
- type: "target",
1797
- position: Position.Left,
1798
- y: HEADER + (baseLeftCount + i) * ROW + ROW / 2,
1854
+ ...createHandleLayout({
1855
+ id,
1856
+ type: "target",
1857
+ position: Position.Left,
1858
+ rowIndex: baseLeftCount + i,
1859
+ }),
1799
1860
  missing: true,
1800
1861
  }));
1801
1862
  const extraHandleLayoutRight = extraOutputs.map((id, i) => ({
1802
- id,
1803
- type: "source",
1804
- position: Position.Right,
1805
- y: HEADER + (baseRightCount + i) * ROW + ROW / 2,
1863
+ ...createHandleLayout({
1864
+ id,
1865
+ type: "source",
1866
+ position: Position.Right,
1867
+ rowIndex: baseRightCount + i,
1868
+ }),
1806
1869
  missing: true,
1807
1870
  }));
1808
1871
  const handleLayout = [
@@ -1811,23 +1874,19 @@ function toReactFlow(def, positions, registry, opts) {
1811
1874
  ...extraHandleLayoutRight,
1812
1875
  ];
1813
1876
  // Precompute handle bounds (including missing) so edges can render immediately
1814
- const missingBoundsLeft = extraInputs.map((id, i) => ({
1877
+ const missingBoundsLeft = extraInputs.map((id, i) => createHandleBounds({
1815
1878
  id,
1816
1879
  type: "target",
1817
1880
  position: Position.Left,
1818
- x: 0,
1819
- y: HEADER + (baseLeftCount + i) * ROW,
1820
- width: 1,
1821
- height: ROW + 2,
1881
+ rowIndex: baseLeftCount + i,
1882
+ nodeWidth: geom.width,
1822
1883
  }));
1823
- const missingBoundsRight = extraOutputs.map((id, i) => ({
1884
+ const missingBoundsRight = extraOutputs.map((id, i) => createHandleBounds({
1824
1885
  id,
1825
1886
  type: "source",
1826
1887
  position: Position.Right,
1827
- x: geom.width - 1,
1828
- y: HEADER + (baseRightCount + i) * ROW,
1829
- width: 1,
1830
- height: ROW + 2,
1888
+ rowIndex: baseRightCount + i,
1889
+ nodeWidth: geom.width,
1831
1890
  }));
1832
1891
  const handles = [
1833
1892
  ...geom.handles,
@@ -1838,7 +1897,7 @@ function toReactFlow(def, positions, registry, opts) {
1838
1897
  const baseRows = Math.max(baseLeftCount, baseRightCount);
1839
1898
  const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
1840
1899
  const initialWidth = geom.width;
1841
- const initialHeight = geom.height + Math.max(0, newRows - baseRows) * ROW;
1900
+ const initialHeight = geom.height + Math.max(0, newRows - baseRows) * NODE_ROW_HEIGHT_PX;
1842
1901
  return {
1843
1902
  id: n.nodeId,
1844
1903
  data: {
@@ -1974,7 +2033,7 @@ function getHandleClassName(args) {
1974
2033
  else {
1975
2034
  borderColor = "!border-gray-500 dark:!border-gray-400";
1976
2035
  }
1977
- return cx("!w-3 !h-3 !bg-white !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
2036
+ return cx("!w-3 !h-3 !bg-white/50 !dark:bg-stone-900", borderColor, kind === "output" && "!rounded-none");
1978
2037
  }
1979
2038
 
1980
2039
  function generateTimestamp() {
@@ -3046,7 +3105,7 @@ function NodeHandleItem({ kind, id, type, position, y, isConnectable, className,
3046
3105
  textOverflow: "ellipsis",
3047
3106
  }, children: renderLabel({ kind, id }) }))] }));
3048
3107
  }
3049
- function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray-600", outputClassName = "!w-2 !h-2 !bg-gray-600", getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
3108
+ function NodeHandles({ data, isConnectable, getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
3050
3109
  const layout = data.handleLayout ?? [];
3051
3110
  const byId = React.useMemo(() => {
3052
3111
  const m = new Map();
@@ -3077,28 +3136,26 @@ function NodeHandles({ data, isConnectable, inputClassName = "!w-2 !h-2 !bg-gray
3077
3136
  const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
3078
3137
  const position = placed?.position ?? Position.Left;
3079
3138
  const y = placed?.y;
3080
- const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ??
3081
- inputClassName;
3139
+ const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ?? "";
3082
3140
  return (jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3083
3141
  }), missingInputs.map((h) => {
3084
3142
  const key = `missing-input:${h.id}`;
3085
3143
  const position = h.position ?? Position.Left;
3086
3144
  const y = h.y;
3087
3145
  const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500";
3088
- return (jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3146
+ return (jsx(NodeHandleItem, { kind: "input", id: h.id, type: "target", position: position, y: y, isConnectable: false, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3089
3147
  }), (data.outputHandles ?? []).map((h) => {
3090
3148
  const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
3091
3149
  const position = placed?.position ?? Position.Right;
3092
3150
  const y = placed?.y;
3093
- const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ??
3094
- outputClassName;
3095
- return (jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3151
+ const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ?? "";
3152
+ return (jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: isConnectable, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, h.id));
3096
3153
  }), missingOutputs.map((h) => {
3097
3154
  const key = `missing-output:${h.id}`;
3098
3155
  const position = h.position ?? Position.Right;
3099
3156
  const y = h.y;
3100
3157
  const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
3101
- return (jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3158
+ return (jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: `${cls} wb-nodrag wb-nowheel`, labelClassName: labelClassName, renderLabel: renderLabel }, key));
3102
3159
  })] }));
3103
3160
  }
3104
3161
 
@@ -3127,7 +3184,7 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
3127
3184
  status,
3128
3185
  validation,
3129
3186
  });
3130
- return (jsxs("div", { className: cx("rounded-lg bg-white/70 !dark:bg-stone-900", containerBorder), style: {
3187
+ return (jsxs("div", { className: cx("rounded-lg bg-white/50 !dark:bg-stone-900", containerBorder), style: {
3131
3188
  position: "relative",
3132
3189
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
3133
3190
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
@@ -3212,15 +3269,13 @@ function DefaultNodeContent({ data, isConnectable, }) {
3212
3269
  } })] }));
3213
3270
  }
3214
3271
 
3215
- function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3216
- const { registry } = useWorkbenchContext();
3272
+ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3217
3273
  const rf = useReactFlow();
3218
- const ids = Array.from(registry.nodes.keys());
3219
3274
  const [query, setQuery] = useState("");
3220
3275
  const q = query.trim().toLowerCase();
3221
3276
  const filteredIds = q
3222
- ? ids.filter((id) => id.toLowerCase().includes(q))
3223
- : ids;
3277
+ ? nodeIds.filter((id) => id.toLowerCase().includes(q))
3278
+ : nodeIds;
3224
3279
  const root = { __children: {} };
3225
3280
  for (const id of filteredIds) {
3226
3281
  const parts = id.split(".");
@@ -3244,11 +3299,11 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3244
3299
  if (!ref.current)
3245
3300
  return;
3246
3301
  if (!ref.current.contains(e.target))
3247
- onClose();
3302
+ handlers.onClose();
3248
3303
  };
3249
3304
  const onKey = (e) => {
3250
3305
  if (e.key === "Escape")
3251
- onClose();
3306
+ handlers.onClose();
3252
3307
  };
3253
3308
  window.addEventListener("mousedown", onDown, true);
3254
3309
  window.addEventListener("keydown", onKey);
@@ -3256,7 +3311,7 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3256
3311
  window.removeEventListener("mousedown", onDown, true);
3257
3312
  window.removeEventListener("keydown", onKey);
3258
3313
  };
3259
- }, [open, onClose]);
3314
+ }, [open, handlers]);
3260
3315
  // Focus search input when menu opens
3261
3316
  const inputRef = useRef(null);
3262
3317
  useEffect(() => {
@@ -3275,8 +3330,8 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3275
3330
  const handleClick = (typeId) => {
3276
3331
  // project() is deprecated; use screenToFlowPosition for screen coordinates
3277
3332
  const p = rf.screenToFlowPosition({ x: clientPos.x, y: clientPos.y });
3278
- onAdd(typeId, p);
3279
- onClose();
3333
+ handlers.onAddNode(typeId, { position: p });
3334
+ handlers.onClose();
3280
3335
  };
3281
3336
  const renderTree = (tree, path = []) => {
3282
3337
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
@@ -3298,8 +3353,7 @@ function DefaultContextMenu({ open, clientPos, onAdd, onClose, }) {
3298
3353
  }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsx("div", { className: "px-2 pb-1", children: jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3299
3354
  }
3300
3355
 
3301
- function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3302
- const { wb, runner, engineKind, registry, outputsMap, outputTypesMap } = useWorkbenchContext();
3356
+ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
3303
3357
  const ref = useRef(null);
3304
3358
  // outside click + ESC
3305
3359
  useEffect(() => {
@@ -3309,11 +3363,11 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3309
3363
  if (!ref.current)
3310
3364
  return;
3311
3365
  if (!ref.current.contains(e.target))
3312
- onClose();
3366
+ handlers.onClose();
3313
3367
  };
3314
3368
  const onKey = (e) => {
3315
3369
  if (e.key === "Escape")
3316
- onClose();
3370
+ handlers.onClose();
3317
3371
  };
3318
3372
  window.addEventListener("mousedown", onDown, true);
3319
3373
  window.addEventListener("keydown", onKey);
@@ -3321,48 +3375,26 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3321
3375
  window.removeEventListener("mousedown", onDown, true);
3322
3376
  window.removeEventListener("keydown", onKey);
3323
3377
  };
3324
- }, [open, onClose]);
3378
+ }, [open, handlers]);
3325
3379
  useEffect(() => {
3326
3380
  if (open)
3327
3381
  ref.current?.focus();
3328
3382
  }, [open]);
3329
- // Bake helpers
3330
- const getBakeableOutputs = () => {
3331
- try {
3332
- const def = wb.export();
3333
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3334
- if (!node)
3335
- return [];
3336
- const desc = registry.nodes.get(node.typeId);
3337
- const handles = Object.keys(desc?.outputs || {});
3338
- const out = [];
3339
- for (const h of handles) {
3340
- const tId = outputTypesMap?.[nodeId]?.[h];
3341
- if (!tId)
3342
- continue;
3343
- if (tId.endsWith("[]")) {
3344
- const base = tId.slice(0, -2);
3345
- const tArr = registry.types.get(tId);
3346
- const tElem = registry.types.get(base);
3347
- const arrT = tArr?.bakeTarget;
3348
- const elemT = tElem?.bakeTarget;
3349
- if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3350
- (elemT && registry.nodes.has(elemT.nodeTypeId)))
3351
- out.push(h);
3352
- }
3353
- else {
3354
- const t = registry.types.get(tId);
3355
- const bt = t?.bakeTarget;
3356
- if (bt && registry.nodes.has(bt.nodeTypeId))
3357
- out.push(h);
3358
- }
3359
- }
3360
- return out;
3361
- }
3362
- catch {
3363
- return [];
3364
- }
3365
- };
3383
+ if (!open || !clientPos || !nodeId)
3384
+ return null;
3385
+ // clamp
3386
+ const MENU_MIN_WIDTH = 180;
3387
+ const PADDING = 16;
3388
+ const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3389
+ (MENU_MIN_WIDTH + PADDING));
3390
+ const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3391
+ return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3392
+ e.preventDefault();
3393
+ e.stopPropagation();
3394
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3395
+ }
3396
+
3397
+ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
3366
3398
  const doBake = async (handleId) => {
3367
3399
  try {
3368
3400
  const typeId = outputTypesMap?.[nodeId]?.[handleId];
@@ -3395,7 +3427,6 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3395
3427
  const newId = wb.addNode({
3396
3428
  typeId: singleTarget.nodeTypeId,
3397
3429
  position: { x: pos.x + 180, y: pos.y },
3398
- params: {},
3399
3430
  });
3400
3431
  runner.update(wb.export());
3401
3432
  await runner.whenIdle();
@@ -3406,12 +3437,9 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3406
3437
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3407
3438
  const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3408
3439
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3409
- const newId = `n${Math.random().toString(36).slice(2, 8)}`;
3410
- wb.addNode({
3411
- nodeId: newId,
3440
+ const newId = wb.addNode({
3412
3441
  typeId: arrTarget.nodeTypeId,
3413
3442
  position: { x: pos.x + 180, y: pos.y },
3414
- params: {},
3415
3443
  });
3416
3444
  runner.update(wb.export());
3417
3445
  await runner.whenIdle();
@@ -3429,14 +3457,11 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3429
3457
  const DY = 160;
3430
3458
  const nodeIds = [];
3431
3459
  for (let idx = 0; idx < coercedItems.length; idx++) {
3432
- const cv = coercedItems[idx];
3433
3460
  const col = idx % COLS;
3434
3461
  const row = Math.floor(idx / COLS);
3435
3462
  const newId = wb.addNode({
3436
3463
  typeId: elemTarget.nodeTypeId,
3437
3464
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3438
- params: {},
3439
- initialInputs: { [elemTarget.inputHandle]: structuredClone(cv) },
3440
3465
  });
3441
3466
  nodeIds.push(newId);
3442
3467
  }
@@ -3454,65 +3479,91 @@ function NodeContextMenu({ open, clientPos, nodeId, onClose, }) {
3454
3479
  }
3455
3480
  catch { }
3456
3481
  };
3457
- // actions
3458
- const handleDelete = useCallback(() => {
3459
- wb.removeNode(nodeId);
3460
- onClose();
3461
- }, [nodeId, wb, onClose]);
3462
- const handleDuplicate = useCallback(() => {
3482
+ return {
3483
+ onDelete: () => {
3484
+ wb.removeNode(nodeId);
3485
+ onClose();
3486
+ },
3487
+ onDuplicate: async () => {
3488
+ const def = wb.export();
3489
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3490
+ if (!n)
3491
+ return onClose();
3492
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3493
+ const newId = wb.addNode({
3494
+ typeId: n.typeId,
3495
+ params: n.params,
3496
+ position: { x: pos.x + 24, y: pos.y + 24 },
3497
+ initialInputs: n.initialInputs,
3498
+ resolvedHandles: n.resolvedHandles,
3499
+ });
3500
+ await runner.whenIdle();
3501
+ runner.setInputs(newId, { ...runner.getInputs(def)[nodeId] });
3502
+ onClose();
3503
+ },
3504
+ onRunPull: async () => {
3505
+ try {
3506
+ await runner.computeNode(nodeId);
3507
+ }
3508
+ catch { }
3509
+ onClose();
3510
+ },
3511
+ onBake: async (handleId) => {
3512
+ await doBake(handleId);
3513
+ onClose();
3514
+ },
3515
+ onCopyId: async () => {
3516
+ try {
3517
+ await navigator.clipboard.writeText(nodeId);
3518
+ }
3519
+ catch { }
3520
+ onClose();
3521
+ },
3522
+ onClose,
3523
+ };
3524
+ }
3525
+ function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3526
+ try {
3463
3527
  const def = wb.export();
3464
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3465
- if (!n)
3466
- return onClose();
3467
- const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3468
- wb.addNode({
3469
- typeId: n.typeId,
3470
- params: n.params,
3471
- position: { x: pos.x + 24, y: pos.y + 24 },
3472
- });
3473
- onClose();
3474
- }, [nodeId, wb, onClose]);
3475
- useCallback(async (handleId) => {
3476
- await doBake(handleId);
3477
- onClose();
3478
- }, [doBake, onClose]);
3479
- const handleCopyId = useCallback(async () => {
3480
- try {
3481
- await navigator.clipboard.writeText(nodeId);
3482
- }
3483
- catch { }
3484
- onClose();
3485
- }, [nodeId, onClose]);
3486
- const handleRunPull = useCallback(async () => {
3487
- try {
3488
- await runner.computeNode(nodeId);
3528
+ const node = def.nodes.find((n) => n.nodeId === nodeId);
3529
+ if (!node)
3530
+ return [];
3531
+ const desc = registry.nodes.get(node.typeId);
3532
+ const handles = Object.keys(desc?.outputs || {});
3533
+ const out = [];
3534
+ for (const h of handles) {
3535
+ const tId = outputTypesMap?.[nodeId]?.[h];
3536
+ if (!tId)
3537
+ continue;
3538
+ if (tId.endsWith("[]")) {
3539
+ const base = tId.slice(0, -2);
3540
+ const tArr = registry.types.get(tId);
3541
+ const tElem = registry.types.get(base);
3542
+ const arrT = tArr?.bakeTarget;
3543
+ const elemT = tElem?.bakeTarget;
3544
+ if ((arrT && registry.nodes.has(arrT.nodeTypeId)) ||
3545
+ (elemT && registry.nodes.has(elemT.nodeTypeId)))
3546
+ out.push(h);
3547
+ }
3548
+ else {
3549
+ const t = registry.types.get(tId);
3550
+ const bt = t?.bakeTarget;
3551
+ if (bt && registry.nodes.has(bt.nodeTypeId))
3552
+ out.push(h);
3553
+ }
3489
3554
  }
3490
- catch { }
3491
- onClose();
3492
- }, [nodeId, runner, onClose]);
3493
- if (!open || !clientPos || !nodeId)
3494
- return null;
3495
- // clamp
3496
- const MENU_MIN_WIDTH = 180;
3497
- const PADDING = 16;
3498
- const x = Math.min(clientPos.x, (typeof window !== "undefined" ? window.innerWidth : 0) -
3499
- (MENU_MIN_WIDTH + PADDING));
3500
- const y = Math.min(clientPos.y, (typeof window !== "undefined" ? window.innerHeight : 0) - 240);
3501
- const canRunPull = engineKind()?.toString() === "pull";
3502
- const outs = getBakeableOutputs();
3503
- return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3504
- e.preventDefault();
3505
- e.stopPropagation();
3506
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), outs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), outs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: async () => {
3507
- await doBake(h);
3508
- onClose();
3509
- }, children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handleCopyId, children: "Copy Node ID" })] }));
3555
+ return out;
3556
+ }
3557
+ catch {
3558
+ return [];
3559
+ }
3510
3560
  }
3511
3561
 
3512
3562
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
3513
- const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, } = useWorkbenchContext();
3563
+ const { wb, registry, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, valuesTick, nodeStatus, edgeStatus, validationByNode, validationByEdge, uiVersion, runner, engineKind, } = useWorkbenchContext();
3514
3564
  const nodeValidation = validationByNode;
3515
3565
  const edgeValidation = validationByEdge.errors;
3566
+ const [registryVersion, setRegistryVersion] = useState(0);
3516
3567
  // Keep stable references for nodes/edges to avoid unnecessary updates
3517
3568
  const prevNodesRef = useRef([]);
3518
3569
  const prevEdgesRef = useRef([]);
@@ -3577,9 +3628,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3577
3628
  },
3578
3629
  }));
3579
3630
  const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
3631
+ const ui = wb.getUI();
3580
3632
  const { nodeTypes, resolveNodeType } = useMemo(() => {
3581
3633
  // Build nodeTypes map using UI extension registry
3582
- const ui = wb.getUI();
3583
3634
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
3584
3635
  const def = wb.export();
3585
3636
  const ids = new Set([
@@ -3601,7 +3652,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3601
3652
  const resolver = (nodeTypeId) => custom.has(nodeTypeId) ? `spark-${nodeTypeId}` : "spark-default";
3602
3653
  return { nodeTypes: types, resolveNodeType: resolver };
3603
3654
  // Include uiVersion to recompute when custom renderers are registered
3604
- }, [wb, registry, uiVersion]);
3655
+ }, [wb, registry, uiVersion, ui]);
3605
3656
  const { nodes, edges } = useMemo(() => {
3606
3657
  const def = wb.export();
3607
3658
  const sel = wb.getSelection();
@@ -3765,15 +3816,65 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3765
3816
  setNodeMenuOpen(false);
3766
3817
  }
3767
3818
  };
3768
- const addNodeAt = useCallback((typeId, pos) => {
3769
- wb.addNode({ typeId, position: pos });
3770
- }, [wb]);
3819
+ const addNodeAt = useCallback(async (typeId, opts) => {
3820
+ const nodeId = wb.addNode({
3821
+ typeId,
3822
+ initialInputs: opts.initialInputs,
3823
+ position: opts.position,
3824
+ });
3825
+ if (opts.inputs) {
3826
+ runner.update(wb.export());
3827
+ await runner.whenIdle();
3828
+ runner.setInputs(nodeId, opts.inputs);
3829
+ }
3830
+ }, [wb, runner]);
3771
3831
  const onCloseMenu = useCallback(() => {
3772
3832
  setMenuOpen(false);
3773
3833
  }, []);
3774
3834
  const onCloseNodeMenu = useCallback(() => {
3775
3835
  setNodeMenuOpen(false);
3776
3836
  }, []);
3837
+ useEffect(() => {
3838
+ const off = runner.on("registry", () => {
3839
+ setRegistryVersion((v) => v + 1);
3840
+ });
3841
+ return () => off();
3842
+ }, [runner]);
3843
+ const nodeIds = useMemo(() => Array.from(registry.nodes.keys()), [registry, registryVersion]);
3844
+ const defaultContextMenuHandlers = useMemo(() => ({
3845
+ onAddNode: addNodeAt,
3846
+ onClose: onCloseMenu,
3847
+ }), [addNodeAt, onCloseMenu]);
3848
+ const nodeContextMenuHandlers = useMemo(() => {
3849
+ if (!nodeAtMenu)
3850
+ return null;
3851
+ return createNodeContextMenuHandlers(nodeAtMenu, wb, runner, registry, outputsMap, outputTypesMap, onCloseNodeMenu);
3852
+ }, [
3853
+ nodeAtMenu,
3854
+ wb,
3855
+ runner,
3856
+ registry,
3857
+ outputsMap,
3858
+ outputTypesMap,
3859
+ onCloseNodeMenu,
3860
+ ]);
3861
+ const canRunPull = useMemo(() => engineKind()?.toString() === "pull", [engineKind]);
3862
+ const bakeableOutputs = useMemo(() => {
3863
+ if (!nodeAtMenu)
3864
+ return [];
3865
+ return getBakeableOutputs(nodeAtMenu, wb, registry, outputTypesMap);
3866
+ }, [nodeAtMenu, wb, registry, outputTypesMap]);
3867
+ // Get custom renderers from UI extension registry (reactive to uiVersion changes)
3868
+ const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = useMemo(() => {
3869
+ return {
3870
+ BackgroundRenderer: ui.getBackgroundRenderer(),
3871
+ MinimapRenderer: ui.getMinimapRenderer(),
3872
+ ControlsRenderer: ui.getControlsRenderer(),
3873
+ DefaultContextMenuRenderer: ui.getDefaultContextMenuRenderer(),
3874
+ NodeContextMenuRenderer: ui.getNodeContextMenuRenderer(),
3875
+ connectionLineRenderer: ui.getConnectionLineRenderer(),
3876
+ };
3877
+ }, [ui, uiVersion]);
3777
3878
  const onMoveEnd = useCallback(() => {
3778
3879
  if (rfInstanceRef.current) {
3779
3880
  const viewport = rfInstanceRef.current.getViewport();
@@ -3798,7 +3899,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3798
3899
  });
3799
3900
  }
3800
3901
  });
3801
- return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsx(ReactFlowProvider, { children: jsxs(ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, selectionOnDrag: true, onInit: (inst) => {
3902
+ return (jsx("div", { className: "w-full h-full", onContextMenu: onContextMenu, children: jsx(ReactFlowProvider, { children: jsxs(ReactFlow, { nodes: throttled.nodes, edges: throttled.edges, nodeTypes: nodeTypes, connectionLineComponent: connectionLineRenderer, selectionOnDrag: true, onInit: (inst) => {
3802
3903
  rfInstanceRef.current = inst;
3803
3904
  const savedViewport = wb.getViewport();
3804
3905
  if (savedViewport) {
@@ -3809,7 +3910,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3809
3910
  zoom: savedViewport.zoom,
3810
3911
  });
3811
3912
  }
3812
- }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 }), jsx(MiniMap, {}), jsx(Controls, {}), jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, onAdd: addNodeAt, onClose: onCloseMenu }), !!nodeAtMenu && (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, onClose: onCloseNodeMenu }))] }) }) }));
3913
+ }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsx(BackgroundRenderer, {})) : (jsx(Background, { id: "workbench-canvas-background", variant: BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsx(MinimapRenderer, {}) : jsx(MiniMap, {}), ControlsRenderer ? jsx(ControlsRenderer, {}) : jsx(Controls, {}), DefaultContextMenuRenderer ? (jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
3914
+ nodeContextMenuHandlers &&
3915
+ (NodeContextMenuRenderer ? (jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })))] }) }) }));
3813
3916
  });
3814
3917
 
3815
3918
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
@@ -4340,5 +4443,5 @@ function WorkbenchStudio({ engine, onEngineChange, example, onExampleChange, bac
4340
4443
  return (jsx(WorkbenchProvider, { wb: wb, runner: runner, registry: registry, setRegistry: setRegistry, overrides: overrides, uiVersion: uiVersion, children: jsx(WorkbenchStudioCanvas, { setRegistry: setRegistry, autoScroll: autoScroll, onAutoScrollChange: onAutoScrollChange, example: example, onExampleChange: onExampleChange, engine: engine, onEngineChange: onEngineChange, backendKind: backendKind, onBackendKindChange: onBackendKindChangeWithDispose, 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 }) }));
4341
4444
  }
4342
4445
 
4343
- export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleClassName, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
4446
+ export { AbstractWorkbench, CLIWorkbench, DefaultNode, DefaultNodeContent, DefaultNodeHeader, DefaultUIExtensionRegistry, InMemoryWorkbench, Inspector, LocalGraphRunner, NodeHandles, RemoteGraphRunner, WorkbenchCanvas, WorkbenchContext, WorkbenchProvider, WorkbenchStudio, computeEffectiveHandles, countVisibleHandles, createHandleBounds, createHandleLayout, download, estimateNodeSize, formatDataUrlAsLabel, formatDeclaredTypeSignature, getHandleBoundsX, getHandleBoundsY, getHandleClassName, getHandleLayoutY, getNodeBorderClassNames, layoutNode, preformatValueForDisplay, prettyHandle, resolveOutputDisplay, summarizeDeep, toReactFlow, upload, useQueryParamBoolean, useQueryParamString, useThrottledValue, useWorkbenchBridge, useWorkbenchContext, useWorkbenchGraphTick, useWorkbenchGraphUiTick, useWorkbenchVersionTick };
4344
4447
  //# sourceMappingURL=index.js.map