@bian-womp/spark-workbench 0.2.77 → 0.2.79

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 (39) hide show
  1. package/lib/cjs/index.cjs +363 -330
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/adapters/cli/index.d.ts +1 -1
  4. package/lib/cjs/src/adapters/cli/index.d.ts.map +1 -1
  5. package/lib/cjs/src/core/AbstractWorkbench.d.ts +1 -1
  6. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
  7. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +50 -2
  8. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  9. package/lib/cjs/src/core/contracts.d.ts +8 -2
  10. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +0 -1
  15. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  17. package/lib/cjs/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
  18. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  19. package/lib/cjs/src/misc/load.d.ts.map +1 -1
  20. package/lib/esm/index.js +364 -331
  21. package/lib/esm/index.js.map +1 -1
  22. package/lib/esm/src/adapters/cli/index.d.ts +1 -1
  23. package/lib/esm/src/adapters/cli/index.d.ts.map +1 -1
  24. package/lib/esm/src/core/AbstractWorkbench.d.ts +1 -1
  25. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  26. package/lib/esm/src/core/InMemoryWorkbench.d.ts +50 -2
  27. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  28. package/lib/esm/src/core/contracts.d.ts +8 -2
  29. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  30. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  31. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  32. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  33. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +0 -1
  34. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  35. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  36. package/lib/esm/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
  37. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  38. package/lib/esm/src/misc/load.d.ts.map +1 -1
  39. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -123,7 +123,7 @@ class AbstractWorkbench {
123
123
  class InMemoryWorkbench extends AbstractWorkbench {
124
124
  constructor() {
125
125
  super(...arguments);
126
- this.def = { nodes: [], edges: [] };
126
+ this._def = { nodes: [], edges: [] };
127
127
  this.listeners = new Map();
128
128
  this.positions = {};
129
129
  this.selection = {
@@ -135,40 +135,40 @@ class InMemoryWorkbench extends AbstractWorkbench {
135
135
  this.historyState = undefined;
136
136
  this.copiedData = null;
137
137
  }
138
+ get def() {
139
+ return this._def;
140
+ }
138
141
  setRegistry(registry) {
139
142
  this.registry = registry;
140
143
  }
141
144
  async load(def) {
142
- this.def = { nodes: [...def.nodes], edges: [...def.edges] };
145
+ this._def = { nodes: [...def.nodes], edges: [...def.edges] };
143
146
  if (this.layout) {
144
- const { positions } = await this.layout.layout(this.def);
147
+ const { positions } = await this.layout.layout(this._def);
145
148
  this.positions = positions;
146
149
  }
147
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
148
- const defEdgeIds = new Set(this.def.edges.map((e) => e.id));
150
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
151
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
149
152
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
150
153
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
151
154
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
152
155
  this.positions = filteredPositions;
153
156
  this.selection = { nodes: filteredNodes, edges: filteredEdges };
154
- this.emit("graphChanged", { def: this.def });
157
+ this.emit("graphChanged", { def: this._def });
155
158
  this.refreshValidation();
156
159
  }
157
- export() {
158
- return this.def;
159
- }
160
160
  refreshValidation() {
161
161
  this.emit("validationChanged", this.validate());
162
162
  }
163
163
  validate() {
164
164
  if (this.registry) {
165
165
  const builder = new sparkGraph.GraphBuilder(this.registry);
166
- const report = builder.validate(this.def);
166
+ const report = builder.validate(this._def);
167
167
  return report;
168
168
  }
169
169
  const issues = [];
170
170
  const nodeIds = new Set();
171
- for (const n of this.def.nodes) {
171
+ for (const n of this._def.nodes) {
172
172
  if (nodeIds.has(n.nodeId)) {
173
173
  issues.push({
174
174
  level: "error",
@@ -180,7 +180,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
180
180
  nodeIds.add(n.nodeId);
181
181
  }
182
182
  const edgeIds = new Set();
183
- for (const e of this.def.edges) {
183
+ for (const e of this._def.edges) {
184
184
  if (edgeIds.has(e.id)) {
185
185
  issues.push({
186
186
  level: "error",
@@ -193,10 +193,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
193
193
  }
194
194
  return { ok: issues.every((i) => i.level !== "error"), issues };
195
195
  }
196
+ setInputs(nodeId, inputs, options) {
197
+ this.emit("graphChanged", {
198
+ def: this._def,
199
+ change: { type: "setInputs", nodeId, inputs },
200
+ ...options,
201
+ });
202
+ }
196
203
  addNode(node, options) {
197
204
  const id = node.nodeId ??
198
- this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
199
- this.def.nodes.push({
205
+ this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
206
+ this._def.nodes.push({
200
207
  nodeId: id,
201
208
  typeId: node.typeId,
202
209
  params: node.params,
@@ -205,7 +212,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
205
212
  if (node.position)
206
213
  this.positions[id] = node.position;
207
214
  this.emit("graphChanged", {
208
- def: this.def,
215
+ def: this._def,
209
216
  change: {
210
217
  type: "addNode",
211
218
  nodeId: id,
@@ -218,26 +225,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
218
225
  return id;
219
226
  }
220
227
  removeNode(nodeId, options) {
221
- this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
222
- this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
228
+ this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
229
+ this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
223
230
  delete this.positions[nodeId];
224
231
  this.emit("graphChanged", {
225
- def: this.def,
232
+ def: this._def,
226
233
  change: { type: "removeNode", nodeId },
227
234
  ...options,
228
235
  });
229
236
  this.refreshValidation();
230
237
  }
231
238
  connect(edge, options) {
232
- const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
233
- this.def.edges.push({
239
+ const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
240
+ this._def.edges.push({
234
241
  id,
235
242
  source: { ...edge.source },
236
243
  target: { ...edge.target },
237
244
  typeId: edge.typeId,
238
245
  });
239
246
  this.emit("graphChanged", {
240
- def: this.def,
247
+ def: this._def,
241
248
  change: { type: "connect", edgeId: id },
242
249
  ...options,
243
250
  });
@@ -245,16 +252,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
245
252
  return id;
246
253
  }
247
254
  disconnect(edgeId, options) {
248
- this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
255
+ this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
249
256
  this.emit("graphChanged", {
250
- def: this.def,
257
+ def: this._def,
251
258
  change: { type: "disconnect", edgeId },
252
259
  ...options,
253
260
  });
254
261
  this.emit("validationChanged", this.validate());
255
262
  }
256
263
  updateEdgeType(edgeId, typeId) {
257
- const e = this.def.edges.find((x) => x.id === edgeId);
264
+ const e = this._def.edges.find((x) => x.id === edgeId);
258
265
  if (!e)
259
266
  return;
260
267
  if (!typeId)
@@ -262,18 +269,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
262
269
  else
263
270
  e.typeId = typeId;
264
271
  this.emit("graphChanged", {
265
- def: this.def,
272
+ def: this._def,
266
273
  change: { type: "updateEdgeType", edgeId, typeId },
267
274
  });
268
275
  this.refreshValidation();
269
276
  }
270
277
  updateParams(nodeId, params) {
271
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
278
+ const n = this._def.nodes.find((n) => n.nodeId === nodeId);
272
279
  if (!n)
273
280
  return;
274
281
  n.params = { ...(n.params ?? {}), ...params };
275
282
  this.emit("graphChanged", {
276
- def: this.def,
283
+ def: this._def,
277
284
  change: { type: "updateParams", nodeId },
278
285
  });
279
286
  }
@@ -281,7 +288,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
281
288
  setPositions(map, options) {
282
289
  this.positions = { ...this.positions, ...map };
283
290
  this.emit("graphUiChanged", {
284
- def: this.def,
291
+ def: this._def,
285
292
  change: { type: "moveNodes" },
286
293
  ...options,
287
294
  });
@@ -289,17 +296,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
289
296
  getPositions() {
290
297
  return { ...this.positions };
291
298
  }
292
- setSelection(sel, options) {
299
+ setSelectionInternal(sel, options) {
293
300
  if (lod.isEqual(this.selection, sel))
294
301
  return;
295
302
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
296
303
  this.emit("selectionChanged", this.selection);
297
304
  this.emit("graphUiChanged", {
298
- def: this.def,
305
+ def: this._def,
299
306
  change: { type: "selection" },
300
307
  ...options,
301
308
  });
302
309
  }
310
+ setSelection(sel, options) {
311
+ this.setSelectionInternal(sel, options);
312
+ }
303
313
  getSelection() {
304
314
  return {
305
315
  nodes: [...this.selection.nodes],
@@ -320,14 +330,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
320
330
  this.disconnect(edgeId);
321
331
  }
322
332
  // Clear selection
323
- this.setSelection({ nodes: [], edges: [] }, options);
333
+ this.setSelectionInternal({ nodes: [], edges: [] }, options);
324
334
  }
325
335
  setViewport(viewport) {
326
336
  if (lod.isEqual(this.viewport, viewport))
327
337
  return;
328
338
  this.viewport = { ...viewport };
329
339
  this.emit("graphUiChanged", {
330
- def: this.def,
340
+ def: this._def,
331
341
  change: { type: "viewport" },
332
342
  });
333
343
  }
@@ -335,8 +345,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
335
345
  return this.viewport ? { ...this.viewport } : null;
336
346
  }
337
347
  getUIState() {
338
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
339
- const defEdgeIds = new Set(this.def.edges.map((e) => e.id));
348
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
349
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
340
350
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
341
351
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
342
352
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
@@ -375,6 +385,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
375
385
  }
376
386
  setRuntimeState(runtime) {
377
387
  this.runtimeState = runtime ? { ...runtime } : null;
388
+ this.emit("runtimeMetadataChanged", {});
378
389
  }
379
390
  getHistory() {
380
391
  return this.historyState;
@@ -391,6 +402,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
391
402
  const nodeMeta = current.nodes[nodeId] ?? {};
392
403
  const updated = updater({ ...nodeMeta });
393
404
  this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
405
+ this.emit("runtimeMetadataChanged", { nodeId });
394
406
  }
395
407
  on(event, handler) {
396
408
  if (!this.listeners.has(event))
@@ -415,11 +427,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
415
427
  const selection = this.getSelection();
416
428
  if (selection.nodes.length === 0)
417
429
  return null;
418
- const def = this.export();
419
430
  const positions = this.getPositions();
420
431
  const selectedNodeSet = new Set(selection.nodes);
421
432
  // Collect nodes to copy
422
- const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
433
+ const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
423
434
  if (nodesToCopy.length === 0)
424
435
  return null;
425
436
  // Calculate bounds
@@ -443,12 +454,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
443
454
  const centerY = (bounds.minY + bounds.maxY) / 2;
444
455
  // Get inputs for each node
445
456
  // Include values from inbound edges if those edges are selected
446
- const allInputs = runner.getInputs(def);
457
+ const allInputs = runner.getInputs(this.def);
447
458
  const selectedEdgeSet = new Set(selection.edges);
448
459
  const copiedNodes = nodesToCopy.map((node) => {
449
460
  const pos = positions[node.nodeId] || { x: 0, y: 0 };
450
461
  // Get all inbound edges for this node
451
- const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
462
+ const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
452
463
  // Build set of handles that have inbound edges
453
464
  // But only exclude handles whose edges are NOT selected
454
465
  const inboundHandlesToExclude = new Set(inboundEdges
@@ -472,7 +483,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
472
483
  };
473
484
  });
474
485
  // Collect edges between copied nodes
475
- const copiedEdges = def.edges
486
+ const copiedEdges = this.def.edges
476
487
  .filter((edge) => {
477
488
  return (selectedNodeSet.has(edge.source.nodeId) &&
478
489
  selectedNodeSet.has(edge.target.nodeId));
@@ -490,6 +501,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
490
501
  bounds,
491
502
  };
492
503
  }
504
+ /**
505
+ * Duplicate all selected nodes.
506
+ * Returns the list of newly created node IDs.
507
+ * Each duplicated node is offset by 24px in both x and y directions.
508
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
509
+ */
510
+ duplicateSelection(runner, options) {
511
+ const selection = this.getSelection();
512
+ if (selection.nodes.length === 0)
513
+ return [];
514
+ const positions = this.getPositions();
515
+ const newNodes = [];
516
+ // Get inputs without bindings (literal values only)
517
+ const allInputs = runner.getInputs(this.def) || {};
518
+ // Duplicate each selected node
519
+ for (const nodeId of selection.nodes) {
520
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
521
+ if (!n)
522
+ continue;
523
+ const pos = positions[nodeId] || { x: 0, y: 0 };
524
+ const inboundHandles = new Set(this.def.edges
525
+ .filter((e) => e.target.nodeId === nodeId)
526
+ .map((e) => e.target.handle));
527
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
528
+ const newNodeId = this.addNode({
529
+ typeId: n.typeId,
530
+ params: n.params,
531
+ position: { x: pos.x + 24, y: pos.y + 24 },
532
+ resolvedHandles: n.resolvedHandles,
533
+ }, {
534
+ inputs: inputsWithoutBindings,
535
+ copyOutputsFrom: nodeId,
536
+ dry: true,
537
+ });
538
+ newNodes.push(newNodeId);
539
+ }
540
+ // Select all newly duplicated nodes
541
+ if (newNodes.length > 0) {
542
+ this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
543
+ }
544
+ return newNodes;
545
+ }
546
+ /**
547
+ * Bake an output value from a node into a new node.
548
+ * Creates a new node based on the output type's bakeTarget configuration.
549
+ * Returns the ID of the last created node (or undefined if none created).
550
+ */
551
+ async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
552
+ try {
553
+ if (!outputTypeId || outputValue === undefined)
554
+ return undefined;
555
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
556
+ const coerceIfNeeded = async (fromType, toType, value) => {
557
+ if (!toType || toType === fromType || !runner?.coerce)
558
+ return value;
559
+ try {
560
+ return await runner.coerce(fromType, toType, value);
561
+ }
562
+ catch {
563
+ return value;
564
+ }
565
+ };
566
+ const pos = nodePosition;
567
+ const isArray = outputTypeId.endsWith("[]");
568
+ const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
569
+ const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
570
+ const tElem = registry.types.get(baseTypeId);
571
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
572
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
573
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
574
+ let newNodeId;
575
+ if (singleTarget) {
576
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
577
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
578
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
579
+ newNodeId = this.addNode({
580
+ typeId: singleTarget.nodeTypeId,
581
+ position: { x: pos.x + 180, y: pos.y },
582
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
583
+ }
584
+ else if (isArray && arrTarget) {
585
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
586
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
587
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
588
+ newNodeId = this.addNode({
589
+ typeId: arrTarget.nodeTypeId,
590
+ position: { x: pos.x + 180, y: pos.y },
591
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
592
+ }
593
+ else if (isArray && elemTarget) {
594
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
595
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
596
+ const src = unwrap(outputValue);
597
+ const items = Array.isArray(src) ? src : [src];
598
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
599
+ const COLS = 4;
600
+ const DX = 180;
601
+ const DY = 160;
602
+ for (let idx = 0; idx < coercedItems.length; idx++) {
603
+ const col = idx % COLS;
604
+ const row = Math.floor(idx / COLS);
605
+ newNodeId = this.addNode({
606
+ typeId: elemTarget.nodeTypeId,
607
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
608
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
609
+ }
610
+ }
611
+ if (newNodeId) {
612
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
613
+ }
614
+ return newNodeId;
615
+ }
616
+ catch {
617
+ return undefined;
618
+ }
619
+ }
620
+ /**
621
+ * Duplicate a single node.
622
+ * Returns the ID of the newly created node.
623
+ * The duplicated node is offset by 24px in both x and y directions.
624
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
625
+ */
626
+ duplicateNode(nodeId, runner, options) {
627
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
628
+ if (!n)
629
+ return undefined;
630
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
631
+ // Get inputs without bindings (literal values only)
632
+ const allInputs = runner.getInputs(this.def)[nodeId] || {};
633
+ const inboundHandles = new Set(this.def.edges
634
+ .filter((e) => e.target.nodeId === nodeId)
635
+ .map((e) => e.target.handle));
636
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
637
+ const newNodeId = this.addNode({
638
+ typeId: n.typeId,
639
+ params: n.params,
640
+ position: { x: pos.x + 24, y: pos.y + 24 },
641
+ resolvedHandles: n.resolvedHandles,
642
+ }, {
643
+ inputs: inputsWithoutBindings,
644
+ copyOutputsFrom: nodeId,
645
+ dry: true,
646
+ });
647
+ // Select the newly duplicated node
648
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
649
+ return newNodeId;
650
+ }
651
+ /**
652
+ * Duplicate a node and all its incoming edges.
653
+ * Returns the ID of the newly created node.
654
+ * The duplicated node is offset by 24px in both x and y directions.
655
+ * All incoming edges are duplicated to point to the new node.
656
+ */
657
+ duplicateNodeWithEdges(nodeId, runner, options) {
658
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
659
+ if (!n)
660
+ return undefined;
661
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
662
+ // Get all inputs (including those with bindings, since edges will be duplicated)
663
+ const inputs = runner.getInputs(this.def)[nodeId] || {};
664
+ // Add the duplicated node
665
+ const newNodeId = this.addNode({
666
+ typeId: n.typeId,
667
+ params: n.params,
668
+ position: { x: pos.x + 24, y: pos.y + 24 },
669
+ resolvedHandles: n.resolvedHandles,
670
+ }, {
671
+ inputs,
672
+ copyOutputsFrom: nodeId,
673
+ dry: true,
674
+ });
675
+ // Find all incoming edges (edges where target is the original node)
676
+ const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
677
+ // Duplicate each incoming edge to point to the new node
678
+ for (const edge of incomingEdges) {
679
+ this.connect({
680
+ source: edge.source, // Keep the same source
681
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
682
+ typeId: edge.typeId,
683
+ }, { dry: true });
684
+ }
685
+ // Select the newly duplicated node
686
+ if (newNodeId) {
687
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
688
+ }
689
+ return newNodeId;
690
+ }
493
691
  /**
494
692
  * Paste copied graph data at the specified center position.
495
693
  * Returns the mapping from original node IDs to new node IDs.
@@ -535,7 +733,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
535
733
  }
536
734
  }
537
735
  // Select the newly pasted nodes
538
- this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
736
+ this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
539
737
  return { nodeIdMap, edgeIds };
540
738
  }
541
739
  /**
@@ -560,8 +758,8 @@ class CLIWorkbench {
560
758
  async load(def) {
561
759
  await this.wb.load(def);
562
760
  }
563
- print(def, options) {
564
- const d = def ?? this.wb.export();
761
+ print(options) {
762
+ const d = this.wb.def;
565
763
  const detail = !!options?.detail;
566
764
  const lines = [];
567
765
  lines.push(`Nodes (${d.nodes.length})`);
@@ -1930,8 +2128,7 @@ function useWorkbenchBridge(wb) {
1930
2128
  if (!params.sourceHandle || !params.targetHandle)
1931
2129
  return;
1932
2130
  // Prevent duplicate edges between the same endpoints
1933
- const def = wb.export();
1934
- const exists = def.edges.some((e) => e.source.nodeId === params.source &&
2131
+ const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
1935
2132
  e.source.handle === params.sourceHandle &&
1936
2133
  e.target.nodeId === params.target &&
1937
2134
  e.target.handle === params.targetHandle);
@@ -1981,7 +2178,10 @@ function useWorkbenchBridge(wb) {
1981
2178
  }
1982
2179
  }
1983
2180
  if (selectionChanged) {
1984
- wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
2181
+ wb.setSelection({
2182
+ nodes: Array.from(nextNodeIds),
2183
+ edges: current.edges,
2184
+ });
1985
2185
  }
1986
2186
  if (Object.keys(positions).length > 0) {
1987
2187
  wb.setPositions(positions, { commit });
@@ -2016,7 +2216,10 @@ function useWorkbenchBridge(wb) {
2016
2216
  }
2017
2217
  }
2018
2218
  if (selectionChanged) {
2019
- wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
2219
+ wb.setSelection({
2220
+ nodes: current.nodes,
2221
+ edges: Array.from(nextEdgeIds),
2222
+ });
2020
2223
  }
2021
2224
  }, [wb]);
2022
2225
  const onNodesDelete = React.useCallback((nodes) => {
@@ -2462,7 +2665,6 @@ function isSnapshotPayload(parsed) {
2462
2665
  }
2463
2666
  async function download(wb, runner) {
2464
2667
  try {
2465
- const def = wb.export();
2466
2668
  const fullUiState = wb.getUIState();
2467
2669
  const uiState = excludeViewportFromUIState(fullUiState);
2468
2670
  const runtimeState = wb.getRuntimeState();
@@ -2471,7 +2673,7 @@ async function download(wb, runner) {
2471
2673
  const fullSnapshot = await runner.snapshotFull();
2472
2674
  snapshot = {
2473
2675
  ...fullSnapshot,
2474
- def,
2676
+ def: wb.def,
2475
2677
  extData: {
2476
2678
  ...(fullSnapshot.extData || {}),
2477
2679
  ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
@@ -2480,9 +2682,9 @@ async function download(wb, runner) {
2480
2682
  };
2481
2683
  }
2482
2684
  else {
2483
- const inputs = runner.getInputs(def);
2685
+ const inputs = runner.getInputs(wb.def);
2484
2686
  snapshot = {
2485
- def,
2687
+ def: wb.def,
2486
2688
  inputs,
2487
2689
  outputs: {},
2488
2690
  environment: {},
@@ -2521,7 +2723,7 @@ async function upload(parsed, wb, runner) {
2521
2723
  }
2522
2724
  if (runner.isRunning()) {
2523
2725
  await runner.applySnapshotFull({
2524
- def,
2726
+ def: wb.def,
2525
2727
  environment,
2526
2728
  inputs,
2527
2729
  outputs,
@@ -2529,10 +2731,10 @@ async function upload(parsed, wb, runner) {
2529
2731
  });
2530
2732
  }
2531
2733
  else {
2532
- runner.build(wb.export());
2734
+ runner.build(wb.def);
2533
2735
  if (inputs && typeof inputs === "object") {
2534
2736
  for (const [nodeId, map] of Object.entries(inputs)) {
2535
- runner.setInputs(nodeId, map);
2737
+ runner.setInputs(nodeId, map, { dry: true });
2536
2738
  }
2537
2739
  }
2538
2740
  }
@@ -2625,14 +2827,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2625
2827
  const versionTick = useWorkbenchVersionTick(runner);
2626
2828
  const valuesTick = versionTick + graphTick + graphUiTick;
2627
2829
  // Def and IO values
2628
- const def = wb.export();
2629
- const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
2630
- const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(def), [runner, def, valuesTick]);
2631
- const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
2830
+ const inputsMap = React.useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
2831
+ const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
2832
+ const outputsMap = React.useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
2632
2833
  const outputTypesMap = React.useMemo(() => {
2633
2834
  const out = {};
2634
2835
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
2635
- for (const n of def.nodes) {
2836
+ for (const n of wb.def.nodes) {
2636
2837
  const effectiveHandles = computeEffectiveHandles(n, registry);
2637
2838
  const outputsDecl = effectiveHandles.outputs;
2638
2839
  const handles = Object.keys(outputsDecl);
@@ -2647,14 +2848,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2647
2848
  out[n.nodeId] = cur;
2648
2849
  }
2649
2850
  return out;
2650
- }, [def, outputsMap, registry]);
2851
+ }, [wb, wb.def, outputsMap, registry]);
2651
2852
  // Initialize nodes and derive invalidated status from persisted metadata
2652
2853
  React.useEffect(() => {
2653
2854
  const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
2654
2855
  setNodeStatus((prev) => {
2655
2856
  const next = { ...prev };
2656
2857
  const metadata = workbenchRuntimeState;
2657
- for (const n of def.nodes) {
2858
+ for (const n of wb.def.nodes) {
2658
2859
  const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
2659
2860
  const nodeMeta = metadata.nodes[n.nodeId];
2660
2861
  const updates = {};
@@ -2676,18 +2877,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2676
2877
  }
2677
2878
  return next;
2678
2879
  });
2679
- }, [def, wb]);
2880
+ }, [wb.def, wb]);
2680
2881
  // Auto layout (simple layered layout)
2681
2882
  const runAutoLayout = React.useCallback(() => {
2682
- const cur = wb.export();
2683
2883
  // Build DAG layers by indegree
2684
2884
  const indegree = {};
2685
2885
  const adj = {};
2686
- for (const n of cur.nodes) {
2886
+ for (const n of wb.def.nodes) {
2687
2887
  indegree[n.nodeId] = 0;
2688
2888
  adj[n.nodeId] = [];
2689
2889
  }
2690
- for (const e of cur.edges) {
2890
+ for (const e of wb.def.edges) {
2691
2891
  indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
2692
2892
  adj[e.source.nodeId].push(e.target.nodeId);
2693
2893
  }
@@ -2718,7 +2918,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2718
2918
  let maxWidth = 0;
2719
2919
  const heights = {};
2720
2920
  for (const id of layer) {
2721
- const node = cur.nodes.find((n) => n.nodeId === id);
2921
+ const node = wb.def.nodes.find((n) => n.nodeId === id);
2722
2922
  if (!node)
2723
2923
  continue;
2724
2924
  // Prefer showValues sizing similar to node rendering
@@ -2744,26 +2944,26 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2744
2944
  curX += maxWidth + H_GAP;
2745
2945
  }
2746
2946
  wb.setPositions(pos, { commit: true, reason: "auto-layout" });
2747
- }, [wb, registry, overrides?.getDefaultNodeSize]);
2947
+ }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2748
2948
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2749
2949
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2750
2950
  // Helper to save runtime metadata and UI state to extData
2751
- const saveUiRuntimeMetadata = React.useCallback(async () => {
2951
+ const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
2752
2952
  try {
2753
- const current = wb.getRuntimeState() ?? { nodes: {} };
2953
+ const current = workbench.getRuntimeState() ?? { nodes: {} };
2754
2954
  const metadata = { nodes: { ...current.nodes } };
2755
2955
  // Clean up metadata for nodes that no longer exist
2756
- const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
2956
+ const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
2757
2957
  for (const nodeId of Object.keys(metadata.nodes)) {
2758
2958
  if (!nodeIds.has(nodeId)) {
2759
2959
  delete metadata.nodes[nodeId];
2760
2960
  }
2761
2961
  }
2762
2962
  // Save cleaned metadata to workbench state
2763
- wb.setRuntimeState(metadata);
2764
- const fullUiState = wb.getUIState();
2963
+ workbench.setRuntimeState(metadata);
2964
+ const fullUiState = workbench.getUIState();
2765
2965
  const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
2766
- await runner.setExtData?.({
2966
+ await graphRunner.setExtData?.({
2767
2967
  ...(Object.keys(uiWithoutViewport || {}).length > 0
2768
2968
  ? { ui: uiWithoutViewport }
2769
2969
  : {}),
@@ -2773,7 +2973,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2773
2973
  catch (err) {
2774
2974
  console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
2775
2975
  }
2776
- }, [wb, def, runner]);
2976
+ }, []);
2777
2977
  // Subscribe to runner/workbench events
2778
2978
  React.useEffect(() => {
2779
2979
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -2799,9 +2999,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2799
2999
  });
2800
3000
  // Helper to apply resolved handles from event payload to workbench
2801
3001
  const applyResolvedHandles = (resolvedHandles) => {
2802
- const cur = wb.export();
2803
3002
  let changed = false;
2804
- for (const n of cur.nodes) {
3003
+ for (const n of wb.def.nodes) {
2805
3004
  const updated = resolvedHandles[n.nodeId];
2806
3005
  if (updated) {
2807
3006
  const before = JSON.stringify(n.resolvedHandles || {});
@@ -2828,10 +3027,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2828
3027
  [handle]: now,
2829
3028
  },
2830
3029
  }));
2831
- setNodeStatus((s) => ({
2832
- ...s,
2833
- [nodeId]: { ...s[nodeId], invalidated: true },
2834
- }));
2835
3030
  // Clear validation errors for this input when a valid value is set
2836
3031
  setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
2837
3032
  }
@@ -2937,30 +3132,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2937
3132
  // If resolvedHandles are included in the event, use them directly (more efficient)
2938
3133
  if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
2939
3134
  applyResolvedHandles(e.resolvedHandles);
2940
- // Mark nodes whose handles changed as invalid
2941
- const affectedNodeIds = Object.keys(e.resolvedHandles);
2942
- if (affectedNodeIds.length > 0) {
2943
- setNodeStatus((prev) => {
2944
- const next = { ...prev };
2945
- for (const id of affectedNodeIds) {
2946
- const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
2947
- next[id] = { ...cur, invalidated: true };
2948
- }
2949
- return next;
2950
- });
2951
- }
2952
- }
2953
- // For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
2954
- if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
2955
- setNodeStatus((prev) => {
2956
- const next = { ...prev };
2957
- for (const n of def.nodes) {
2958
- const cur = next[n.nodeId] ??
2959
- (next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
2960
- next[n.nodeId] = { ...cur, invalidated: true };
2961
- }
2962
- return next;
2963
- });
2964
3135
  }
2965
3136
  return add("runner", "invalidate")(e);
2966
3137
  });
@@ -2993,7 +3164,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2993
3164
  ? [...currentRunIds, runId]
2994
3165
  : currentRunIds,
2995
3166
  progress: 0,
2996
- invalidated: false,
2997
3167
  },
2998
3168
  };
2999
3169
  });
@@ -3096,16 +3266,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3096
3266
  const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
3097
3267
  const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
3098
3268
  // Ensure newly added nodes start as invalidated until first evaluation
3099
- const offWbAddNode = wb.on("graphChanged", (e) => {
3100
- const change = e.change;
3101
- if (change?.type === "addNode" && typeof change.nodeId === "string") {
3102
- const id = change.nodeId;
3103
- setNodeStatus((s) => ({
3104
- ...s,
3105
- [id]: { ...s[id], invalidated: true },
3106
- }));
3107
- }
3108
- });
3109
3269
  const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
3110
3270
  // Build detailed reason from change type
3111
3271
  let reason = "graph-changed";
@@ -3129,17 +3289,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3129
3289
  else if (changeType === "updateEdgeType") {
3130
3290
  reason = "update-edge-type";
3131
3291
  }
3292
+ else if (changeType === "setInputs") {
3293
+ reason = "set-inputs";
3294
+ }
3295
+ }
3296
+ if (event.change?.type === "setInputs") {
3297
+ const { nodeId, inputs } = event.change;
3298
+ await runner.setInputs(nodeId, inputs, { dry: event.dry });
3132
3299
  }
3133
3300
  if (!runner.isRunning()) {
3134
3301
  if (event.commit) {
3135
- await saveUiRuntimeMetadata();
3302
+ await saveUiRuntimeMetadata(wb, runner);
3136
3303
  const history = await runner.commit(reason).catch((err) => {
3137
3304
  console.error("[WorkbenchContext] Error committing:", err);
3138
3305
  return undefined;
3139
3306
  });
3140
- if (history) {
3307
+ if (history)
3141
3308
  wb.setHistory(history);
3142
- }
3143
3309
  }
3144
3310
  return;
3145
3311
  }
@@ -3162,11 +3328,11 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3162
3328
  }
3163
3329
  }
3164
3330
  }
3165
- else {
3331
+ else if (event.change?.type !== "setInputs") {
3166
3332
  await runner.update(event.def, { dry: event.dry });
3167
3333
  }
3168
3334
  if (event.commit) {
3169
- await saveUiRuntimeMetadata();
3335
+ await saveUiRuntimeMetadata(wb, runner);
3170
3336
  const history = await runner
3171
3337
  .commit(event.reason ?? reason)
3172
3338
  .catch((err) => {
@@ -3187,7 +3353,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3187
3353
  setSelectedNodeId(sel.nodes?.[0]);
3188
3354
  setSelectedEdgeId(sel.edges?.[0]);
3189
3355
  if (sel.commit) {
3190
- await saveUiRuntimeMetadata();
3356
+ await saveUiRuntimeMetadata(wb, runner);
3191
3357
  const history = await runner
3192
3358
  .commit(sel.reason ?? "selection")
3193
3359
  .catch((err) => {
@@ -3226,7 +3392,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3226
3392
  reason = "selection";
3227
3393
  }
3228
3394
  }
3229
- await saveUiRuntimeMetadata();
3395
+ await saveUiRuntimeMetadata(wb, runner);
3230
3396
  const history = await runner
3231
3397
  .commit(event.reason ?? reason)
3232
3398
  .catch((err) => {
@@ -3246,7 +3412,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3246
3412
  wb.setRegistry(newReg);
3247
3413
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
3248
3414
  try {
3249
- await runner.update(wb.export());
3415
+ await runner.update(wb.def);
3250
3416
  }
3251
3417
  catch {
3252
3418
  console.error("Failed to update graph definition after registry changed");
@@ -3269,7 +3435,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3269
3435
  setNodeStatus(() => {
3270
3436
  const next = {};
3271
3437
  const metadata = wb.getRuntimeState() ?? { nodes: {} };
3272
- for (const n of def.nodes) {
3438
+ for (const n of wb.def.nodes) {
3273
3439
  const nodeMeta = metadata.nodes[n.nodeId];
3274
3440
  next[n.nodeId] = {
3275
3441
  activeRuns: 0,
@@ -3284,6 +3450,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3284
3450
  errorRunsRef.current = {};
3285
3451
  }
3286
3452
  });
3453
+ const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
3454
+ const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
3455
+ setNodeStatus((prev) => {
3456
+ const next = { ...prev };
3457
+ const metadata = workbenchRuntimeState;
3458
+ let changed = false;
3459
+ // If nodeId is specified, only update that node; otherwise update all nodes
3460
+ const nodesToUpdate = event.nodeId
3461
+ ? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
3462
+ : wb.def.nodes;
3463
+ for (const n of nodesToUpdate) {
3464
+ const cur = next[n.nodeId];
3465
+ if (!cur)
3466
+ continue;
3467
+ const nodeMeta = metadata.nodes[n.nodeId];
3468
+ const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
3469
+ if (cur.invalidated !== newInvalidated) {
3470
+ next[n.nodeId] = { ...cur, invalidated: newInvalidated };
3471
+ changed = true;
3472
+ }
3473
+ }
3474
+ return changed ? next : prev;
3475
+ });
3476
+ });
3287
3477
  wb.refreshValidation();
3288
3478
  return () => {
3289
3479
  offRunnerValue();
@@ -3295,20 +3485,20 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3295
3485
  offWbGraphUiChanged();
3296
3486
  offWbValidationChanged();
3297
3487
  offWbError();
3298
- offWbAddNode();
3299
3488
  offWbGraphChangedForUpdate();
3300
3489
  offWbdSetValidation();
3301
3490
  offWbSelectionChanged();
3302
3491
  offRunnerRegistry();
3303
3492
  offRunnerTransport();
3304
3493
  offFlowViewport();
3494
+ offWbRuntimeMetadataChanged();
3305
3495
  };
3306
3496
  }, [runner, wb, setRegistry]);
3307
3497
  const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
3308
3498
  const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
3309
3499
  const start = React.useCallback((engine) => {
3310
3500
  try {
3311
- runner.launch(wb.export(), { engine });
3501
+ runner.launch(wb.def, { engine });
3312
3502
  }
3313
3503
  catch { }
3314
3504
  }, [runner, wb]);
@@ -3383,7 +3573,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3383
3573
  runner,
3384
3574
  registry,
3385
3575
  setRegistry,
3386
- def,
3387
3576
  selectedNodeId,
3388
3577
  selectedEdgeId,
3389
3578
  setSelection,
@@ -3424,7 +3613,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3424
3613
  runner,
3425
3614
  registry,
3426
3615
  setRegistry,
3427
- def,
3428
3616
  selectedNodeId,
3429
3617
  selectedEdgeId,
3430
3618
  setSelection,
@@ -3464,137 +3652,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3464
3652
  }
3465
3653
 
3466
3654
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3467
- const doBake = async (handleId) => {
3468
- try {
3469
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3470
- const raw = outputsMap?.[nodeId]?.[handleId];
3471
- let newNodeId;
3472
- if (!typeId || raw === undefined)
3473
- return;
3474
- const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3475
- const coerceIfNeeded = async (fromType, toType, value) => {
3476
- if (!toType || toType === fromType || !runner?.coerce)
3477
- return value;
3478
- try {
3479
- return await runner.coerce(fromType, toType, value);
3480
- }
3481
- catch {
3482
- return value;
3483
- }
3484
- };
3485
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3486
- const isArray = typeId.endsWith("[]");
3487
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3488
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3489
- const tElem = registry.types.get(baseTypeId);
3490
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3491
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3492
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3493
- if (singleTarget) {
3494
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3495
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3496
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3497
- newNodeId = wb.addNode({
3498
- typeId: singleTarget.nodeTypeId,
3499
- position: { x: pos.x + 180, y: pos.y },
3500
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3501
- }
3502
- else if (isArray && arrTarget) {
3503
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3504
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3505
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3506
- newNodeId = wb.addNode({
3507
- typeId: arrTarget.nodeTypeId,
3508
- position: { x: pos.x + 180, y: pos.y },
3509
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3510
- }
3511
- else if (isArray && elemTarget) {
3512
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3513
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3514
- const src = unwrap(raw);
3515
- const items = Array.isArray(src) ? src : [src];
3516
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3517
- const COLS = 4;
3518
- const DX = 180;
3519
- const DY = 160;
3520
- for (let idx = 0; idx < coercedItems.length; idx++) {
3521
- const col = idx % COLS;
3522
- const row = Math.floor(idx / COLS);
3523
- newNodeId = wb.addNode({
3524
- typeId: elemTarget.nodeTypeId,
3525
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3526
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3527
- }
3528
- }
3529
- if (newNodeId) {
3530
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
3531
- }
3532
- }
3533
- catch { }
3534
- };
3535
3655
  return {
3536
3656
  onDelete: () => {
3537
3657
  wb.removeNode(nodeId, { commit: true });
3538
3658
  onClose();
3539
3659
  },
3540
3660
  onDuplicate: async () => {
3541
- const def = wb.export();
3542
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3543
- if (!n)
3544
- return onClose();
3545
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3546
- const inboundHandles = new Set(def.edges
3547
- .filter((e) => e.target.nodeId === nodeId)
3548
- .map((e) => e.target.handle));
3549
- const allInputs = runner.getInputs(def)[nodeId] || {};
3550
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3551
- const newNodeId = wb.addNode({
3552
- typeId: n.typeId,
3553
- params: n.params,
3554
- position: { x: pos.x + 24, y: pos.y + 24 },
3555
- resolvedHandles: n.resolvedHandles,
3556
- }, {
3557
- inputs: inputsWithoutBindings,
3558
- copyOutputsFrom: nodeId,
3559
- dry: true,
3661
+ wb.duplicateNode(nodeId, runner, {
3662
+ commit: true,
3663
+ reason: "duplicate-node",
3560
3664
  });
3561
- // Select the newly duplicated node
3562
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
3563
3665
  onClose();
3564
3666
  },
3565
3667
  onDuplicateWithEdges: async () => {
3566
- const def = wb.export();
3567
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3568
- if (!n)
3569
- return onClose();
3570
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3571
- // Get inputs without bindings (literal values only)
3572
- const inputs = runner.getInputs(def)[nodeId] || {};
3573
- // Add the duplicated node
3574
- const newNodeId = wb.addNode({
3575
- typeId: n.typeId,
3576
- params: n.params,
3577
- position: { x: pos.x + 24, y: pos.y + 24 },
3578
- resolvedHandles: n.resolvedHandles,
3579
- }, {
3580
- inputs,
3581
- copyOutputsFrom: nodeId,
3582
- dry: true,
3668
+ wb.duplicateNodeWithEdges(nodeId, runner, {
3669
+ commit: true,
3670
+ reason: "duplicate-node-with-edges",
3583
3671
  });
3584
- // Find all incoming edges (edges where target is the original node)
3585
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3586
- // Duplicate each incoming edge to point to the new node
3587
- for (const edge of incomingEdges) {
3588
- wb.connect({
3589
- source: edge.source, // Keep the same source
3590
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3591
- typeId: edge.typeId,
3592
- }, { dry: true });
3593
- }
3594
- // Select the newly duplicated node and edges
3595
- if (newNodeId) {
3596
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
3597
- }
3598
3672
  onClose();
3599
3673
  },
3600
3674
  onRunPull: async () => {
@@ -3605,7 +3679,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3605
3679
  onClose();
3606
3680
  },
3607
3681
  onBake: async (handleId) => {
3608
- await doBake(handleId);
3682
+ const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3683
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3684
+ const raw = outputsMap?.[nodeId]?.[handleId];
3685
+ await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
3686
+ commit: true,
3687
+ reason: "bake",
3688
+ });
3609
3689
  onClose();
3610
3690
  },
3611
3691
  onCopy: () => {
@@ -3662,42 +3742,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
3662
3742
  function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3663
3743
  const onDuplicate = runner
3664
3744
  ? () => {
3665
- const selection = wb.getSelection();
3666
- if (selection.nodes.length === 0) {
3667
- onClose();
3668
- return;
3669
- }
3670
- const def = wb.export();
3671
- const positions = wb.getPositions();
3672
- const newNodes = [];
3673
- // Duplicate each selected node
3674
- for (const nodeId of selection.nodes) {
3675
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3676
- if (!n)
3677
- continue;
3678
- const pos = positions[nodeId] || { x: 0, y: 0 };
3679
- // Get inputs without bindings (literal values only)
3680
- const allInputs = runner.getInputs(def)[nodeId] || {};
3681
- const inboundHandles = new Set(def.edges
3682
- .filter((e) => e.target.nodeId === nodeId)
3683
- .map((e) => e.target.handle));
3684
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3685
- const newNodeId = wb.addNode({
3686
- typeId: n.typeId,
3687
- params: n.params,
3688
- position: { x: pos.x + 24, y: pos.y + 24 },
3689
- resolvedHandles: n.resolvedHandles,
3690
- }, {
3691
- inputs: inputsWithoutBindings,
3692
- copyOutputsFrom: nodeId,
3693
- dry: true,
3694
- });
3695
- newNodes.push(newNodeId);
3696
- }
3697
- // Select all newly duplicated nodes
3698
- if (newNodes.length > 0) {
3699
- wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
3700
- }
3745
+ wb.duplicateSelection(runner, {
3746
+ commit: true,
3747
+ reason: "duplicate-selection",
3748
+ });
3701
3749
  onClose();
3702
3750
  }
3703
3751
  : undefined;
@@ -3732,9 +3780,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3732
3780
  const canRedo = history ? history.redoCount > 0 : undefined;
3733
3781
  const onSelectAll = wb
3734
3782
  ? () => {
3735
- const def = wb.export();
3736
- const allNodeIds = def.nodes.map((n) => n.nodeId);
3737
- const allEdgeIds = def.edges.map((e) => e.id);
3783
+ const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
3784
+ const allEdgeIds = wb.def.edges.map((e) => e.id);
3738
3785
  wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
3739
3786
  onClose();
3740
3787
  }
@@ -3753,8 +3800,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3753
3800
  }
3754
3801
  function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3755
3802
  try {
3756
- const def = wb.export();
3757
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3803
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
3758
3804
  if (!node)
3759
3805
  return [];
3760
3806
  const desc = registry.nodes.get(node.typeId);
@@ -3853,13 +3899,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3853
3899
  return String(value ?? "");
3854
3900
  }
3855
3901
  };
3856
- const { registry, def, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
3902
+ const { wb, registry, selectedNodeId, selectedEdgeId, inputsMap, inputDefaultsMap, outputsMap, outputTypesMap, nodeStatus, edgeStatus, validationByNode, validationByEdge, validationGlobal, valuesTick, updateEdgeType, systemErrors, registryErrors, inputValidationErrors, clearSystemErrors, clearRegistryErrors, clearInputValidationErrors, removeSystemError, removeRegistryError, removeInputValidationError, } = useWorkbenchContext();
3857
3903
  const nodeValidationIssues = validationByNode.issues;
3858
3904
  const edgeValidationIssues = validationByEdge.issues;
3859
3905
  const nodeValidationHandles = validationByNode;
3860
3906
  const globalValidationIssues = validationGlobal;
3861
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3862
- const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
3907
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
3908
+ const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
3863
3909
  // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
3864
3910
  const effectiveHandles = selectedNode
3865
3911
  ? computeEffectiveHandles(selectedNode, registry)
@@ -3991,7 +4037,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3991
4037
  setOriginals(nextOriginals);
3992
4038
  }, [selectedNodeId, selectedNode, registry, valuesTick]);
3993
4039
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
3994
- const { wb } = useWorkbenchContext();
3995
4040
  const deleteEdgeById = (edgeId) => {
3996
4041
  if (!edgeId)
3997
4042
  return;
@@ -4018,9 +4063,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
4018
4063
  selectedNodeStatus.activeRunIds.length > 0 ? (jsxRuntime.jsxs("div", { className: "mt-1", children: [jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600", children: "RunIds:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: selectedNodeStatus.activeRunIds.map((runId, idx) => (jsxRuntime.jsx("span", { className: "text-[10px] px-1.5 py-0.5 bg-blue-100 border border-blue-300 rounded font-mono", children: runId }, idx))) })] })) : (jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "RunIds not available (some runs may have started without runId)" }))] })), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
4019
4064
  selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
4020
4065
  const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
4021
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
4066
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
4022
4067
  e.target.handle === h);
4023
- const inbound = new Set(def.edges
4068
+ const inbound = new Set(wb.def.edges
4024
4069
  .filter((e) => e.target.nodeId === selectedNodeId &&
4025
4070
  e.target.handle === h)
4026
4071
  .map((e) => e.target.handle));
@@ -4596,10 +4641,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4596
4641
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
4597
4642
  // Build nodeTypes map using UI extension registry
4598
4643
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
4599
- const def = wb.export();
4600
4644
  const ids = new Set([
4601
4645
  ...Array.from(registry.nodes.keys()),
4602
- ...def.nodes.map((n) => n.typeId),
4646
+ ...wb.def.nodes.map((n) => n.typeId),
4603
4647
  ]);
4604
4648
  for (const typeId of ids) {
4605
4649
  const renderer = ui.getNodeRenderer(typeId);
@@ -4618,14 +4662,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4618
4662
  // Include uiVersion to recompute when custom renderers are registered
4619
4663
  }, [wb, registry, uiVersion, ui]);
4620
4664
  const { nodes, edges } = React.useMemo(() => {
4621
- const def = wb.export();
4622
4665
  const sel = wb.getSelection();
4623
4666
  // Merge defaults with inputs for node display (defaults shown in lighter gray)
4624
4667
  const inputsWithDefaults = {};
4625
- for (const n of def.nodes) {
4668
+ for (const n of wb.def.nodes) {
4626
4669
  const nodeInputs = inputsMap[n.nodeId] ?? {};
4627
4670
  const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
4628
- const inbound = new Set(def.edges
4671
+ const inbound = new Set(wb.def.edges
4629
4672
  .filter((e) => e.target.nodeId === n.nodeId)
4630
4673
  .map((e) => e.target.handle));
4631
4674
  const merged = { ...nodeInputs };
@@ -4638,7 +4681,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4638
4681
  inputsWithDefaults[n.nodeId] = merged;
4639
4682
  }
4640
4683
  }
4641
- const out = toReactFlow(def, wb.getPositions(), registry, {
4684
+ const out = toReactFlow(wb.def, wb.getPositions(), registry, {
4642
4685
  showValues,
4643
4686
  inputs: inputsWithDefaults,
4644
4687
  inputDefaults: inputDefaultsMap,
@@ -5162,11 +5205,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5162
5205
  });
5163
5206
 
5164
5207
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
5165
- const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5208
+ const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5166
5209
  const [transportStatus, setTransportStatus] = React.useState({
5167
5210
  state: "local",
5168
5211
  });
5169
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
5212
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
5170
5213
  const effectiveHandles = selectedNode
5171
5214
  ? computeEffectiveHandles(selectedNode, registry)
5172
5215
  : { inputs: {}, outputs: {}, inputDefaults: {} };
@@ -5194,7 +5237,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5194
5237
  if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
5195
5238
  return;
5196
5239
  try {
5197
- runner.launch(wb.export(), {
5240
+ runner.launch(wb.def, {
5198
5241
  engine: kind,
5199
5242
  invalidate: evt.shiftKey,
5200
5243
  });
@@ -5262,12 +5305,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5262
5305
  const setInitialGraph = async (d, inputs) => {
5263
5306
  await wb.load(d);
5264
5307
  try {
5265
- runner.build(wb.export());
5308
+ runner.build(wb.def);
5266
5309
  }
5267
5310
  catch { }
5268
5311
  if (inputs) {
5269
5312
  for (const [nodeId, map] of Object.entries(inputs)) {
5270
- runner.setInputs(nodeId, map);
5313
+ runner.setInputs(nodeId, map, { dry: true });
5271
5314
  }
5272
5315
  }
5273
5316
  };
@@ -5277,36 +5320,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5277
5320
  React.useEffect(() => {
5278
5321
  if (!onChange)
5279
5322
  return;
5280
- const off1 = wb.on("graphChanged", () => {
5281
- try {
5282
- const cur = wb.export();
5283
- const inputs = runner.getInputs(cur);
5284
- onChange({ def: cur, inputs });
5285
- }
5286
- catch { }
5287
- });
5288
- const off2 = runner.on("value", () => {
5323
+ const offGraphChanged = wb.on("graphChanged", () => {
5289
5324
  try {
5290
- const cur = wb.export();
5325
+ const cur = wb.def;
5291
5326
  const inputs = runner.getInputs(cur);
5292
5327
  onChange({ def: cur, inputs });
5293
5328
  }
5294
5329
  catch { }
5295
5330
  });
5296
- const off3 = wb.on("graphUiChanged", (evt) => {
5331
+ const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
5297
5332
  if (!evt.commit)
5298
5333
  return;
5299
5334
  try {
5300
- const cur = wb.export();
5335
+ const cur = wb.def;
5301
5336
  const inputs = runner.getInputs(cur);
5302
5337
  onChange({ def: cur, inputs });
5303
5338
  }
5304
5339
  catch { }
5305
5340
  });
5306
5341
  return () => {
5307
- off1();
5308
- off2();
5309
- off3();
5342
+ offGraphChanged();
5343
+ offGraphUiChanged();
5310
5344
  };
5311
5345
  }, [wb, runner, onChange]);
5312
5346
  const applyExample = React.useCallback(async (key) => {
@@ -5329,11 +5363,11 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5329
5363
  }
5330
5364
  await wb.load(def);
5331
5365
  // Build a local runtime so seeded defaults are visible pre-run
5332
- runner.build(wb.export());
5366
+ runner.build(wb.def);
5333
5367
  // Set initial inputs if provided
5334
5368
  if (inputs) {
5335
5369
  for (const [nodeId, map] of Object.entries(inputs)) {
5336
- runner.setInputs(nodeId, map);
5370
+ runner.setInputs(nodeId, map, { dry: true });
5337
5371
  }
5338
5372
  }
5339
5373
  runAutoLayout();
@@ -5414,11 +5448,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5414
5448
  // Only auto-launch for local backend; require explicit Start for remote
5415
5449
  if (backendKind !== "local")
5416
5450
  return;
5417
- const d = wb.export();
5418
- if (!d.nodes || d.nodes.length === 0)
5451
+ if (!wb.def.nodes || wb.def.nodes.length === 0)
5419
5452
  return;
5420
5453
  try {
5421
- runner.launch(d, { engine: engine });
5454
+ runner.launch(wb.def, { engine: engine });
5422
5455
  }
5423
5456
  catch {
5424
5457
  // ignore
@@ -5428,12 +5461,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5428
5461
  if (!selectedNodeId)
5429
5462
  return;
5430
5463
  // If selected input is wired (has inbound edge), ignore user input to respect runtime value
5431
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
5464
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
5432
5465
  if (isLinked)
5433
5466
  return;
5434
5467
  // If raw is undefined, pass it through to delete the input value
5435
5468
  if (raw === undefined) {
5436
- runner.setInputs(selectedNodeId, { [handle]: undefined });
5469
+ wb.setInputs(selectedNodeId, { [handle]: undefined }, { commit: true });
5437
5470
  return;
5438
5471
  }
5439
5472
  const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, handle);
@@ -5512,8 +5545,8 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5512
5545
  value = raw;
5513
5546
  }
5514
5547
  }
5515
- runner.setInputs(selectedNodeId, { [handle]: value });
5516
- }, [selectedNodeId, def.edges, effectiveHandles, runner]);
5548
+ wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
5549
+ }, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
5517
5550
  const setInput = React.useMemo(() => {
5518
5551
  if (overrides?.setInput) {
5519
5552
  return overrides.setInput(baseSetInput, {