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