@bian-womp/spark-workbench 0.2.83 → 0.2.85

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 (53) hide show
  1. package/lib/cjs/index.cjs +344 -238
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +27 -28
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/contracts.d.ts +4 -1
  6. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  7. package/lib/cjs/src/core/ui-extensions.d.ts +16 -7
  8. package/lib/cjs/src/core/ui-extensions.d.ts.map +1 -1
  9. package/lib/cjs/src/index.d.ts +2 -0
  10. package/lib/cjs/src/index.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/DefaultNode.d.ts +0 -14
  12. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/DefaultNodeContent.d.ts +4 -0
  14. package/lib/cjs/src/misc/DefaultNodeContent.d.ts.map +1 -0
  15. package/lib/cjs/src/misc/DefaultNodeHeader.d.ts +15 -0
  16. package/lib/cjs/src/misc/DefaultNodeHeader.d.ts.map +1 -0
  17. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts +2 -0
  18. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  19. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  20. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  21. package/lib/cjs/src/misc/mapping.d.ts +9 -2
  22. package/lib/cjs/src/misc/mapping.d.ts.map +1 -1
  23. package/lib/cjs/src/misc/merge-utils.d.ts +6 -1
  24. package/lib/cjs/src/misc/merge-utils.d.ts.map +1 -1
  25. package/lib/cjs/src/misc/types.d.ts +4 -0
  26. package/lib/cjs/src/misc/types.d.ts.map +1 -1
  27. package/lib/esm/index.js +346 -240
  28. package/lib/esm/index.js.map +1 -1
  29. package/lib/esm/src/core/InMemoryWorkbench.d.ts +27 -28
  30. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  31. package/lib/esm/src/core/contracts.d.ts +4 -1
  32. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  33. package/lib/esm/src/core/ui-extensions.d.ts +16 -7
  34. package/lib/esm/src/core/ui-extensions.d.ts.map +1 -1
  35. package/lib/esm/src/index.d.ts +2 -0
  36. package/lib/esm/src/index.d.ts.map +1 -1
  37. package/lib/esm/src/misc/DefaultNode.d.ts +0 -14
  38. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  39. package/lib/esm/src/misc/DefaultNodeContent.d.ts +4 -0
  40. package/lib/esm/src/misc/DefaultNodeContent.d.ts.map +1 -0
  41. package/lib/esm/src/misc/DefaultNodeHeader.d.ts +15 -0
  42. package/lib/esm/src/misc/DefaultNodeHeader.d.ts.map +1 -0
  43. package/lib/esm/src/misc/WorkbenchCanvas.d.ts +2 -0
  44. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  45. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  46. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  47. package/lib/esm/src/misc/mapping.d.ts +9 -2
  48. package/lib/esm/src/misc/mapping.d.ts.map +1 -1
  49. package/lib/esm/src/misc/merge-utils.d.ts +6 -1
  50. package/lib/esm/src/misc/merge-utils.d.ts.map +1 -1
  51. package/lib/esm/src/misc/types.d.ts +4 -0
  52. package/lib/esm/src/misc/types.d.ts.map +1 -1
  53. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -75,6 +75,13 @@ class DefaultUIExtensionRegistry {
75
75
  getNodeContextMenuRenderer() {
76
76
  return this.nodeContextMenuRenderer;
77
77
  }
78
+ registerSelectionContextMenuRenderer(renderer) {
79
+ this.selectionContextMenuRenderer = renderer;
80
+ return this;
81
+ }
82
+ getSelectionContextMenuRenderer() {
83
+ return this.selectionContextMenuRenderer;
84
+ }
78
85
  // Layout function overrides
79
86
  registerEstimateNodeSize(override) {
80
87
  this.estimateNodeSizeOverride = override;
@@ -126,6 +133,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
126
133
  this._def = { nodes: [], edges: [] };
127
134
  this.listeners = new Map();
128
135
  this.positions = {};
136
+ this.sizes = {};
129
137
  this.selection = {
130
138
  nodes: [],
131
139
  edges: [],
@@ -151,9 +159,11 @@ class InMemoryWorkbench extends AbstractWorkbench {
151
159
  const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
152
160
  const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
153
161
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
162
+ const filteredSizes = Object.fromEntries(Object.entries(this.sizes).filter(([id]) => defNodeIds.has(id)));
154
163
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
155
164
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
156
165
  this.positions = filteredPositions;
166
+ this.sizes = filteredSizes;
157
167
  this.selection = { nodes: filteredNodes, edges: filteredEdges };
158
168
  this.emit("graphChanged", { def: this._def });
159
169
  this.refreshValidation();
@@ -210,8 +220,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
210
220
  params: node.params,
211
221
  resolvedHandles: node.resolvedHandles,
212
222
  });
213
- if (node.position)
214
- this.positions[id] = node.position;
223
+ if (options?.position)
224
+ this.positions[id] = options.position;
225
+ if (options?.size)
226
+ this.sizes[id] = options.size;
215
227
  this.emit("graphChanged", {
216
228
  def: this._def,
217
229
  change: {
@@ -229,6 +241,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
229
241
  this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
230
242
  this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
231
243
  delete this.positions[nodeId];
244
+ delete this.sizes[nodeId];
232
245
  delete this.nodeNames[nodeId];
233
246
  this.emit("graphChanged", {
234
247
  def: this._def,
@@ -287,27 +300,38 @@ class InMemoryWorkbench extends AbstractWorkbench {
287
300
  });
288
301
  }
289
302
  // Position and selection APIs for React Flow bridge
290
- setPositions(map, options) {
291
- this.positions = { ...this.positions, ...map };
303
+ setPositions(positions, options) {
304
+ this.positions = { ...this.positions, ...positions };
305
+ this.emit("graphUiChanged", { change: { type: "moveNodes" }, ...options });
306
+ }
307
+ getPositions() {
308
+ return { ...this.positions };
309
+ }
310
+ setSizes(sizes, options) {
311
+ const updatedSizes = { ...this.sizes };
312
+ for (const [nodeId, size] of Object.entries(sizes)) {
313
+ if (size) {
314
+ updatedSizes[nodeId] = size;
315
+ }
316
+ else {
317
+ delete updatedSizes[nodeId];
318
+ }
319
+ }
320
+ this.sizes = updatedSizes;
292
321
  this.emit("graphUiChanged", {
293
- def: this._def,
294
- change: { type: "moveNodes" },
322
+ change: { type: "resizeNodes" },
295
323
  ...options,
296
324
  });
297
325
  }
298
- getPositions() {
299
- return { ...this.positions };
326
+ getSizes() {
327
+ return { ...this.sizes };
300
328
  }
301
329
  setSelectionInternal(sel, options) {
302
330
  if (lod.isEqual(this.selection, sel))
303
331
  return;
304
332
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
305
333
  this.emit("selectionChanged", this.selection);
306
- this.emit("graphUiChanged", {
307
- def: this._def,
308
- change: { type: "selection" },
309
- ...options,
310
- });
334
+ this.emit("graphUiChanged", { change: { type: "selection" }, ...options });
311
335
  }
312
336
  setSelection(sel, options) {
313
337
  this.setSelectionInternal(sel, options);
@@ -338,10 +362,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
338
362
  if (lod.isEqual(this.viewport, viewport))
339
363
  return;
340
364
  this.viewport = { ...viewport };
341
- this.emit("graphUiChanged", {
342
- def: this._def,
343
- change: { type: "viewport" },
344
- });
365
+ this.emit("graphUiChanged", { change: { type: "viewport" } });
345
366
  }
346
367
  getViewport() {
347
368
  return this.viewport ? { ...this.viewport } : null;
@@ -353,6 +374,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
353
374
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
354
375
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
355
376
  const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
377
+ const filteredSizes = Object.fromEntries(Object.entries(this.sizes).filter(([id]) => defNodeIds.has(id)));
356
378
  return {
357
379
  positions: Object.keys(filteredPositions).length > 0
358
380
  ? filteredPositions
@@ -367,6 +389,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
367
389
  nodeNames: Object.keys(filteredNodeNames).length > 0
368
390
  ? filteredNodeNames
369
391
  : undefined,
392
+ sizes: Object.keys(filteredSizes).length > 0 ? filteredSizes : undefined,
370
393
  };
371
394
  }
372
395
  setUIState(ui) {
@@ -381,12 +404,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
381
404
  edges: [...ui.selection.edges],
382
405
  };
383
406
  this.emit("selectionChanged", this.selection);
407
+ this.emit("graphUiChanged", {
408
+ change: { type: "selection" },
409
+ init: true,
410
+ });
384
411
  }
385
412
  if (ui.viewport) {
386
413
  this.viewport = { ...ui.viewport };
414
+ this.emit("graphUiChanged", { change: { type: "viewport" }, init: true });
387
415
  }
388
416
  if (ui.nodeNames !== undefined) {
389
417
  this.nodeNames = { ...ui.nodeNames };
418
+ for (const [nodeId, name] of Object.entries(ui.nodeNames)) {
419
+ this.emit("graphUiChanged", {
420
+ change: { type: "nodeName", nodeId, name },
421
+ init: true,
422
+ });
423
+ }
424
+ }
425
+ if (ui.sizes !== undefined) {
426
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
427
+ this.sizes = Object.fromEntries(Object.entries(ui.sizes).filter(([id]) => defNodeIds.has(id)));
390
428
  }
391
429
  }
392
430
  getRuntimeState() {
@@ -437,6 +475,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
437
475
  if (selection.nodes.length === 0)
438
476
  return null;
439
477
  const positions = this.getPositions();
478
+ const sizes = this.getSizes();
440
479
  const selectedNodeSet = new Set(selection.nodes);
441
480
  // Collect nodes to copy
442
481
  const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
@@ -448,15 +487,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
448
487
  let maxX = -Infinity;
449
488
  let maxY = -Infinity;
450
489
  nodesToCopy.forEach((node) => {
451
- const pos = positions[node.nodeId] || { x: 0, y: 0 };
452
- const size = getNodeSize?.(node.nodeId, node.typeId) || {
453
- width: 200,
454
- height: 100,
455
- };
456
- minX = Math.min(minX, pos.x);
457
- minY = Math.min(minY, pos.y);
458
- maxX = Math.max(maxX, pos.x + size.width);
459
- maxY = Math.max(maxY, pos.y + size.height);
490
+ const pos = positions[node.nodeId];
491
+ const size = sizes[node.nodeId] || getNodeSize?.(node.nodeId, node.typeId);
492
+ if (pos) {
493
+ minX = Math.min(minX, pos.x);
494
+ minY = Math.min(minY, pos.y);
495
+ if (size) {
496
+ maxX = Math.max(maxX, pos.x + size.width);
497
+ maxY = Math.max(maxY, pos.y + size.height);
498
+ }
499
+ }
460
500
  });
461
501
  const bounds = { minX, minY, maxX, maxY };
462
502
  const centerX = (bounds.minX + bounds.maxX) / 2;
@@ -466,7 +506,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
466
506
  const allInputs = runner.getInputs(this.def);
467
507
  const selectedEdgeSet = new Set(selection.edges);
468
508
  const copiedNodes = nodesToCopy.map((node) => {
469
- const pos = positions[node.nodeId] || { x: 0, y: 0 };
509
+ const pos = positions[node.nodeId];
510
+ const size = sizes[node.nodeId];
470
511
  // Get all inbound edges for this node
471
512
  const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
472
513
  // Build set of handles that have inbound edges
@@ -483,10 +524,13 @@ class InMemoryWorkbench extends AbstractWorkbench {
483
524
  typeId: node.typeId,
484
525
  params: node.params,
485
526
  resolvedHandles: node.resolvedHandles,
486
- position: {
487
- x: pos.x - centerX,
488
- y: pos.y - centerY,
489
- },
527
+ position: pos
528
+ ? {
529
+ x: pos.x - centerX,
530
+ y: pos.y - centerY,
531
+ }
532
+ : undefined,
533
+ size,
490
534
  inputs: inputsToCopy,
491
535
  originalNodeId: node.nodeId,
492
536
  };
@@ -521,6 +565,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
521
565
  if (selection.nodes.length === 0)
522
566
  return [];
523
567
  const positions = this.getPositions();
568
+ const sizes = this.getSizes();
524
569
  const newNodes = [];
525
570
  // Get inputs without bindings (literal values only)
526
571
  const allInputs = runner.getInputs(this.def) || {};
@@ -529,7 +574,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
529
574
  const n = this.def.nodes.find((n) => n.nodeId === nodeId);
530
575
  if (!n)
531
576
  continue;
532
- const pos = positions[nodeId] || { x: 0, y: 0 };
577
+ const pos = positions[nodeId];
578
+ const size = sizes[nodeId];
533
579
  const inboundHandles = new Set(this.def.edges
534
580
  .filter((e) => e.target.nodeId === nodeId)
535
581
  .map((e) => e.target.handle));
@@ -537,10 +583,11 @@ class InMemoryWorkbench extends AbstractWorkbench {
537
583
  const newNodeId = this.addNode({
538
584
  typeId: n.typeId,
539
585
  params: n.params,
540
- position: { x: pos.x + 24, y: pos.y + 24 },
541
586
  resolvedHandles: n.resolvedHandles,
542
587
  }, {
543
588
  inputs: inputsWithoutBindings,
589
+ position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
590
+ size,
544
591
  copyOutputsFrom: nodeId,
545
592
  dry: true,
546
593
  });
@@ -587,8 +634,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
587
634
  const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
588
635
  newNodeId = this.addNode({
589
636
  typeId: singleTarget.nodeTypeId,
637
+ }, {
638
+ inputs: { [singleTarget.inputHandle]: coerced },
590
639
  position: { x: pos.x + 180, y: pos.y },
591
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
640
+ });
592
641
  }
593
642
  else if (isArray && arrTarget) {
594
643
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
@@ -596,8 +645,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
596
645
  const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
597
646
  newNodeId = this.addNode({
598
647
  typeId: arrTarget.nodeTypeId,
648
+ }, {
599
649
  position: { x: pos.x + 180, y: pos.y },
600
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
650
+ inputs: { [arrTarget.inputHandle]: coerced },
651
+ });
601
652
  }
602
653
  else if (isArray && elemTarget) {
603
654
  const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
@@ -613,8 +664,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
613
664
  const row = Math.floor(idx / COLS);
614
665
  newNodeId = this.addNode({
615
666
  typeId: elemTarget.nodeTypeId,
667
+ }, {
616
668
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
617
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
669
+ inputs: { [elemTarget.inputHandle]: coercedItems[idx] },
670
+ });
618
671
  }
619
672
  }
620
673
  if (newNodeId) {
@@ -636,7 +689,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
636
689
  const n = this.def.nodes.find((n) => n.nodeId === nodeId);
637
690
  if (!n)
638
691
  return undefined;
639
- const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
692
+ const pos = this.getPositions()[nodeId];
693
+ const size = this.getSizes()[nodeId];
640
694
  // Get inputs without bindings (literal values only)
641
695
  const allInputs = runner.getInputs(this.def)[nodeId] || {};
642
696
  const inboundHandles = new Set(this.def.edges
@@ -646,10 +700,11 @@ class InMemoryWorkbench extends AbstractWorkbench {
646
700
  const newNodeId = this.addNode({
647
701
  typeId: n.typeId,
648
702
  params: n.params,
649
- position: { x: pos.x + 24, y: pos.y + 24 },
650
703
  resolvedHandles: n.resolvedHandles,
651
704
  }, {
652
705
  inputs: inputsWithoutBindings,
706
+ position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
707
+ size,
653
708
  copyOutputsFrom: nodeId,
654
709
  dry: true,
655
710
  });
@@ -667,17 +722,19 @@ class InMemoryWorkbench extends AbstractWorkbench {
667
722
  const n = this.def.nodes.find((n) => n.nodeId === nodeId);
668
723
  if (!n)
669
724
  return undefined;
670
- const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
725
+ const pos = this.getPositions()[nodeId];
726
+ const size = this.getSizes()[nodeId];
671
727
  // Get all inputs (including those with bindings, since edges will be duplicated)
672
728
  const inputs = runner.getInputs(this.def)[nodeId] || {};
673
729
  // Add the duplicated node
674
730
  const newNodeId = this.addNode({
675
731
  typeId: n.typeId,
676
732
  params: n.params,
677
- position: { x: pos.x + 24, y: pos.y + 24 },
678
733
  resolvedHandles: n.resolvedHandles,
679
734
  }, {
680
735
  inputs,
736
+ position: pos ? { x: pos.x + 24, y: pos.y + 24 } : undefined,
737
+ size,
681
738
  copyOutputsFrom: nodeId,
682
739
  dry: true,
683
740
  });
@@ -711,12 +768,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
711
768
  typeId: nodeData.typeId,
712
769
  params: nodeData.params,
713
770
  resolvedHandles: nodeData.resolvedHandles,
714
- position: {
715
- x: nodeData.position.x + center.x,
716
- y: nodeData.position.y + center.y,
717
- },
718
771
  }, {
719
772
  inputs: nodeData.inputs,
773
+ position: nodeData.position
774
+ ? {
775
+ x: nodeData.position.x + center.x,
776
+ y: nodeData.position.y + center.y,
777
+ }
778
+ : undefined,
779
+ size: nodeData.size,
720
780
  copyOutputsFrom: nodeData.originalNodeId,
721
781
  dry: true,
722
782
  });
@@ -775,8 +835,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
775
835
  this.nodeNames[nodeId] = name.trim();
776
836
  }
777
837
  this.emit("graphUiChanged", {
778
- def: this._def,
779
- change: { type: "nodeName", nodeId },
838
+ change: { type: "nodeName", nodeId, name },
780
839
  ...options,
781
840
  });
782
841
  }
@@ -2196,7 +2255,6 @@ function useWorkbenchBridge(wb) {
2196
2255
  }, { commit: true });
2197
2256
  }, [wb]);
2198
2257
  const onNodesChange = React.useCallback((changes) => {
2199
- // Apply position updates continuously, but mark commit only on drag end
2200
2258
  const positions = {};
2201
2259
  let commit = false;
2202
2260
  changes.forEach((c) => {
@@ -2206,6 +2264,14 @@ function useWorkbenchBridge(wb) {
2206
2264
  commit = true;
2207
2265
  }
2208
2266
  });
2267
+ const sizes = {};
2268
+ changes.forEach((c) => {
2269
+ if (c.type === "dimensions" && c.dimensions) {
2270
+ sizes[c.id] = c.dimensions;
2271
+ if (!c.resizing)
2272
+ commit = true;
2273
+ }
2274
+ });
2209
2275
  // Derive next node selection from change set
2210
2276
  const current = wb.getSelection();
2211
2277
  const nextNodeIds = new Set(current.nodes);
@@ -2240,7 +2306,12 @@ function useWorkbenchBridge(wb) {
2240
2306
  });
2241
2307
  }
2242
2308
  if (Object.keys(positions).length > 0) {
2243
- wb.setPositions(positions, { commit });
2309
+ wb.setPositions(positions, {
2310
+ commit: commit && !Object.keys(sizes).length,
2311
+ });
2312
+ }
2313
+ if (Object.keys(sizes).length > 0) {
2314
+ wb.setSizes(sizes, { commit });
2244
2315
  }
2245
2316
  }, [wb]);
2246
2317
  const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
@@ -2430,7 +2501,7 @@ function useQueryParamString(key, defaultValue) {
2430
2501
  return [val, set];
2431
2502
  }
2432
2503
 
2433
- function toReactFlow(def, positions, registry, opts) {
2504
+ function toReactFlow(def, positions, sizes, registry, opts) {
2434
2505
  const EDGE_STYLE_MISSING = { stroke: "#f59e0b", strokeWidth: 2 }; // amber-500
2435
2506
  const EDGE_STYLE_ERROR = { stroke: "#ef4444", strokeWidth: 2 };
2436
2507
  const EDGE_STYLE_RUNNING = { stroke: "#3b82f6" };
@@ -2474,45 +2545,41 @@ function toReactFlow(def, positions, registry, opts) {
2474
2545
  const createHandleBoundsFn = opts.ui?.getCreateHandleBounds() ?? createHandleBounds;
2475
2546
  const createHandleLayoutFn = opts.ui?.getCreateHandleLayout() ?? createHandleLayout;
2476
2547
  const estimateNodeSizeFn = opts.ui?.getEstimateNodeSize() ?? estimateNodeSize;
2477
- const nodes = def.nodes.map((n) => {
2478
- const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
2479
- const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
2480
- // If layoutNode is overridden, use it directly; otherwise use default with internal overrides
2481
- const geom = layoutNodeOverride
2548
+ const computeLayout = (node, overrides) => {
2549
+ return layoutNodeOverride
2482
2550
  ? layoutNodeOverride({
2483
- node: n,
2551
+ node,
2484
2552
  registry,
2485
2553
  showValues: opts.showValues,
2486
- overrides: overrideSize,
2554
+ overrides,
2487
2555
  })
2488
2556
  : layoutNode({
2489
- node: n,
2557
+ node,
2490
2558
  registry,
2491
2559
  showValues: opts.showValues,
2492
- overrides: overrideSize,
2560
+ overrides,
2493
2561
  }, {
2494
2562
  estimateNodeSize: estimateNodeSizeFn,
2495
2563
  createHandleBounds: createHandleBoundsFn,
2496
2564
  createHandleLayout: createHandleLayoutFn,
2497
2565
  });
2498
- const inputHandles = geom.inputOrder.map((id) => ({
2499
- id,
2500
- typeId: sparkGraph.getInputTypeId(inputSource, id),
2501
- }));
2502
- const outputHandles = geom.outputOrder.map((id) => ({
2503
- id,
2504
- typeId: formatDeclaredTypeSignature(outputSource[id]),
2505
- }));
2506
- nodeHandleMap[n.nodeId] = {
2507
- inputs: new Set(inputHandles.map((h) => h.id)),
2508
- outputs: new Set(outputHandles.map((h) => h.id)),
2509
- };
2510
- // Append placeholder entries for any missing handles (below valid ones)
2566
+ };
2567
+ const calculateDimensionsWithMissingHandles = (geom, extraInputs, extraOutputs) => {
2511
2568
  const baseLeftCount = geom.inputOrder.length;
2512
2569
  const baseRightCount = geom.outputOrder.length;
2513
- const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
2514
- const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
2515
- const extraHandleLayoutLeft = extraInputs.map((id, i) => ({
2570
+ const baseRows = Math.max(baseLeftCount, baseRightCount);
2571
+ const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
2572
+ return {
2573
+ baseLeftCount,
2574
+ baseRightCount,
2575
+ baseRows,
2576
+ newRows,
2577
+ width: geom.width,
2578
+ height: geom.height + Math.max(0, newRows - baseRows) * NODE_ROW_HEIGHT_PX,
2579
+ };
2580
+ };
2581
+ const createMissingHandleLayouts = (extraInputs, extraOutputs, baseLeftCount, baseRightCount) => {
2582
+ const left = extraInputs.map((id, i) => ({
2516
2583
  ...createHandleLayoutFn({
2517
2584
  id,
2518
2585
  type: "target",
@@ -2521,7 +2588,7 @@ function toReactFlow(def, positions, registry, opts) {
2521
2588
  }),
2522
2589
  missing: true,
2523
2590
  }));
2524
- const extraHandleLayoutRight = extraOutputs.map((id, i) => ({
2591
+ const right = extraOutputs.map((id, i) => ({
2525
2592
  ...createHandleLayoutFn({
2526
2593
  id,
2527
2594
  type: "source",
@@ -2530,36 +2597,58 @@ function toReactFlow(def, positions, registry, opts) {
2530
2597
  }),
2531
2598
  missing: true,
2532
2599
  }));
2533
- const handleLayout = [
2534
- ...geom.handleLayout,
2535
- ...extraHandleLayoutLeft,
2536
- ...extraHandleLayoutRight,
2537
- ];
2538
- // Precompute handle bounds (including missing) so edges can render immediately
2539
- const missingBoundsLeft = extraInputs.map((id, i) => createHandleBoundsFn({
2600
+ return [...left, ...right];
2601
+ };
2602
+ const createMissingHandleBounds = (extraInputs, extraOutputs, baseLeftCount, baseRightCount, nodeWidth) => {
2603
+ const left = extraInputs.map((id, i) => createHandleBoundsFn({
2540
2604
  id,
2541
2605
  type: "target",
2542
2606
  position: react.Position.Left,
2543
2607
  rowIndex: baseLeftCount + i,
2544
- nodeWidth: geom.width,
2608
+ nodeWidth,
2545
2609
  }));
2546
- const missingBoundsRight = extraOutputs.map((id, i) => createHandleBoundsFn({
2610
+ const right = extraOutputs.map((id, i) => createHandleBoundsFn({
2547
2611
  id,
2548
2612
  type: "source",
2549
2613
  position: react.Position.Right,
2550
2614
  rowIndex: baseRightCount + i,
2551
- nodeWidth: geom.width,
2615
+ nodeWidth,
2552
2616
  }));
2553
- const handles = [
2554
- ...geom.handles,
2555
- ...missingBoundsLeft,
2556
- ...missingBoundsRight,
2557
- ];
2558
- // Adjust node height to accommodate missing handle rows
2559
- const baseRows = Math.max(baseLeftCount, baseRightCount);
2560
- const newRows = Math.max(baseLeftCount + extraInputs.length, baseRightCount + extraOutputs.length);
2561
- const initialWidth = geom.width;
2562
- const initialHeight = geom.height + Math.max(0, newRows - baseRows) * NODE_ROW_HEIGHT_PX;
2617
+ return [...left, ...right];
2618
+ };
2619
+ const nodes = def.nodes.map((n) => {
2620
+ const { inputs: inputSource, outputs: outputSource } = computeEffectiveHandles(n, registry);
2621
+ const overrideSize = opts.getDefaultNodeSize?.(n.typeId);
2622
+ const customSize = sizes?.[n.nodeId];
2623
+ const sizeOverrides = customSize
2624
+ ? { ...overrideSize, width: customSize.width, height: customSize.height }
2625
+ : overrideSize;
2626
+ const extraInputs = Array.from(missingInputsByNode[n.nodeId] || []);
2627
+ const extraOutputs = Array.from(missingOutputsByNode[n.nodeId] || []);
2628
+ const geom = computeLayout(n, sizeOverrides);
2629
+ const finalDims = calculateDimensionsWithMissingHandles(geom, extraInputs, extraOutputs);
2630
+ const renderWidth = customSize?.width ?? finalDims.width;
2631
+ const renderHeight = customSize?.height ?? finalDims.height;
2632
+ const initialGeom = customSize ? computeLayout(n, overrideSize) : geom;
2633
+ const initialDims = customSize
2634
+ ? calculateDimensionsWithMissingHandles(initialGeom, extraInputs, extraOutputs)
2635
+ : finalDims;
2636
+ const inputHandles = geom.inputOrder.map((id) => ({
2637
+ id,
2638
+ typeId: sparkGraph.getInputTypeId(inputSource, id),
2639
+ }));
2640
+ const outputHandles = geom.outputOrder.map((id) => ({
2641
+ id,
2642
+ typeId: formatDeclaredTypeSignature(outputSource[id]),
2643
+ }));
2644
+ nodeHandleMap[n.nodeId] = {
2645
+ inputs: new Set(inputHandles.map((h) => h.id)),
2646
+ outputs: new Set(outputHandles.map((h) => h.id)),
2647
+ };
2648
+ const missingHandleLayouts = createMissingHandleLayouts(extraInputs, extraOutputs, finalDims.baseLeftCount, finalDims.baseRightCount);
2649
+ const handleLayout = [...geom.handleLayout, ...missingHandleLayouts];
2650
+ const missingHandleBounds = createMissingHandleBounds(extraInputs, extraOutputs, finalDims.baseLeftCount, finalDims.baseRightCount, renderWidth);
2651
+ const handles = [...geom.handles, ...missingHandleBounds];
2563
2652
  return {
2564
2653
  id: n.nodeId,
2565
2654
  data: {
@@ -2573,8 +2662,10 @@ function toReactFlow(def, positions, registry, opts) {
2573
2662
  ])),
2574
2663
  handleLayout,
2575
2664
  showValues: opts.showValues,
2576
- renderWidth: initialWidth,
2577
- renderHeight: initialHeight,
2665
+ renderWidth,
2666
+ renderHeight,
2667
+ initialWidth: initialDims.width,
2668
+ initialHeight: initialDims.height,
2578
2669
  inputValues: opts.inputs?.[n.nodeId],
2579
2670
  inputDefaults: opts.inputDefaults?.[n.nodeId],
2580
2671
  outputValues: opts.outputs?.[n.nodeId],
@@ -2592,11 +2683,13 @@ function toReactFlow(def, positions, registry, opts) {
2592
2683
  selected: opts.selectedNodeIds
2593
2684
  ? opts.selectedNodeIds.has(n.nodeId)
2594
2685
  : undefined,
2595
- initialWidth,
2596
- initialHeight,
2686
+ measured: {
2687
+ width: renderWidth,
2688
+ height: renderHeight,
2689
+ },
2597
2690
  handles,
2598
- width: initialWidth,
2599
- height: initialHeight,
2691
+ width: renderWidth,
2692
+ height: renderHeight,
2600
2693
  };
2601
2694
  });
2602
2695
  const edges = def.edges.map((e) => {
@@ -2803,22 +2896,30 @@ async function upload(parsed, wb, runner) {
2803
2896
  /**
2804
2897
  * Merge UI state from source into target, remapping node IDs using nodeIdMap.
2805
2898
  * Preserves target state and adds/updates source state with remapped IDs.
2899
+ * If anchorPos and sourceDef are provided, positions will be offset relative to anchorPos.
2806
2900
  */
2807
- function mergeUIState(targetUI, sourceUI, nodeIdMap) {
2901
+ function mergeUIState(targetUI, sourceUI, nodeIdMap, anchorPos, sourceDef) {
2808
2902
  const result = {
2809
2903
  ...targetUI,
2810
2904
  };
2811
2905
  if (!sourceUI)
2812
2906
  return result;
2813
- // Merge positions (already handled by mergeSnapshotData, but included for completeness)
2907
+ // Merge positions with optional offset
2814
2908
  if (sourceUI.positions) {
2815
- result.positions = {
2816
- ...(targetUI?.positions || {}),
2817
- ...Object.fromEntries(Object.entries(sourceUI.positions).map(([oldId, pos]) => [
2818
- nodeIdMap[oldId] || oldId,
2819
- pos,
2820
- ])),
2821
- };
2909
+ if (anchorPos && sourceDef) {
2910
+ // Apply offset when anchorPos and sourceDef are provided
2911
+ result.positions = sparkGraph.offsetImportedPositions(targetUI?.positions ?? {}, sourceUI.positions, sourceDef, nodeIdMap, anchorPos);
2912
+ }
2913
+ else {
2914
+ // Simple remapping without offset
2915
+ result.positions = {
2916
+ ...(targetUI?.positions || {}),
2917
+ ...Object.fromEntries(Object.entries(sourceUI.positions).map(([oldId, pos]) => [
2918
+ nodeIdMap[oldId] || oldId,
2919
+ pos,
2920
+ ])),
2921
+ };
2922
+ }
2822
2923
  }
2823
2924
  // Merge selection: remap node IDs and edge IDs
2824
2925
  if (sourceUI.selection) {
@@ -2841,6 +2942,15 @@ function mergeUIState(targetUI, sourceUI, nodeIdMap) {
2841
2942
  ])),
2842
2943
  };
2843
2944
  }
2945
+ if (sourceUI.sizes) {
2946
+ result.sizes = {
2947
+ ...(targetUI?.sizes || {}),
2948
+ ...Object.fromEntries(Object.entries(sourceUI.sizes).map(([oldId, size]) => [
2949
+ nodeIdMap[oldId] || oldId,
2950
+ size,
2951
+ ])),
2952
+ };
2953
+ }
2844
2954
  return result;
2845
2955
  }
2846
2956
 
@@ -3098,7 +3208,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3098
3208
  (type === "graphChanged" || type === "graphUiChanged")) {
3099
3209
  const changeType = payload
3100
3210
  .change?.type;
3101
- if (changeType === "moveNode" || changeType === "moveNodes")
3211
+ if (changeType === "moveNode" ||
3212
+ changeType === "moveNodes" ||
3213
+ changeType === "resizeNodes")
3102
3214
  return prev;
3103
3215
  }
3104
3216
  const nextNo = prev.length > 0 ? (prev[0]?.no ?? 0) + 1 : 1;
@@ -3507,6 +3619,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3507
3619
  else if (changeType === "moveNodes") {
3508
3620
  reason = "move-nodes";
3509
3621
  }
3622
+ else if (changeType === "resizeNodes") {
3623
+ reason = "resize-nodes";
3624
+ }
3510
3625
  else if (changeType === "selection") {
3511
3626
  reason = "selection";
3512
3627
  }
@@ -4264,103 +4379,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
4264
4379
  }, title: "Delete referenced edge", children: "Delete edge" }))] }, i))) })] }))] })) })] }), debug && (jsxRuntime.jsx("div", { className: "mt-3 flex-none min-h-0 h-[50%]", children: jsxRuntime.jsx(DebugEvents, { autoScroll: !!autoScroll, hideWorkbench: !!hideWorkbench, onAutoScrollChange: onAutoScrollChange, onHideWorkbenchChange: onHideWorkbenchChange }) }))] }));
4265
4380
  }
4266
4381
 
4267
- function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
4268
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + (kind === "input" ? " left-2" : " right-2"), style: {
4269
- top: (y ?? 0) - 8,
4270
- ...(kind === "input"
4271
- ? { right: "50%" }
4272
- : { left: "50%", textAlign: "right" }),
4273
- whiteSpace: "nowrap",
4274
- overflow: "hidden",
4275
- textOverflow: "ellipsis",
4276
- }, children: renderLabel({ kind, id }) }))] }));
4277
- }
4278
- function NodeHandles({ data, isConnectable, getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
4279
- const layout = data.handleLayout ?? [];
4280
- const byId = React.useMemo(() => {
4281
- const m = new Map();
4282
- for (const h of layout) {
4283
- // Prefer namespaced key to disambiguate inputs/outputs that share id
4284
- m.set(`${h.type}:${h.id}`, {
4285
- position: h.position,
4286
- y: h.y,
4287
- type: h.type,
4288
- missing: h.missing,
4289
- });
4290
- // Back-compat: also store by id-only if not already set
4291
- if (!m.has(h.id))
4292
- m.set(h.id, {
4293
- position: h.position,
4294
- y: h.y,
4295
- type: h.type,
4296
- missing: h.missing,
4297
- });
4298
- }
4299
- return m;
4300
- }, [layout]);
4301
- const inputIds = React.useMemo(() => new Set((data.inputHandles ?? []).map((h) => h.id)), [data.inputHandles]);
4302
- const outputIds = React.useMemo(() => new Set((data.outputHandles ?? []).map((h) => h.id)), [data.outputHandles]);
4303
- const missingInputs = React.useMemo(() => (layout || []).filter((h) => h.type === "target" && (!inputIds.has(h.id) || h.missing)), [layout, inputIds]);
4304
- const missingOutputs = React.useMemo(() => (layout || []).filter((h) => h.type === "source" && (!outputIds.has(h.id) || h.missing)), [layout, outputIds]);
4305
- return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
4306
- const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
4307
- const position = placed?.position ?? react.Position.Left;
4308
- const y = placed?.y;
4309
- const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ?? "";
4310
- 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));
4311
- }), missingInputs.map((h) => {
4312
- const key = `missing-input:${h.id}`;
4313
- const position = h.position ?? react.Position.Left;
4314
- const y = h.y;
4315
- const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 wb-nodrag wb-nowheel";
4316
- 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));
4317
- }), (data.outputHandles ?? []).map((h) => {
4318
- const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
4319
- const position = placed?.position ?? react.Position.Right;
4320
- const y = placed?.y;
4321
- const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ?? "";
4322
- 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));
4323
- }), missingOutputs.map((h) => {
4324
- const key = `missing-output:${h.id}`;
4325
- const position = h.position ?? react.Position.Right;
4326
- const y = h.y;
4327
- const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
4328
- return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
4329
- })] }));
4330
- }
4331
-
4332
- const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
4333
- const updateNodeInternals = react.useUpdateNodeInternals();
4334
- const { typeId, showValues } = data;
4335
- const inputEntries = data.inputHandles ?? [];
4336
- const outputEntries = data.outputHandles ?? [];
4337
- React.useEffect(() => {
4338
- updateNodeInternals(id);
4339
- }, [
4340
- id,
4341
- inputEntries.length,
4342
- outputEntries.length,
4343
- showValues,
4344
- updateNodeInternals,
4345
- ]);
4346
- const status = data.status ?? { activeRuns: 0 };
4347
- const validation = data.validation ?? {
4348
- inputs: [],
4349
- outputs: [],
4350
- issues: [],
4351
- };
4352
- const containerBorder = getNodeBorderClassNames({
4353
- selected,
4354
- status,
4355
- validation,
4356
- });
4357
- return (jsxRuntime.jsxs("div", { className: cx("rounded-lg bg-white/50 !dark:bg-stone-900", containerBorder), style: {
4358
- position: "relative",
4359
- minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
4360
- minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
4361
- }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4362
- });
4363
- DefaultNode.displayName = "DefaultNode";
4364
4382
  function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4365
4383
  const ctx = useWorkbenchContext();
4366
4384
  const [isEditing, setIsEditing] = React.useState(false);
@@ -4444,6 +4462,72 @@ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInv
4444
4462
  .map((v) => `${v.code}: ${v.message}`)
4445
4463
  .join("; ") })), showId && jsxRuntime.jsxs("span", { className: "text-[10px] opacity-70", children: ["(", id, ")"] })] })] }));
4446
4464
  }
4465
+
4466
+ function NodeHandleItem({ kind, id, type, position, y, isConnectable, className, labelClassName, renderLabel, }) {
4467
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(react.Handle, { id: id, type: type, position: position, isConnectable: isConnectable, className: className, style: y !== undefined ? { top: y } : undefined }), renderLabel && (jsxRuntime.jsx("div", { className: labelClassName + (kind === "input" ? " left-2" : " right-2"), style: {
4468
+ top: (y ?? 0) - 8,
4469
+ ...(kind === "input"
4470
+ ? { right: "50%" }
4471
+ : { left: "50%", textAlign: "right" }),
4472
+ whiteSpace: "nowrap",
4473
+ overflow: "hidden",
4474
+ textOverflow: "ellipsis",
4475
+ }, children: renderLabel({ kind, id }) }))] }));
4476
+ }
4477
+ function NodeHandles({ data, isConnectable, getClassName, renderLabel, labelClassName = "absolute text-[11px] text-gray-700 dark:text-gray-300 pointer-events-none", }) {
4478
+ const layout = data.handleLayout ?? [];
4479
+ const byId = React.useMemo(() => {
4480
+ const m = new Map();
4481
+ for (const h of layout) {
4482
+ // Prefer namespaced key to disambiguate inputs/outputs that share id
4483
+ m.set(`${h.type}:${h.id}`, {
4484
+ position: h.position,
4485
+ y: h.y,
4486
+ type: h.type,
4487
+ missing: h.missing,
4488
+ });
4489
+ // Back-compat: also store by id-only if not already set
4490
+ if (!m.has(h.id))
4491
+ m.set(h.id, {
4492
+ position: h.position,
4493
+ y: h.y,
4494
+ type: h.type,
4495
+ missing: h.missing,
4496
+ });
4497
+ }
4498
+ return m;
4499
+ }, [layout]);
4500
+ const inputIds = React.useMemo(() => new Set((data.inputHandles ?? []).map((h) => h.id)), [data.inputHandles]);
4501
+ const outputIds = React.useMemo(() => new Set((data.outputHandles ?? []).map((h) => h.id)), [data.outputHandles]);
4502
+ const missingInputs = React.useMemo(() => (layout || []).filter((h) => h.type === "target" && (!inputIds.has(h.id) || h.missing)), [layout, inputIds]);
4503
+ const missingOutputs = React.useMemo(() => (layout || []).filter((h) => h.type === "source" && (!outputIds.has(h.id) || h.missing)), [layout, outputIds]);
4504
+ return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [(data.inputHandles ?? []).map((h) => {
4505
+ const placed = byId.get(`target:${h.id}`) ?? byId.get(h.id);
4506
+ const position = placed?.position ?? react.Position.Left;
4507
+ const y = placed?.y;
4508
+ const cls = getClassName?.({ kind: "input", id: h.id, type: "target" }) ?? "";
4509
+ 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));
4510
+ }), missingInputs.map((h) => {
4511
+ const key = `missing-input:${h.id}`;
4512
+ const position = h.position ?? react.Position.Left;
4513
+ const y = h.y;
4514
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 wb-nodrag wb-nowheel";
4515
+ 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));
4516
+ }), (data.outputHandles ?? []).map((h) => {
4517
+ const placed = byId.get(`source:${h.id}`) ?? byId.get(h.id);
4518
+ const position = placed?.position ?? react.Position.Right;
4519
+ const y = placed?.y;
4520
+ const cls = getClassName?.({ kind: "output", id: h.id, type: "source" }) ?? "";
4521
+ 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));
4522
+ }), missingOutputs.map((h) => {
4523
+ const key = `missing-output:${h.id}`;
4524
+ const position = h.position ?? react.Position.Right;
4525
+ const y = h.y;
4526
+ const cls = "!w-3 !h-3 !bg-amber-400 !border-amber-500 !rounded-none wb-nodrag wb-nowheel";
4527
+ return (jsxRuntime.jsx(NodeHandleItem, { kind: "output", id: h.id, type: "source", position: position, y: y, isConnectable: false, className: cls, labelClassName: labelClassName, renderLabel: renderLabel }, key));
4528
+ })] }));
4529
+ }
4530
+
4447
4531
  function DefaultNodeContent({ data, isConnectable, }) {
4448
4532
  const { showValues, inputValues, inputDefaults, outputValues, toString } = data;
4449
4533
  const inputEntries = data.inputHandles ?? [];
@@ -4496,6 +4580,24 @@ function DefaultNodeContent({ data, isConnectable, }) {
4496
4580
  } })] }));
4497
4581
  }
4498
4582
 
4583
+ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConnectable, }) {
4584
+ const nodeRef = React.useRef(null);
4585
+ const { typeId } = data;
4586
+ const status = data.status ?? { activeRuns: 0 };
4587
+ const validation = data.validation ?? {
4588
+ inputs: [],
4589
+ outputs: [],
4590
+ issues: [],
4591
+ };
4592
+ const containerBorder = getNodeBorderClassNames({
4593
+ selected,
4594
+ status,
4595
+ validation,
4596
+ });
4597
+ return (jsxRuntime.jsxs("div", { ref: nodeRef, className: cx("rounded-lg bg-white/50 !dark:bg-stone-900 w-full h-full relative", containerBorder), children: [jsxRuntime.jsx(react.NodeResizer, { isVisible: selected, minWidth: data.initialWidth, minHeight: data.initialHeight }), jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4598
+ });
4599
+ DefaultNode.displayName = "DefaultNode";
4600
+
4499
4601
  // Helper to format shortcut for current platform
4500
4602
  function formatShortcut(shortcut) {
4501
4603
  const isMac = typeof navigator !== "undefined" &&
@@ -4776,8 +4878,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4776
4878
  position: n.position,
4777
4879
  type: n.type,
4778
4880
  selected: n.selected,
4779
- initialWidth: n.initialWidth,
4780
- initialHeight: n.initialHeight,
4881
+ measured: n.measured,
4781
4882
  data: n.data && {
4782
4883
  typeId: n.data.typeId,
4783
4884
  inputHandles: n.data.inputHandles,
@@ -4811,11 +4912,18 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4811
4912
  React.useImperativeHandle(ref, () => ({
4812
4913
  fitView: () => {
4813
4914
  try {
4814
- rfInstanceRef.current?.fitView({ padding: 0.2 });
4915
+ rfInstanceRef.current?.fitView();
4916
+ }
4917
+ catch (err) {
4918
+ console.warn("Failed to fit view", err);
4815
4919
  }
4816
- catch { }
4817
4920
  },
4818
- }));
4921
+ setViewport: (viewport) => {
4922
+ if (rfInstanceRef.current) {
4923
+ rfInstanceRef.current.setViewport(lod.clone(viewport));
4924
+ }
4925
+ },
4926
+ }), []);
4819
4927
  const { onConnect, onNodesChange, onEdgesChange, onEdgesDelete, onNodesDelete, } = useWorkbenchBridge(wb);
4820
4928
  const ui = wb.getUI();
4821
4929
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
@@ -4861,7 +4969,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4861
4969
  inputsWithDefaults[n.nodeId] = merged;
4862
4970
  }
4863
4971
  }
4864
- const out = toReactFlow(wb.def, wb.getPositions(), registry, {
4972
+ const out = toReactFlow(wb.def, wb.getPositions(), wb.getSizes(), registry, {
4865
4973
  showValues,
4866
4974
  inputs: inputsWithDefaults,
4867
4975
  inputDefaults: inputDefaultsMap,
@@ -5081,7 +5189,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5081
5189
  setNodeMenuOpen(false);
5082
5190
  setSelectionMenuOpen(false);
5083
5191
  };
5084
- const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs, commit: true }), [wb]);
5192
+ const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId }, { inputs: opts.inputs, position: opts.position, commit: true }), [wb]);
5085
5193
  const onCloseMenu = React.useCallback(() => {
5086
5194
  setMenuOpen(false);
5087
5195
  }, []);
@@ -5333,55 +5441,53 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5333
5441
  showToast,
5334
5442
  ]);
5335
5443
  // Get custom renderers from UI extension registry (reactive to uiVersion changes)
5336
- const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = React.useMemo(() => {
5444
+ const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, SelectionContextMenuRenderer, connectionLineRenderer, } = React.useMemo(() => {
5337
5445
  return {
5338
5446
  BackgroundRenderer: ui.getBackgroundRenderer(),
5339
5447
  MinimapRenderer: ui.getMinimapRenderer(),
5340
5448
  ControlsRenderer: ui.getControlsRenderer(),
5341
5449
  DefaultContextMenuRenderer: ui.getDefaultContextMenuRenderer(),
5342
5450
  NodeContextMenuRenderer: ui.getNodeContextMenuRenderer(),
5451
+ SelectionContextMenuRenderer: ui.getSelectionContextMenuRenderer(),
5343
5452
  connectionLineRenderer: ui.getConnectionLineRenderer(),
5344
5453
  };
5345
5454
  }, [ui, uiVersion]);
5346
5455
  const onMoveEnd = React.useCallback(() => {
5347
5456
  if (rfInstanceRef.current) {
5348
5457
  const viewport = rfInstanceRef.current.getViewport();
5349
- const viewportData = lod.pick(viewport, ["x", "y", "zoom"]);
5458
+ const viewportData = lod.clone(viewport);
5350
5459
  wb.setViewport(viewportData);
5351
5460
  }
5352
5461
  }, [wb]);
5353
- const viewportRef = React.useRef(null);
5462
+ // Sync viewport when workbench fires graphUiChanged with viewport event
5354
5463
  React.useEffect(() => {
5355
- if (!rfInstanceRef.current)
5356
- return;
5357
- const currentViewport = wb.getViewport();
5358
- if (currentViewport &&
5359
- (!viewportRef.current ||
5360
- viewportRef.current.x !== currentViewport.x ||
5361
- viewportRef.current.y !== currentViewport.y ||
5362
- viewportRef.current.zoom !== currentViewport.zoom)) {
5363
- viewportRef.current = currentViewport;
5364
- rfInstanceRef.current.setViewport({
5365
- x: currentViewport.x,
5366
- y: currentViewport.y,
5367
- zoom: currentViewport.zoom,
5368
- });
5369
- }
5370
- });
5464
+ const off = wb.on("graphUiChanged", (event) => {
5465
+ if (event.change?.type === "viewport" &&
5466
+ rfInstanceRef.current &&
5467
+ event.init) {
5468
+ const viewport = wb.getViewport();
5469
+ if (viewport) {
5470
+ rfInstanceRef.current.setViewport(lod.clone(viewport));
5471
+ }
5472
+ }
5473
+ });
5474
+ return () => off();
5475
+ }, [wb]);
5371
5476
  return (jsxRuntime.jsxs("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) => {
5372
5477
  rfInstanceRef.current = inst;
5373
5478
  const savedViewport = wb.getViewport();
5374
5479
  if (savedViewport) {
5375
- viewportRef.current = savedViewport;
5376
- inst.setViewport(lod.pick(savedViewport, ["x", "y", "zoom"]));
5480
+ inst.setViewport(lod.clone(savedViewport));
5377
5481
  }
5378
- }, 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, ...(enableKeyboardShortcuts !== false
5482
+ }, 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", 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, ...(enableKeyboardShortcuts !== false
5379
5483
  ? { enableKeyboardShortcuts, keyboardShortcuts }
5380
5484
  : {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
5381
5485
  nodeContextMenuHandlers &&
5382
5486
  (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, ...(enableKeyboardShortcuts !== false
5383
5487
  ? { enableKeyboardShortcuts, keyboardShortcuts }
5384
- : {}) })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }), toast && (jsxRuntime.jsx(KeyboardShortcutToast, { message: toast.message, onClose: hideToast }, toast.id))] }));
5488
+ : {}) })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen &&
5489
+ selectionMenuPos &&
5490
+ (SelectionContextMenuRenderer ? (jsxRuntime.jsx(SelectionContextMenuRenderer, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })) : (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })))] }) }), toast && (jsxRuntime.jsx(KeyboardShortcutToast, { message: toast.message, onClose: hideToast }, toast.id))] }));
5385
5491
  });
5386
5492
 
5387
5493
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {