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