@bian-womp/spark-workbench 0.2.78 → 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 +346 -283
  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 +45 -2
  8. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  9. package/lib/cjs/src/core/contracts.d.ts +4 -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 +347 -284
  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 +45 -2
  27. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  28. package/lib/esm/src/core/contracts.d.ts +4 -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",
@@ -193,15 +193,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
193
193
  }
194
194
  setInputs(nodeId, inputs, options) {
195
195
  this.emit("graphChanged", {
196
- def: this.def,
196
+ def: this._def,
197
197
  change: { type: "setInputs", nodeId, inputs },
198
198
  ...options,
199
199
  });
200
200
  }
201
201
  addNode(node, options) {
202
202
  const id = node.nodeId ??
203
- this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
204
- this.def.nodes.push({
203
+ this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
204
+ this._def.nodes.push({
205
205
  nodeId: id,
206
206
  typeId: node.typeId,
207
207
  params: node.params,
@@ -210,7 +210,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
210
210
  if (node.position)
211
211
  this.positions[id] = node.position;
212
212
  this.emit("graphChanged", {
213
- def: this.def,
213
+ def: this._def,
214
214
  change: {
215
215
  type: "addNode",
216
216
  nodeId: id,
@@ -223,26 +223,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
223
223
  return id;
224
224
  }
225
225
  removeNode(nodeId, options) {
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);
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);
228
228
  delete this.positions[nodeId];
229
229
  this.emit("graphChanged", {
230
- def: this.def,
230
+ def: this._def,
231
231
  change: { type: "removeNode", nodeId },
232
232
  ...options,
233
233
  });
234
234
  this.refreshValidation();
235
235
  }
236
236
  connect(edge, options) {
237
- const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
238
- 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({
239
239
  id,
240
240
  source: { ...edge.source },
241
241
  target: { ...edge.target },
242
242
  typeId: edge.typeId,
243
243
  });
244
244
  this.emit("graphChanged", {
245
- def: this.def,
245
+ def: this._def,
246
246
  change: { type: "connect", edgeId: id },
247
247
  ...options,
248
248
  });
@@ -250,16 +250,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
250
250
  return id;
251
251
  }
252
252
  disconnect(edgeId, options) {
253
- this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
253
+ this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
254
254
  this.emit("graphChanged", {
255
- def: this.def,
255
+ def: this._def,
256
256
  change: { type: "disconnect", edgeId },
257
257
  ...options,
258
258
  });
259
259
  this.emit("validationChanged", this.validate());
260
260
  }
261
261
  updateEdgeType(edgeId, typeId) {
262
- const e = this.def.edges.find((x) => x.id === edgeId);
262
+ const e = this._def.edges.find((x) => x.id === edgeId);
263
263
  if (!e)
264
264
  return;
265
265
  if (!typeId)
@@ -267,18 +267,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
267
267
  else
268
268
  e.typeId = typeId;
269
269
  this.emit("graphChanged", {
270
- def: this.def,
270
+ def: this._def,
271
271
  change: { type: "updateEdgeType", edgeId, typeId },
272
272
  });
273
273
  this.refreshValidation();
274
274
  }
275
275
  updateParams(nodeId, params) {
276
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
276
+ const n = this._def.nodes.find((n) => n.nodeId === nodeId);
277
277
  if (!n)
278
278
  return;
279
279
  n.params = { ...(n.params ?? {}), ...params };
280
280
  this.emit("graphChanged", {
281
- def: this.def,
281
+ def: this._def,
282
282
  change: { type: "updateParams", nodeId },
283
283
  });
284
284
  }
@@ -286,7 +286,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
286
286
  setPositions(map, options) {
287
287
  this.positions = { ...this.positions, ...map };
288
288
  this.emit("graphUiChanged", {
289
- def: this.def,
289
+ def: this._def,
290
290
  change: { type: "moveNodes" },
291
291
  ...options,
292
292
  });
@@ -294,17 +294,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
294
294
  getPositions() {
295
295
  return { ...this.positions };
296
296
  }
297
- setSelection(sel, options) {
297
+ setSelectionInternal(sel, options) {
298
298
  if (lod.isEqual(this.selection, sel))
299
299
  return;
300
300
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
301
301
  this.emit("selectionChanged", this.selection);
302
302
  this.emit("graphUiChanged", {
303
- def: this.def,
303
+ def: this._def,
304
304
  change: { type: "selection" },
305
305
  ...options,
306
306
  });
307
307
  }
308
+ setSelection(sel, options) {
309
+ this.setSelectionInternal(sel, options);
310
+ }
308
311
  getSelection() {
309
312
  return {
310
313
  nodes: [...this.selection.nodes],
@@ -325,14 +328,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
325
328
  this.disconnect(edgeId);
326
329
  }
327
330
  // Clear selection
328
- this.setSelection({ nodes: [], edges: [] }, options);
331
+ this.setSelectionInternal({ nodes: [], edges: [] }, options);
329
332
  }
330
333
  setViewport(viewport) {
331
334
  if (lod.isEqual(this.viewport, viewport))
332
335
  return;
333
336
  this.viewport = { ...viewport };
334
337
  this.emit("graphUiChanged", {
335
- def: this.def,
338
+ def: this._def,
336
339
  change: { type: "viewport" },
337
340
  });
338
341
  }
@@ -340,8 +343,8 @@ class InMemoryWorkbench extends AbstractWorkbench {
340
343
  return this.viewport ? { ...this.viewport } : null;
341
344
  }
342
345
  getUIState() {
343
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
344
- 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));
345
348
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
346
349
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
347
350
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
@@ -380,6 +383,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
380
383
  }
381
384
  setRuntimeState(runtime) {
382
385
  this.runtimeState = runtime ? { ...runtime } : null;
386
+ this.emit("runtimeMetadataChanged", {});
383
387
  }
384
388
  getHistory() {
385
389
  return this.historyState;
@@ -396,6 +400,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
396
400
  const nodeMeta = current.nodes[nodeId] ?? {};
397
401
  const updated = updater({ ...nodeMeta });
398
402
  this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
403
+ this.emit("runtimeMetadataChanged", { nodeId });
399
404
  }
400
405
  on(event, handler) {
401
406
  if (!this.listeners.has(event))
@@ -420,11 +425,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
420
425
  const selection = this.getSelection();
421
426
  if (selection.nodes.length === 0)
422
427
  return null;
423
- const def = this.export();
424
428
  const positions = this.getPositions();
425
429
  const selectedNodeSet = new Set(selection.nodes);
426
430
  // Collect nodes to copy
427
- const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
431
+ const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
428
432
  if (nodesToCopy.length === 0)
429
433
  return null;
430
434
  // Calculate bounds
@@ -448,12 +452,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
448
452
  const centerY = (bounds.minY + bounds.maxY) / 2;
449
453
  // Get inputs for each node
450
454
  // Include values from inbound edges if those edges are selected
451
- const allInputs = runner.getInputs(def);
455
+ const allInputs = runner.getInputs(this.def);
452
456
  const selectedEdgeSet = new Set(selection.edges);
453
457
  const copiedNodes = nodesToCopy.map((node) => {
454
458
  const pos = positions[node.nodeId] || { x: 0, y: 0 };
455
459
  // Get all inbound edges for this node
456
- const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
460
+ const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
457
461
  // Build set of handles that have inbound edges
458
462
  // But only exclude handles whose edges are NOT selected
459
463
  const inboundHandlesToExclude = new Set(inboundEdges
@@ -477,7 +481,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
477
481
  };
478
482
  });
479
483
  // Collect edges between copied nodes
480
- const copiedEdges = def.edges
484
+ const copiedEdges = this.def.edges
481
485
  .filter((edge) => {
482
486
  return (selectedNodeSet.has(edge.source.nodeId) &&
483
487
  selectedNodeSet.has(edge.target.nodeId));
@@ -495,6 +499,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
495
499
  bounds,
496
500
  };
497
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
+ }
498
689
  /**
499
690
  * Paste copied graph data at the specified center position.
500
691
  * Returns the mapping from original node IDs to new node IDs.
@@ -540,7 +731,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
540
731
  }
541
732
  }
542
733
  // Select the newly pasted nodes
543
- this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
734
+ this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
544
735
  return { nodeIdMap, edgeIds };
545
736
  }
546
737
  /**
@@ -565,8 +756,8 @@ class CLIWorkbench {
565
756
  async load(def) {
566
757
  await this.wb.load(def);
567
758
  }
568
- print(def, options) {
569
- const d = def ?? this.wb.export();
759
+ print(options) {
760
+ const d = this.wb.def;
570
761
  const detail = !!options?.detail;
571
762
  const lines = [];
572
763
  lines.push(`Nodes (${d.nodes.length})`);
@@ -1935,8 +2126,7 @@ function useWorkbenchBridge(wb) {
1935
2126
  if (!params.sourceHandle || !params.targetHandle)
1936
2127
  return;
1937
2128
  // Prevent duplicate edges between the same endpoints
1938
- const def = wb.export();
1939
- const exists = def.edges.some((e) => e.source.nodeId === params.source &&
2129
+ const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
1940
2130
  e.source.handle === params.sourceHandle &&
1941
2131
  e.target.nodeId === params.target &&
1942
2132
  e.target.handle === params.targetHandle);
@@ -1986,7 +2176,10 @@ function useWorkbenchBridge(wb) {
1986
2176
  }
1987
2177
  }
1988
2178
  if (selectionChanged) {
1989
- 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
+ });
1990
2183
  }
1991
2184
  if (Object.keys(positions).length > 0) {
1992
2185
  wb.setPositions(positions, { commit });
@@ -2021,7 +2214,10 @@ function useWorkbenchBridge(wb) {
2021
2214
  }
2022
2215
  }
2023
2216
  if (selectionChanged) {
2024
- wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
2217
+ wb.setSelection({
2218
+ nodes: current.nodes,
2219
+ edges: Array.from(nextEdgeIds),
2220
+ });
2025
2221
  }
2026
2222
  }, [wb]);
2027
2223
  const onNodesDelete = useCallback((nodes) => {
@@ -2467,7 +2663,6 @@ function isSnapshotPayload(parsed) {
2467
2663
  }
2468
2664
  async function download(wb, runner) {
2469
2665
  try {
2470
- const def = wb.export();
2471
2666
  const fullUiState = wb.getUIState();
2472
2667
  const uiState = excludeViewportFromUIState(fullUiState);
2473
2668
  const runtimeState = wb.getRuntimeState();
@@ -2476,7 +2671,7 @@ async function download(wb, runner) {
2476
2671
  const fullSnapshot = await runner.snapshotFull();
2477
2672
  snapshot = {
2478
2673
  ...fullSnapshot,
2479
- def,
2674
+ def: wb.def,
2480
2675
  extData: {
2481
2676
  ...(fullSnapshot.extData || {}),
2482
2677
  ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
@@ -2485,9 +2680,9 @@ async function download(wb, runner) {
2485
2680
  };
2486
2681
  }
2487
2682
  else {
2488
- const inputs = runner.getInputs(def);
2683
+ const inputs = runner.getInputs(wb.def);
2489
2684
  snapshot = {
2490
- def,
2685
+ def: wb.def,
2491
2686
  inputs,
2492
2687
  outputs: {},
2493
2688
  environment: {},
@@ -2526,7 +2721,7 @@ async function upload(parsed, wb, runner) {
2526
2721
  }
2527
2722
  if (runner.isRunning()) {
2528
2723
  await runner.applySnapshotFull({
2529
- def,
2724
+ def: wb.def,
2530
2725
  environment,
2531
2726
  inputs,
2532
2727
  outputs,
@@ -2534,7 +2729,7 @@ async function upload(parsed, wb, runner) {
2534
2729
  });
2535
2730
  }
2536
2731
  else {
2537
- runner.build(wb.export());
2732
+ runner.build(wb.def);
2538
2733
  if (inputs && typeof inputs === "object") {
2539
2734
  for (const [nodeId, map] of Object.entries(inputs)) {
2540
2735
  runner.setInputs(nodeId, map, { dry: true });
@@ -2630,14 +2825,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2630
2825
  const versionTick = useWorkbenchVersionTick(runner);
2631
2826
  const valuesTick = versionTick + graphTick + graphUiTick;
2632
2827
  // Def and IO values
2633
- const def = wb.export();
2634
- const inputsMap = useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
2635
- const inputDefaultsMap = useMemo(() => runner.getInputDefaults(def), [runner, def, valuesTick]);
2636
- 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]);
2637
2831
  const outputTypesMap = useMemo(() => {
2638
2832
  const out = {};
2639
2833
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
2640
- for (const n of def.nodes) {
2834
+ for (const n of wb.def.nodes) {
2641
2835
  const effectiveHandles = computeEffectiveHandles(n, registry);
2642
2836
  const outputsDecl = effectiveHandles.outputs;
2643
2837
  const handles = Object.keys(outputsDecl);
@@ -2652,14 +2846,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2652
2846
  out[n.nodeId] = cur;
2653
2847
  }
2654
2848
  return out;
2655
- }, [def, outputsMap, registry]);
2849
+ }, [wb, wb.def, outputsMap, registry]);
2656
2850
  // Initialize nodes and derive invalidated status from persisted metadata
2657
2851
  useEffect(() => {
2658
2852
  const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
2659
2853
  setNodeStatus((prev) => {
2660
2854
  const next = { ...prev };
2661
2855
  const metadata = workbenchRuntimeState;
2662
- for (const n of def.nodes) {
2856
+ for (const n of wb.def.nodes) {
2663
2857
  const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
2664
2858
  const nodeMeta = metadata.nodes[n.nodeId];
2665
2859
  const updates = {};
@@ -2681,18 +2875,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2681
2875
  }
2682
2876
  return next;
2683
2877
  });
2684
- }, [def, wb]);
2878
+ }, [wb.def, wb]);
2685
2879
  // Auto layout (simple layered layout)
2686
2880
  const runAutoLayout = useCallback(() => {
2687
- const cur = wb.export();
2688
2881
  // Build DAG layers by indegree
2689
2882
  const indegree = {};
2690
2883
  const adj = {};
2691
- for (const n of cur.nodes) {
2884
+ for (const n of wb.def.nodes) {
2692
2885
  indegree[n.nodeId] = 0;
2693
2886
  adj[n.nodeId] = [];
2694
2887
  }
2695
- for (const e of cur.edges) {
2888
+ for (const e of wb.def.edges) {
2696
2889
  indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
2697
2890
  adj[e.source.nodeId].push(e.target.nodeId);
2698
2891
  }
@@ -2723,7 +2916,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2723
2916
  let maxWidth = 0;
2724
2917
  const heights = {};
2725
2918
  for (const id of layer) {
2726
- const node = cur.nodes.find((n) => n.nodeId === id);
2919
+ const node = wb.def.nodes.find((n) => n.nodeId === id);
2727
2920
  if (!node)
2728
2921
  continue;
2729
2922
  // Prefer showValues sizing similar to node rendering
@@ -2749,26 +2942,26 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2749
2942
  curX += maxWidth + H_GAP;
2750
2943
  }
2751
2944
  wb.setPositions(pos, { commit: true, reason: "auto-layout" });
2752
- }, [wb, registry, overrides?.getDefaultNodeSize]);
2945
+ }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2753
2946
  const updateEdgeType = useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2754
2947
  const triggerExternal = useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2755
2948
  // Helper to save runtime metadata and UI state to extData
2756
- const saveUiRuntimeMetadata = useCallback(async () => {
2949
+ const saveUiRuntimeMetadata = useCallback(async (workbench, graphRunner) => {
2757
2950
  try {
2758
- const current = wb.getRuntimeState() ?? { nodes: {} };
2951
+ const current = workbench.getRuntimeState() ?? { nodes: {} };
2759
2952
  const metadata = { nodes: { ...current.nodes } };
2760
2953
  // Clean up metadata for nodes that no longer exist
2761
- const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
2954
+ const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
2762
2955
  for (const nodeId of Object.keys(metadata.nodes)) {
2763
2956
  if (!nodeIds.has(nodeId)) {
2764
2957
  delete metadata.nodes[nodeId];
2765
2958
  }
2766
2959
  }
2767
2960
  // Save cleaned metadata to workbench state
2768
- wb.setRuntimeState(metadata);
2769
- const fullUiState = wb.getUIState();
2961
+ workbench.setRuntimeState(metadata);
2962
+ const fullUiState = workbench.getUIState();
2770
2963
  const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
2771
- await runner.setExtData?.({
2964
+ await graphRunner.setExtData?.({
2772
2965
  ...(Object.keys(uiWithoutViewport || {}).length > 0
2773
2966
  ? { ui: uiWithoutViewport }
2774
2967
  : {}),
@@ -2778,7 +2971,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2778
2971
  catch (err) {
2779
2972
  console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
2780
2973
  }
2781
- }, [wb, def, runner]);
2974
+ }, []);
2782
2975
  // Subscribe to runner/workbench events
2783
2976
  useEffect(() => {
2784
2977
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -2804,9 +2997,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2804
2997
  });
2805
2998
  // Helper to apply resolved handles from event payload to workbench
2806
2999
  const applyResolvedHandles = (resolvedHandles) => {
2807
- const cur = wb.export();
2808
3000
  let changed = false;
2809
- for (const n of cur.nodes) {
3001
+ for (const n of wb.def.nodes) {
2810
3002
  const updated = resolvedHandles[n.nodeId];
2811
3003
  if (updated) {
2812
3004
  const before = JSON.stringify(n.resolvedHandles || {});
@@ -3095,6 +3287,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3095
3287
  else if (changeType === "updateEdgeType") {
3096
3288
  reason = "update-edge-type";
3097
3289
  }
3290
+ else if (changeType === "setInputs") {
3291
+ reason = "set-inputs";
3292
+ }
3098
3293
  }
3099
3294
  if (event.change?.type === "setInputs") {
3100
3295
  const { nodeId, inputs } = event.change;
@@ -3102,7 +3297,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3102
3297
  }
3103
3298
  if (!runner.isRunning()) {
3104
3299
  if (event.commit) {
3105
- await saveUiRuntimeMetadata();
3300
+ await saveUiRuntimeMetadata(wb, runner);
3106
3301
  const history = await runner.commit(reason).catch((err) => {
3107
3302
  console.error("[WorkbenchContext] Error committing:", err);
3108
3303
  return undefined;
@@ -3135,7 +3330,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3135
3330
  await runner.update(event.def, { dry: event.dry });
3136
3331
  }
3137
3332
  if (event.commit) {
3138
- await saveUiRuntimeMetadata();
3333
+ await saveUiRuntimeMetadata(wb, runner);
3139
3334
  const history = await runner
3140
3335
  .commit(event.reason ?? reason)
3141
3336
  .catch((err) => {
@@ -3156,7 +3351,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3156
3351
  setSelectedNodeId(sel.nodes?.[0]);
3157
3352
  setSelectedEdgeId(sel.edges?.[0]);
3158
3353
  if (sel.commit) {
3159
- await saveUiRuntimeMetadata();
3354
+ await saveUiRuntimeMetadata(wb, runner);
3160
3355
  const history = await runner
3161
3356
  .commit(sel.reason ?? "selection")
3162
3357
  .catch((err) => {
@@ -3195,7 +3390,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3195
3390
  reason = "selection";
3196
3391
  }
3197
3392
  }
3198
- await saveUiRuntimeMetadata();
3393
+ await saveUiRuntimeMetadata(wb, runner);
3199
3394
  const history = await runner
3200
3395
  .commit(event.reason ?? reason)
3201
3396
  .catch((err) => {
@@ -3215,7 +3410,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3215
3410
  wb.setRegistry(newReg);
3216
3411
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
3217
3412
  try {
3218
- await runner.update(wb.export());
3413
+ await runner.update(wb.def);
3219
3414
  }
3220
3415
  catch {
3221
3416
  console.error("Failed to update graph definition after registry changed");
@@ -3238,7 +3433,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3238
3433
  setNodeStatus(() => {
3239
3434
  const next = {};
3240
3435
  const metadata = wb.getRuntimeState() ?? { nodes: {} };
3241
- for (const n of def.nodes) {
3436
+ for (const n of wb.def.nodes) {
3242
3437
  const nodeMeta = metadata.nodes[n.nodeId];
3243
3438
  next[n.nodeId] = {
3244
3439
  activeRuns: 0,
@@ -3253,6 +3448,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3253
3448
  errorRunsRef.current = {};
3254
3449
  }
3255
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
+ });
3256
3475
  wb.refreshValidation();
3257
3476
  return () => {
3258
3477
  offRunnerValue();
@@ -3270,13 +3489,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3270
3489
  offRunnerRegistry();
3271
3490
  offRunnerTransport();
3272
3491
  offFlowViewport();
3492
+ offWbRuntimeMetadataChanged();
3273
3493
  };
3274
3494
  }, [runner, wb, setRegistry]);
3275
3495
  const isRunning = useCallback(() => runner.isRunning(), [runner]);
3276
3496
  const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
3277
3497
  const start = useCallback((engine) => {
3278
3498
  try {
3279
- runner.launch(wb.export(), { engine });
3499
+ runner.launch(wb.def, { engine });
3280
3500
  }
3281
3501
  catch { }
3282
3502
  }, [runner, wb]);
@@ -3351,7 +3571,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3351
3571
  runner,
3352
3572
  registry,
3353
3573
  setRegistry,
3354
- def,
3355
3574
  selectedNodeId,
3356
3575
  selectedEdgeId,
3357
3576
  setSelection,
@@ -3392,7 +3611,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3392
3611
  runner,
3393
3612
  registry,
3394
3613
  setRegistry,
3395
- def,
3396
3614
  selectedNodeId,
3397
3615
  selectedEdgeId,
3398
3616
  setSelection,
@@ -3432,137 +3650,23 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3432
3650
  }
3433
3651
 
3434
3652
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3435
- const doBake = async (handleId) => {
3436
- try {
3437
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3438
- const raw = outputsMap?.[nodeId]?.[handleId];
3439
- let newNodeId;
3440
- if (!typeId || raw === undefined)
3441
- return;
3442
- const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
3443
- const coerceIfNeeded = async (fromType, toType, value) => {
3444
- if (!toType || toType === fromType || !runner?.coerce)
3445
- return value;
3446
- try {
3447
- return await runner.coerce(fromType, toType, value);
3448
- }
3449
- catch {
3450
- return value;
3451
- }
3452
- };
3453
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3454
- const isArray = typeId.endsWith("[]");
3455
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3456
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3457
- const tElem = registry.types.get(baseTypeId);
3458
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3459
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3460
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3461
- if (singleTarget) {
3462
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3463
- const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3464
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3465
- newNodeId = wb.addNode({
3466
- typeId: singleTarget.nodeTypeId,
3467
- position: { x: pos.x + 180, y: pos.y },
3468
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3469
- }
3470
- else if (isArray && arrTarget) {
3471
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3472
- const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3473
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3474
- newNodeId = wb.addNode({
3475
- typeId: arrTarget.nodeTypeId,
3476
- position: { x: pos.x + 180, y: pos.y },
3477
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3478
- }
3479
- else if (isArray && elemTarget) {
3480
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3481
- const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3482
- const src = unwrap(raw);
3483
- const items = Array.isArray(src) ? src : [src];
3484
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3485
- const COLS = 4;
3486
- const DX = 180;
3487
- const DY = 160;
3488
- for (let idx = 0; idx < coercedItems.length; idx++) {
3489
- const col = idx % COLS;
3490
- const row = Math.floor(idx / COLS);
3491
- newNodeId = wb.addNode({
3492
- typeId: elemTarget.nodeTypeId,
3493
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3494
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3495
- }
3496
- }
3497
- if (newNodeId) {
3498
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
3499
- }
3500
- }
3501
- catch { }
3502
- };
3503
3653
  return {
3504
3654
  onDelete: () => {
3505
3655
  wb.removeNode(nodeId, { commit: true });
3506
3656
  onClose();
3507
3657
  },
3508
3658
  onDuplicate: async () => {
3509
- const def = wb.export();
3510
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3511
- if (!n)
3512
- return onClose();
3513
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3514
- const inboundHandles = new Set(def.edges
3515
- .filter((e) => e.target.nodeId === nodeId)
3516
- .map((e) => e.target.handle));
3517
- const allInputs = runner.getInputs(def)[nodeId] || {};
3518
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3519
- const newNodeId = wb.addNode({
3520
- typeId: n.typeId,
3521
- params: n.params,
3522
- position: { x: pos.x + 24, y: pos.y + 24 },
3523
- resolvedHandles: n.resolvedHandles,
3524
- }, {
3525
- inputs: inputsWithoutBindings,
3526
- copyOutputsFrom: nodeId,
3527
- dry: true,
3659
+ wb.duplicateNode(nodeId, runner, {
3660
+ commit: true,
3661
+ reason: "duplicate-node",
3528
3662
  });
3529
- // Select the newly duplicated node
3530
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
3531
3663
  onClose();
3532
3664
  },
3533
3665
  onDuplicateWithEdges: async () => {
3534
- const def = wb.export();
3535
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3536
- if (!n)
3537
- return onClose();
3538
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3539
- // Get inputs without bindings (literal values only)
3540
- const inputs = runner.getInputs(def)[nodeId] || {};
3541
- // Add the duplicated node
3542
- const newNodeId = wb.addNode({
3543
- typeId: n.typeId,
3544
- params: n.params,
3545
- position: { x: pos.x + 24, y: pos.y + 24 },
3546
- resolvedHandles: n.resolvedHandles,
3547
- }, {
3548
- inputs,
3549
- copyOutputsFrom: nodeId,
3550
- dry: true,
3666
+ wb.duplicateNodeWithEdges(nodeId, runner, {
3667
+ commit: true,
3668
+ reason: "duplicate-node-with-edges",
3551
3669
  });
3552
- // Find all incoming edges (edges where target is the original node)
3553
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3554
- // Duplicate each incoming edge to point to the new node
3555
- for (const edge of incomingEdges) {
3556
- wb.connect({
3557
- source: edge.source, // Keep the same source
3558
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3559
- typeId: edge.typeId,
3560
- }, { dry: true });
3561
- }
3562
- // Select the newly duplicated node and edges
3563
- if (newNodeId) {
3564
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
3565
- }
3566
3670
  onClose();
3567
3671
  },
3568
3672
  onRunPull: async () => {
@@ -3573,7 +3677,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3573
3677
  onClose();
3574
3678
  },
3575
3679
  onBake: async (handleId) => {
3576
- 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
+ });
3577
3687
  onClose();
3578
3688
  },
3579
3689
  onCopy: () => {
@@ -3630,42 +3740,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
3630
3740
  function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3631
3741
  const onDuplicate = runner
3632
3742
  ? () => {
3633
- const selection = wb.getSelection();
3634
- if (selection.nodes.length === 0) {
3635
- onClose();
3636
- return;
3637
- }
3638
- const def = wb.export();
3639
- const positions = wb.getPositions();
3640
- const newNodes = [];
3641
- // Duplicate each selected node
3642
- for (const nodeId of selection.nodes) {
3643
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3644
- if (!n)
3645
- continue;
3646
- const pos = positions[nodeId] || { x: 0, y: 0 };
3647
- // Get inputs without bindings (literal values only)
3648
- const allInputs = runner.getInputs(def)[nodeId] || {};
3649
- const inboundHandles = new Set(def.edges
3650
- .filter((e) => e.target.nodeId === nodeId)
3651
- .map((e) => e.target.handle));
3652
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3653
- const newNodeId = wb.addNode({
3654
- typeId: n.typeId,
3655
- params: n.params,
3656
- position: { x: pos.x + 24, y: pos.y + 24 },
3657
- resolvedHandles: n.resolvedHandles,
3658
- }, {
3659
- inputs: inputsWithoutBindings,
3660
- copyOutputsFrom: nodeId,
3661
- dry: true,
3662
- });
3663
- newNodes.push(newNodeId);
3664
- }
3665
- // Select all newly duplicated nodes
3666
- if (newNodes.length > 0) {
3667
- wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
3668
- }
3743
+ wb.duplicateSelection(runner, {
3744
+ commit: true,
3745
+ reason: "duplicate-selection",
3746
+ });
3669
3747
  onClose();
3670
3748
  }
3671
3749
  : undefined;
@@ -3700,9 +3778,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3700
3778
  const canRedo = history ? history.redoCount > 0 : undefined;
3701
3779
  const onSelectAll = wb
3702
3780
  ? () => {
3703
- const def = wb.export();
3704
- const allNodeIds = def.nodes.map((n) => n.nodeId);
3705
- 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);
3706
3783
  wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
3707
3784
  onClose();
3708
3785
  }
@@ -3721,8 +3798,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3721
3798
  }
3722
3799
  function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3723
3800
  try {
3724
- const def = wb.export();
3725
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3801
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
3726
3802
  if (!node)
3727
3803
  return [];
3728
3804
  const desc = registry.nodes.get(node.typeId);
@@ -3821,13 +3897,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3821
3897
  return String(value ?? "");
3822
3898
  }
3823
3899
  };
3824
- 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();
3825
3901
  const nodeValidationIssues = validationByNode.issues;
3826
3902
  const edgeValidationIssues = validationByEdge.issues;
3827
3903
  const nodeValidationHandles = validationByNode;
3828
3904
  const globalValidationIssues = validationGlobal;
3829
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3830
- 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);
3831
3907
  // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
3832
3908
  const effectiveHandles = selectedNode
3833
3909
  ? computeEffectiveHandles(selectedNode, registry)
@@ -3959,7 +4035,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3959
4035
  setOriginals(nextOriginals);
3960
4036
  }, [selectedNodeId, selectedNode, registry, valuesTick]);
3961
4037
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
3962
- const { wb } = useWorkbenchContext();
3963
4038
  const deleteEdgeById = (edgeId) => {
3964
4039
  if (!edgeId)
3965
4040
  return;
@@ -3986,9 +4061,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3986
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 ??
3987
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) => {
3988
4063
  const typeId = getInputTypeId(effectiveHandles.inputs, h);
3989
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
4064
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
3990
4065
  e.target.handle === h);
3991
- const inbound = new Set(def.edges
4066
+ const inbound = new Set(wb.def.edges
3992
4067
  .filter((e) => e.target.nodeId === selectedNodeId &&
3993
4068
  e.target.handle === h)
3994
4069
  .map((e) => e.target.handle));
@@ -4564,10 +4639,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4564
4639
  const { nodeTypes, resolveNodeType } = useMemo(() => {
4565
4640
  // Build nodeTypes map using UI extension registry
4566
4641
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
4567
- const def = wb.export();
4568
4642
  const ids = new Set([
4569
4643
  ...Array.from(registry.nodes.keys()),
4570
- ...def.nodes.map((n) => n.typeId),
4644
+ ...wb.def.nodes.map((n) => n.typeId),
4571
4645
  ]);
4572
4646
  for (const typeId of ids) {
4573
4647
  const renderer = ui.getNodeRenderer(typeId);
@@ -4586,14 +4660,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4586
4660
  // Include uiVersion to recompute when custom renderers are registered
4587
4661
  }, [wb, registry, uiVersion, ui]);
4588
4662
  const { nodes, edges } = useMemo(() => {
4589
- const def = wb.export();
4590
4663
  const sel = wb.getSelection();
4591
4664
  // Merge defaults with inputs for node display (defaults shown in lighter gray)
4592
4665
  const inputsWithDefaults = {};
4593
- for (const n of def.nodes) {
4666
+ for (const n of wb.def.nodes) {
4594
4667
  const nodeInputs = inputsMap[n.nodeId] ?? {};
4595
4668
  const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
4596
- const inbound = new Set(def.edges
4669
+ const inbound = new Set(wb.def.edges
4597
4670
  .filter((e) => e.target.nodeId === n.nodeId)
4598
4671
  .map((e) => e.target.handle));
4599
4672
  const merged = { ...nodeInputs };
@@ -4606,7 +4679,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4606
4679
  inputsWithDefaults[n.nodeId] = merged;
4607
4680
  }
4608
4681
  }
4609
- const out = toReactFlow(def, wb.getPositions(), registry, {
4682
+ const out = toReactFlow(wb.def, wb.getPositions(), registry, {
4610
4683
  showValues,
4611
4684
  inputs: inputsWithDefaults,
4612
4685
  inputDefaults: inputDefaultsMap,
@@ -5130,11 +5203,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5130
5203
  });
5131
5204
 
5132
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, }) {
5133
- const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5206
+ const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5134
5207
  const [transportStatus, setTransportStatus] = useState({
5135
5208
  state: "local",
5136
5209
  });
5137
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
5210
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
5138
5211
  const effectiveHandles = selectedNode
5139
5212
  ? computeEffectiveHandles(selectedNode, registry)
5140
5213
  : { inputs: {}, outputs: {}, inputDefaults: {} };
@@ -5162,7 +5235,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5162
5235
  if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
5163
5236
  return;
5164
5237
  try {
5165
- runner.launch(wb.export(), {
5238
+ runner.launch(wb.def, {
5166
5239
  engine: kind,
5167
5240
  invalidate: evt.shiftKey,
5168
5241
  });
@@ -5230,7 +5303,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5230
5303
  const setInitialGraph = async (d, inputs) => {
5231
5304
  await wb.load(d);
5232
5305
  try {
5233
- runner.build(wb.export());
5306
+ runner.build(wb.def);
5234
5307
  }
5235
5308
  catch { }
5236
5309
  if (inputs) {
@@ -5245,36 +5318,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5245
5318
  useEffect(() => {
5246
5319
  if (!onChange)
5247
5320
  return;
5248
- const off1 = wb.on("graphChanged", () => {
5249
- try {
5250
- const cur = wb.export();
5251
- const inputs = runner.getInputs(cur);
5252
- onChange({ def: cur, inputs });
5253
- }
5254
- catch { }
5255
- });
5256
- const off2 = runner.on("value", () => {
5321
+ const offGraphChanged = wb.on("graphChanged", () => {
5257
5322
  try {
5258
- const cur = wb.export();
5323
+ const cur = wb.def;
5259
5324
  const inputs = runner.getInputs(cur);
5260
5325
  onChange({ def: cur, inputs });
5261
5326
  }
5262
5327
  catch { }
5263
5328
  });
5264
- const off3 = wb.on("graphUiChanged", (evt) => {
5329
+ const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
5265
5330
  if (!evt.commit)
5266
5331
  return;
5267
5332
  try {
5268
- const cur = wb.export();
5333
+ const cur = wb.def;
5269
5334
  const inputs = runner.getInputs(cur);
5270
5335
  onChange({ def: cur, inputs });
5271
5336
  }
5272
5337
  catch { }
5273
5338
  });
5274
5339
  return () => {
5275
- off1();
5276
- off2();
5277
- off3();
5340
+ offGraphChanged();
5341
+ offGraphUiChanged();
5278
5342
  };
5279
5343
  }, [wb, runner, onChange]);
5280
5344
  const applyExample = useCallback(async (key) => {
@@ -5297,7 +5361,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5297
5361
  }
5298
5362
  await wb.load(def);
5299
5363
  // Build a local runtime so seeded defaults are visible pre-run
5300
- runner.build(wb.export());
5364
+ runner.build(wb.def);
5301
5365
  // Set initial inputs if provided
5302
5366
  if (inputs) {
5303
5367
  for (const [nodeId, map] of Object.entries(inputs)) {
@@ -5382,11 +5446,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5382
5446
  // Only auto-launch for local backend; require explicit Start for remote
5383
5447
  if (backendKind !== "local")
5384
5448
  return;
5385
- const d = wb.export();
5386
- if (!d.nodes || d.nodes.length === 0)
5449
+ if (!wb.def.nodes || wb.def.nodes.length === 0)
5387
5450
  return;
5388
5451
  try {
5389
- runner.launch(d, { engine: engine });
5452
+ runner.launch(wb.def, { engine: engine });
5390
5453
  }
5391
5454
  catch {
5392
5455
  // ignore
@@ -5396,7 +5459,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5396
5459
  if (!selectedNodeId)
5397
5460
  return;
5398
5461
  // If selected input is wired (has inbound edge), ignore user input to respect runtime value
5399
- 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);
5400
5463
  if (isLinked)
5401
5464
  return;
5402
5465
  // If raw is undefined, pass it through to delete the input value
@@ -5481,7 +5544,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5481
5544
  }
5482
5545
  }
5483
5546
  wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
5484
- }, [selectedNodeId, def.edges, effectiveHandles, wb]);
5547
+ }, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
5485
5548
  const setInput = useMemo(() => {
5486
5549
  if (overrides?.setInput) {
5487
5550
  return overrides.setInput(baseSetInput, {