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