@bian-womp/spark-workbench 0.2.78 → 0.2.80

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 (43) hide show
  1. package/lib/cjs/index.cjs +456 -287
  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 +61 -3
  8. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  9. package/lib/cjs/src/core/contracts.d.ts +7 -2
  10. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/DefaultNode.d.ts +3 -2
  12. package/lib/cjs/src/misc/DefaultNode.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/Inspector.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  15. package/lib/cjs/src/misc/WorkbenchStudio.d.ts.map +1 -1
  16. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -1
  17. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  18. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  19. package/lib/cjs/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
  20. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  21. package/lib/cjs/src/misc/load.d.ts.map +1 -1
  22. package/lib/esm/index.js +457 -288
  23. package/lib/esm/index.js.map +1 -1
  24. package/lib/esm/src/adapters/cli/index.d.ts +1 -1
  25. package/lib/esm/src/adapters/cli/index.d.ts.map +1 -1
  26. package/lib/esm/src/core/AbstractWorkbench.d.ts +1 -1
  27. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  28. package/lib/esm/src/core/InMemoryWorkbench.d.ts +61 -3
  29. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  30. package/lib/esm/src/core/contracts.d.ts +7 -2
  31. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  32. package/lib/esm/src/misc/DefaultNode.d.ts +3 -2
  33. package/lib/esm/src/misc/DefaultNode.d.ts.map +1 -1
  34. package/lib/esm/src/misc/Inspector.d.ts.map +1 -1
  35. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  36. package/lib/esm/src/misc/WorkbenchStudio.d.ts.map +1 -1
  37. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -1
  38. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  39. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  40. package/lib/esm/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -1
  41. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  42. package/lib/esm/src/misc/load.d.ts.map +1 -1
  43. 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,52 +121,53 @@ 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 = {
128
128
  nodes: [],
129
129
  edges: [],
130
130
  };
131
- this.viewport = null;
131
+ this.nodeNames = {};
132
132
  this.runtimeState = null;
133
+ this.viewport = null;
133
134
  this.historyState = undefined;
134
135
  this.copiedData = null;
135
136
  }
137
+ get def() {
138
+ return this._def;
139
+ }
136
140
  setRegistry(registry) {
137
141
  this.registry = registry;
138
142
  }
139
143
  async load(def) {
140
- this.def = { nodes: [...def.nodes], edges: [...def.edges] };
144
+ this._def = { nodes: [...def.nodes], edges: [...def.edges] };
141
145
  if (this.layout) {
142
- const { positions } = await this.layout.layout(this.def);
146
+ const { positions } = await this.layout.layout(this._def);
143
147
  this.positions = positions;
144
148
  }
145
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
146
- const defEdgeIds = new Set(this.def.edges.map((e) => e.id));
149
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
150
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
147
151
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
148
152
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
149
153
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
150
154
  this.positions = filteredPositions;
151
155
  this.selection = { nodes: filteredNodes, edges: filteredEdges };
152
- this.emit("graphChanged", { def: this.def });
156
+ this.emit("graphChanged", { def: this._def });
153
157
  this.refreshValidation();
154
158
  }
155
- export() {
156
- return this.def;
157
- }
158
159
  refreshValidation() {
159
160
  this.emit("validationChanged", this.validate());
160
161
  }
161
162
  validate() {
162
163
  if (this.registry) {
163
164
  const builder = new GraphBuilder(this.registry);
164
- const report = builder.validate(this.def);
165
+ const report = builder.validate(this._def);
165
166
  return report;
166
167
  }
167
168
  const issues = [];
168
169
  const nodeIds = new Set();
169
- for (const n of this.def.nodes) {
170
+ for (const n of this._def.nodes) {
170
171
  if (nodeIds.has(n.nodeId)) {
171
172
  issues.push({
172
173
  level: "error",
@@ -178,7 +179,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
178
179
  nodeIds.add(n.nodeId);
179
180
  }
180
181
  const edgeIds = new Set();
181
- for (const e of this.def.edges) {
182
+ for (const e of this._def.edges) {
182
183
  if (edgeIds.has(e.id)) {
183
184
  issues.push({
184
185
  level: "error",
@@ -193,15 +194,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
193
194
  }
194
195
  setInputs(nodeId, inputs, options) {
195
196
  this.emit("graphChanged", {
196
- def: this.def,
197
+ def: this._def,
197
198
  change: { type: "setInputs", nodeId, inputs },
198
199
  ...options,
199
200
  });
200
201
  }
201
202
  addNode(node, options) {
202
203
  const id = node.nodeId ??
203
- this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
204
- this.def.nodes.push({
204
+ this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
205
+ this._def.nodes.push({
205
206
  nodeId: id,
206
207
  typeId: node.typeId,
207
208
  params: node.params,
@@ -210,7 +211,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
210
211
  if (node.position)
211
212
  this.positions[id] = node.position;
212
213
  this.emit("graphChanged", {
213
- def: this.def,
214
+ def: this._def,
214
215
  change: {
215
216
  type: "addNode",
216
217
  nodeId: id,
@@ -223,26 +224,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
223
224
  return id;
224
225
  }
225
226
  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);
227
+ this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
228
+ this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
228
229
  delete this.positions[nodeId];
230
+ delete this.nodeNames[nodeId];
229
231
  this.emit("graphChanged", {
230
- def: this.def,
232
+ def: this._def,
231
233
  change: { type: "removeNode", nodeId },
232
234
  ...options,
233
235
  });
234
236
  this.refreshValidation();
235
237
  }
236
238
  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({
239
+ const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
240
+ this._def.edges.push({
239
241
  id,
240
242
  source: { ...edge.source },
241
243
  target: { ...edge.target },
242
244
  typeId: edge.typeId,
243
245
  });
244
246
  this.emit("graphChanged", {
245
- def: this.def,
247
+ def: this._def,
246
248
  change: { type: "connect", edgeId: id },
247
249
  ...options,
248
250
  });
@@ -250,16 +252,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
250
252
  return id;
251
253
  }
252
254
  disconnect(edgeId, options) {
253
- this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
255
+ this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
254
256
  this.emit("graphChanged", {
255
- def: this.def,
257
+ def: this._def,
256
258
  change: { type: "disconnect", edgeId },
257
259
  ...options,
258
260
  });
259
261
  this.emit("validationChanged", this.validate());
260
262
  }
261
263
  updateEdgeType(edgeId, typeId) {
262
- const e = this.def.edges.find((x) => x.id === edgeId);
264
+ const e = this._def.edges.find((x) => x.id === edgeId);
263
265
  if (!e)
264
266
  return;
265
267
  if (!typeId)
@@ -267,18 +269,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
267
269
  else
268
270
  e.typeId = typeId;
269
271
  this.emit("graphChanged", {
270
- def: this.def,
272
+ def: this._def,
271
273
  change: { type: "updateEdgeType", edgeId, typeId },
272
274
  });
273
275
  this.refreshValidation();
274
276
  }
275
277
  updateParams(nodeId, params) {
276
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
278
+ const n = this._def.nodes.find((n) => n.nodeId === nodeId);
277
279
  if (!n)
278
280
  return;
279
281
  n.params = { ...(n.params ?? {}), ...params };
280
282
  this.emit("graphChanged", {
281
- def: this.def,
283
+ def: this._def,
282
284
  change: { type: "updateParams", nodeId },
283
285
  });
284
286
  }
@@ -286,7 +288,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
286
288
  setPositions(map, options) {
287
289
  this.positions = { ...this.positions, ...map };
288
290
  this.emit("graphUiChanged", {
289
- def: this.def,
291
+ def: this._def,
290
292
  change: { type: "moveNodes" },
291
293
  ...options,
292
294
  });
@@ -294,17 +296,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
294
296
  getPositions() {
295
297
  return { ...this.positions };
296
298
  }
297
- setSelection(sel, options) {
299
+ setSelectionInternal(sel, options) {
298
300
  if (lod.isEqual(this.selection, sel))
299
301
  return;
300
302
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
301
303
  this.emit("selectionChanged", this.selection);
302
304
  this.emit("graphUiChanged", {
303
- def: this.def,
305
+ def: this._def,
304
306
  change: { type: "selection" },
305
307
  ...options,
306
308
  });
307
309
  }
310
+ setSelection(sel, options) {
311
+ this.setSelectionInternal(sel, options);
312
+ }
308
313
  getSelection() {
309
314
  return {
310
315
  nodes: [...this.selection.nodes],
@@ -325,14 +330,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
325
330
  this.disconnect(edgeId);
326
331
  }
327
332
  // Clear selection
328
- this.setSelection({ nodes: [], edges: [] }, options);
333
+ this.setSelectionInternal({ nodes: [], edges: [] }, options);
329
334
  }
330
335
  setViewport(viewport) {
331
336
  if (lod.isEqual(this.viewport, viewport))
332
337
  return;
333
338
  this.viewport = { ...viewport };
334
339
  this.emit("graphUiChanged", {
335
- def: this.def,
340
+ def: this._def,
336
341
  change: { type: "viewport" },
337
342
  });
338
343
  }
@@ -340,11 +345,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
340
345
  return this.viewport ? { ...this.viewport } : null;
341
346
  }
342
347
  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));
348
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
349
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
345
350
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
346
351
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
347
352
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
353
+ const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
348
354
  return {
349
355
  positions: Object.keys(filteredPositions).length > 0
350
356
  ? filteredPositions
@@ -356,6 +362,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
356
362
  }
357
363
  : undefined,
358
364
  viewport: this.viewport ? { ...this.viewport } : undefined,
365
+ nodeNames: Object.keys(filteredNodeNames).length > 0
366
+ ? filteredNodeNames
367
+ : undefined,
359
368
  };
360
369
  }
361
370
  setUIState(ui) {
@@ -374,12 +383,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
374
383
  if (ui.viewport) {
375
384
  this.viewport = { ...ui.viewport };
376
385
  }
386
+ if (ui.nodeNames !== undefined) {
387
+ this.nodeNames = { ...ui.nodeNames };
388
+ }
377
389
  }
378
390
  getRuntimeState() {
379
391
  return this.runtimeState ? { ...this.runtimeState } : null;
380
392
  }
381
393
  setRuntimeState(runtime) {
382
394
  this.runtimeState = runtime ? { ...runtime } : null;
395
+ this.emit("runtimeMetadataChanged", {});
383
396
  }
384
397
  getHistory() {
385
398
  return this.historyState;
@@ -396,6 +409,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
396
409
  const nodeMeta = current.nodes[nodeId] ?? {};
397
410
  const updated = updater({ ...nodeMeta });
398
411
  this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
412
+ this.emit("runtimeMetadataChanged", { nodeId });
399
413
  }
400
414
  on(event, handler) {
401
415
  if (!this.listeners.has(event))
@@ -420,11 +434,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
420
434
  const selection = this.getSelection();
421
435
  if (selection.nodes.length === 0)
422
436
  return null;
423
- const def = this.export();
424
437
  const positions = this.getPositions();
425
438
  const selectedNodeSet = new Set(selection.nodes);
426
439
  // Collect nodes to copy
427
- const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
440
+ const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
428
441
  if (nodesToCopy.length === 0)
429
442
  return null;
430
443
  // Calculate bounds
@@ -448,12 +461,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
448
461
  const centerY = (bounds.minY + bounds.maxY) / 2;
449
462
  // Get inputs for each node
450
463
  // Include values from inbound edges if those edges are selected
451
- const allInputs = runner.getInputs(def);
464
+ const allInputs = runner.getInputs(this.def);
452
465
  const selectedEdgeSet = new Set(selection.edges);
453
466
  const copiedNodes = nodesToCopy.map((node) => {
454
467
  const pos = positions[node.nodeId] || { x: 0, y: 0 };
455
468
  // Get all inbound edges for this node
456
- const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
469
+ const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
457
470
  // Build set of handles that have inbound edges
458
471
  // But only exclude handles whose edges are NOT selected
459
472
  const inboundHandlesToExclude = new Set(inboundEdges
@@ -477,7 +490,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
477
490
  };
478
491
  });
479
492
  // Collect edges between copied nodes
480
- const copiedEdges = def.edges
493
+ const copiedEdges = this.def.edges
481
494
  .filter((edge) => {
482
495
  return (selectedNodeSet.has(edge.source.nodeId) &&
483
496
  selectedNodeSet.has(edge.target.nodeId));
@@ -495,6 +508,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
495
508
  bounds,
496
509
  };
497
510
  }
511
+ /**
512
+ * Duplicate all selected nodes.
513
+ * Returns the list of newly created node IDs.
514
+ * Each duplicated node is offset by 24px in both x and y directions.
515
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
516
+ */
517
+ duplicateSelection(runner, options) {
518
+ const selection = this.getSelection();
519
+ if (selection.nodes.length === 0)
520
+ return [];
521
+ const positions = this.getPositions();
522
+ const newNodes = [];
523
+ // Get inputs without bindings (literal values only)
524
+ const allInputs = runner.getInputs(this.def) || {};
525
+ // Duplicate each selected node
526
+ for (const nodeId of selection.nodes) {
527
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
528
+ if (!n)
529
+ continue;
530
+ const pos = positions[nodeId] || { x: 0, y: 0 };
531
+ const inboundHandles = new Set(this.def.edges
532
+ .filter((e) => e.target.nodeId === nodeId)
533
+ .map((e) => e.target.handle));
534
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
535
+ const newNodeId = this.addNode({
536
+ typeId: n.typeId,
537
+ params: n.params,
538
+ position: { x: pos.x + 24, y: pos.y + 24 },
539
+ resolvedHandles: n.resolvedHandles,
540
+ }, {
541
+ inputs: inputsWithoutBindings,
542
+ copyOutputsFrom: nodeId,
543
+ dry: true,
544
+ });
545
+ newNodes.push(newNodeId);
546
+ }
547
+ // Select all newly duplicated nodes
548
+ if (newNodes.length > 0) {
549
+ this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
550
+ }
551
+ return newNodes;
552
+ }
553
+ /**
554
+ * Bake an output value from a node into a new node.
555
+ * Creates a new node based on the output type's bakeTarget configuration.
556
+ * Returns the ID of the last created node (or undefined if none created).
557
+ */
558
+ async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
559
+ try {
560
+ if (!outputTypeId || outputValue === undefined)
561
+ return undefined;
562
+ const unwrap = (v) => isTypedOutput(v) ? getTypedOutputValue(v) : v;
563
+ const coerceIfNeeded = async (fromType, toType, value) => {
564
+ if (!toType || toType === fromType || !runner?.coerce)
565
+ return value;
566
+ try {
567
+ return await runner.coerce(fromType, toType, value);
568
+ }
569
+ catch {
570
+ return value;
571
+ }
572
+ };
573
+ const pos = nodePosition;
574
+ const isArray = outputTypeId.endsWith("[]");
575
+ const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
576
+ const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
577
+ const tElem = registry.types.get(baseTypeId);
578
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
579
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
580
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
581
+ let newNodeId;
582
+ if (singleTarget) {
583
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
584
+ const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
585
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
586
+ newNodeId = this.addNode({
587
+ typeId: singleTarget.nodeTypeId,
588
+ position: { x: pos.x + 180, y: pos.y },
589
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
590
+ }
591
+ else if (isArray && arrTarget) {
592
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
593
+ const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
594
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
595
+ newNodeId = this.addNode({
596
+ typeId: arrTarget.nodeTypeId,
597
+ position: { x: pos.x + 180, y: pos.y },
598
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
599
+ }
600
+ else if (isArray && elemTarget) {
601
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
602
+ const inType = getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
603
+ const src = unwrap(outputValue);
604
+ const items = Array.isArray(src) ? src : [src];
605
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
606
+ const COLS = 4;
607
+ const DX = 180;
608
+ const DY = 160;
609
+ for (let idx = 0; idx < coercedItems.length; idx++) {
610
+ const col = idx % COLS;
611
+ const row = Math.floor(idx / COLS);
612
+ newNodeId = this.addNode({
613
+ typeId: elemTarget.nodeTypeId,
614
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
615
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
616
+ }
617
+ }
618
+ if (newNodeId) {
619
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
620
+ }
621
+ return newNodeId;
622
+ }
623
+ catch {
624
+ return undefined;
625
+ }
626
+ }
627
+ /**
628
+ * Duplicate a single node.
629
+ * Returns the ID of the newly created node.
630
+ * The duplicated node is offset by 24px in both x and y directions.
631
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
632
+ */
633
+ duplicateNode(nodeId, runner, options) {
634
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
635
+ if (!n)
636
+ return undefined;
637
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
638
+ // Get inputs without bindings (literal values only)
639
+ const allInputs = runner.getInputs(this.def)[nodeId] || {};
640
+ const inboundHandles = new Set(this.def.edges
641
+ .filter((e) => e.target.nodeId === nodeId)
642
+ .map((e) => e.target.handle));
643
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
644
+ const newNodeId = this.addNode({
645
+ typeId: n.typeId,
646
+ params: n.params,
647
+ position: { x: pos.x + 24, y: pos.y + 24 },
648
+ resolvedHandles: n.resolvedHandles,
649
+ }, {
650
+ inputs: inputsWithoutBindings,
651
+ copyOutputsFrom: nodeId,
652
+ dry: true,
653
+ });
654
+ // Select the newly duplicated node
655
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
656
+ return newNodeId;
657
+ }
658
+ /**
659
+ * Duplicate a node and all its incoming edges.
660
+ * Returns the ID of the newly created node.
661
+ * The duplicated node is offset by 24px in both x and y directions.
662
+ * All incoming edges are duplicated to point to the new node.
663
+ */
664
+ duplicateNodeWithEdges(nodeId, runner, options) {
665
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
666
+ if (!n)
667
+ return undefined;
668
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
669
+ // Get all inputs (including those with bindings, since edges will be duplicated)
670
+ const inputs = runner.getInputs(this.def)[nodeId] || {};
671
+ // Add the duplicated node
672
+ const newNodeId = this.addNode({
673
+ typeId: n.typeId,
674
+ params: n.params,
675
+ position: { x: pos.x + 24, y: pos.y + 24 },
676
+ resolvedHandles: n.resolvedHandles,
677
+ }, {
678
+ inputs,
679
+ copyOutputsFrom: nodeId,
680
+ dry: true,
681
+ });
682
+ // Find all incoming edges (edges where target is the original node)
683
+ const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
684
+ // Duplicate each incoming edge to point to the new node
685
+ for (const edge of incomingEdges) {
686
+ this.connect({
687
+ source: edge.source, // Keep the same source
688
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
689
+ typeId: edge.typeId,
690
+ }, { dry: true });
691
+ }
692
+ // Select the newly duplicated node
693
+ if (newNodeId) {
694
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
695
+ }
696
+ return newNodeId;
697
+ }
498
698
  /**
499
699
  * Paste copied graph data at the specified center position.
500
700
  * Returns the mapping from original node IDs to new node IDs.
@@ -540,7 +740,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
540
740
  }
541
741
  }
542
742
  // Select the newly pasted nodes
543
- this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
743
+ this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
544
744
  return { nodeIdMap, edgeIds };
545
745
  }
546
746
  /**
@@ -555,6 +755,29 @@ class InMemoryWorkbench extends AbstractWorkbench {
555
755
  setCopiedData(data) {
556
756
  this.copiedData = data;
557
757
  }
758
+ /**
759
+ * Get the custom name for a node, if set.
760
+ */
761
+ getNodeName(nodeId) {
762
+ return this.nodeNames[nodeId];
763
+ }
764
+ /**
765
+ * Set a custom name for a node. Empty string or undefined removes the custom name.
766
+ * This is included in undo/redo history via extData.ui.
767
+ */
768
+ setNodeName(nodeId, name, options) {
769
+ if (name === undefined || name.trim() === "") {
770
+ delete this.nodeNames[nodeId];
771
+ }
772
+ else {
773
+ this.nodeNames[nodeId] = name.trim();
774
+ }
775
+ this.emit("graphUiChanged", {
776
+ def: this._def,
777
+ change: { type: "nodeName", nodeId },
778
+ ...options,
779
+ });
780
+ }
558
781
  }
559
782
 
560
783
  class CLIWorkbench {
@@ -565,8 +788,8 @@ class CLIWorkbench {
565
788
  async load(def) {
566
789
  await this.wb.load(def);
567
790
  }
568
- print(def, options) {
569
- const d = def ?? this.wb.export();
791
+ print(options) {
792
+ const d = this.wb.def;
570
793
  const detail = !!options?.detail;
571
794
  const lines = [];
572
795
  lines.push(`Nodes (${d.nodes.length})`);
@@ -1935,8 +2158,7 @@ function useWorkbenchBridge(wb) {
1935
2158
  if (!params.sourceHandle || !params.targetHandle)
1936
2159
  return;
1937
2160
  // Prevent duplicate edges between the same endpoints
1938
- const def = wb.export();
1939
- const exists = def.edges.some((e) => e.source.nodeId === params.source &&
2161
+ const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
1940
2162
  e.source.handle === params.sourceHandle &&
1941
2163
  e.target.nodeId === params.target &&
1942
2164
  e.target.handle === params.targetHandle);
@@ -1986,7 +2208,10 @@ function useWorkbenchBridge(wb) {
1986
2208
  }
1987
2209
  }
1988
2210
  if (selectionChanged) {
1989
- wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
2211
+ wb.setSelection({
2212
+ nodes: Array.from(nextNodeIds),
2213
+ edges: current.edges,
2214
+ });
1990
2215
  }
1991
2216
  if (Object.keys(positions).length > 0) {
1992
2217
  wb.setPositions(positions, { commit });
@@ -2021,7 +2246,10 @@ function useWorkbenchBridge(wb) {
2021
2246
  }
2022
2247
  }
2023
2248
  if (selectionChanged) {
2024
- wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
2249
+ wb.setSelection({
2250
+ nodes: current.nodes,
2251
+ edges: Array.from(nextEdgeIds),
2252
+ });
2025
2253
  }
2026
2254
  }, [wb]);
2027
2255
  const onNodesDelete = useCallback((nodes) => {
@@ -2467,7 +2695,6 @@ function isSnapshotPayload(parsed) {
2467
2695
  }
2468
2696
  async function download(wb, runner) {
2469
2697
  try {
2470
- const def = wb.export();
2471
2698
  const fullUiState = wb.getUIState();
2472
2699
  const uiState = excludeViewportFromUIState(fullUiState);
2473
2700
  const runtimeState = wb.getRuntimeState();
@@ -2476,7 +2703,7 @@ async function download(wb, runner) {
2476
2703
  const fullSnapshot = await runner.snapshotFull();
2477
2704
  snapshot = {
2478
2705
  ...fullSnapshot,
2479
- def,
2706
+ def: wb.def,
2480
2707
  extData: {
2481
2708
  ...(fullSnapshot.extData || {}),
2482
2709
  ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
@@ -2485,9 +2712,9 @@ async function download(wb, runner) {
2485
2712
  };
2486
2713
  }
2487
2714
  else {
2488
- const inputs = runner.getInputs(def);
2715
+ const inputs = runner.getInputs(wb.def);
2489
2716
  snapshot = {
2490
- def,
2717
+ def: wb.def,
2491
2718
  inputs,
2492
2719
  outputs: {},
2493
2720
  environment: {},
@@ -2526,7 +2753,7 @@ async function upload(parsed, wb, runner) {
2526
2753
  }
2527
2754
  if (runner.isRunning()) {
2528
2755
  await runner.applySnapshotFull({
2529
- def,
2756
+ def: wb.def,
2530
2757
  environment,
2531
2758
  inputs,
2532
2759
  outputs,
@@ -2534,7 +2761,7 @@ async function upload(parsed, wb, runner) {
2534
2761
  });
2535
2762
  }
2536
2763
  else {
2537
- runner.build(wb.export());
2764
+ runner.build(wb.def);
2538
2765
  if (inputs && typeof inputs === "object") {
2539
2766
  for (const [nodeId, map] of Object.entries(inputs)) {
2540
2767
  runner.setInputs(nodeId, map, { dry: true });
@@ -2630,14 +2857,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2630
2857
  const versionTick = useWorkbenchVersionTick(runner);
2631
2858
  const valuesTick = versionTick + graphTick + graphUiTick;
2632
2859
  // 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]);
2860
+ const inputsMap = useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
2861
+ const inputDefaultsMap = useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
2862
+ const outputsMap = useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
2637
2863
  const outputTypesMap = useMemo(() => {
2638
2864
  const out = {};
2639
2865
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
2640
- for (const n of def.nodes) {
2866
+ for (const n of wb.def.nodes) {
2641
2867
  const effectiveHandles = computeEffectiveHandles(n, registry);
2642
2868
  const outputsDecl = effectiveHandles.outputs;
2643
2869
  const handles = Object.keys(outputsDecl);
@@ -2652,14 +2878,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2652
2878
  out[n.nodeId] = cur;
2653
2879
  }
2654
2880
  return out;
2655
- }, [def, outputsMap, registry]);
2881
+ }, [wb, wb.def, outputsMap, registry]);
2656
2882
  // Initialize nodes and derive invalidated status from persisted metadata
2657
2883
  useEffect(() => {
2658
2884
  const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
2659
2885
  setNodeStatus((prev) => {
2660
2886
  const next = { ...prev };
2661
2887
  const metadata = workbenchRuntimeState;
2662
- for (const n of def.nodes) {
2888
+ for (const n of wb.def.nodes) {
2663
2889
  const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
2664
2890
  const nodeMeta = metadata.nodes[n.nodeId];
2665
2891
  const updates = {};
@@ -2681,18 +2907,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2681
2907
  }
2682
2908
  return next;
2683
2909
  });
2684
- }, [def, wb]);
2910
+ }, [wb.def, wb]);
2685
2911
  // Auto layout (simple layered layout)
2686
2912
  const runAutoLayout = useCallback(() => {
2687
- const cur = wb.export();
2688
2913
  // Build DAG layers by indegree
2689
2914
  const indegree = {};
2690
2915
  const adj = {};
2691
- for (const n of cur.nodes) {
2916
+ for (const n of wb.def.nodes) {
2692
2917
  indegree[n.nodeId] = 0;
2693
2918
  adj[n.nodeId] = [];
2694
2919
  }
2695
- for (const e of cur.edges) {
2920
+ for (const e of wb.def.edges) {
2696
2921
  indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
2697
2922
  adj[e.source.nodeId].push(e.target.nodeId);
2698
2923
  }
@@ -2723,7 +2948,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2723
2948
  let maxWidth = 0;
2724
2949
  const heights = {};
2725
2950
  for (const id of layer) {
2726
- const node = cur.nodes.find((n) => n.nodeId === id);
2951
+ const node = wb.def.nodes.find((n) => n.nodeId === id);
2727
2952
  if (!node)
2728
2953
  continue;
2729
2954
  // Prefer showValues sizing similar to node rendering
@@ -2749,26 +2974,39 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2749
2974
  curX += maxWidth + H_GAP;
2750
2975
  }
2751
2976
  wb.setPositions(pos, { commit: true, reason: "auto-layout" });
2752
- }, [wb, registry, overrides?.getDefaultNodeSize]);
2977
+ }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2753
2978
  const updateEdgeType = useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2754
2979
  const triggerExternal = useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2980
+ const getNodeDisplayName = useCallback((nodeId) => {
2981
+ const customName = wb.getNodeName(nodeId);
2982
+ if (customName)
2983
+ return customName;
2984
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
2985
+ if (!node)
2986
+ return nodeId;
2987
+ const desc = registry.nodes.get(node.typeId);
2988
+ return desc?.displayName || node.typeId;
2989
+ }, [wb, registry]);
2990
+ const setNodeName = useCallback((nodeId, name) => {
2991
+ wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
2992
+ }, [wb]);
2755
2993
  // Helper to save runtime metadata and UI state to extData
2756
- const saveUiRuntimeMetadata = useCallback(async () => {
2994
+ const saveUiRuntimeMetadata = useCallback(async (workbench, graphRunner) => {
2757
2995
  try {
2758
- const current = wb.getRuntimeState() ?? { nodes: {} };
2996
+ const current = workbench.getRuntimeState() ?? { nodes: {} };
2759
2997
  const metadata = { nodes: { ...current.nodes } };
2760
2998
  // Clean up metadata for nodes that no longer exist
2761
- const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
2999
+ const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
2762
3000
  for (const nodeId of Object.keys(metadata.nodes)) {
2763
3001
  if (!nodeIds.has(nodeId)) {
2764
3002
  delete metadata.nodes[nodeId];
2765
3003
  }
2766
3004
  }
2767
3005
  // Save cleaned metadata to workbench state
2768
- wb.setRuntimeState(metadata);
2769
- const fullUiState = wb.getUIState();
3006
+ workbench.setRuntimeState(metadata);
3007
+ const fullUiState = workbench.getUIState();
2770
3008
  const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
2771
- await runner.setExtData?.({
3009
+ await graphRunner.setExtData?.({
2772
3010
  ...(Object.keys(uiWithoutViewport || {}).length > 0
2773
3011
  ? { ui: uiWithoutViewport }
2774
3012
  : {}),
@@ -2778,7 +3016,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2778
3016
  catch (err) {
2779
3017
  console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
2780
3018
  }
2781
- }, [wb, def, runner]);
3019
+ }, []);
2782
3020
  // Subscribe to runner/workbench events
2783
3021
  useEffect(() => {
2784
3022
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -2804,9 +3042,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2804
3042
  });
2805
3043
  // Helper to apply resolved handles from event payload to workbench
2806
3044
  const applyResolvedHandles = (resolvedHandles) => {
2807
- const cur = wb.export();
2808
3045
  let changed = false;
2809
- for (const n of cur.nodes) {
3046
+ for (const n of wb.def.nodes) {
2810
3047
  const updated = resolvedHandles[n.nodeId];
2811
3048
  if (updated) {
2812
3049
  const before = JSON.stringify(n.resolvedHandles || {});
@@ -3095,6 +3332,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3095
3332
  else if (changeType === "updateEdgeType") {
3096
3333
  reason = "update-edge-type";
3097
3334
  }
3335
+ else if (changeType === "setInputs") {
3336
+ reason = "set-inputs";
3337
+ }
3098
3338
  }
3099
3339
  if (event.change?.type === "setInputs") {
3100
3340
  const { nodeId, inputs } = event.change;
@@ -3102,7 +3342,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3102
3342
  }
3103
3343
  if (!runner.isRunning()) {
3104
3344
  if (event.commit) {
3105
- await saveUiRuntimeMetadata();
3345
+ await saveUiRuntimeMetadata(wb, runner);
3106
3346
  const history = await runner.commit(reason).catch((err) => {
3107
3347
  console.error("[WorkbenchContext] Error committing:", err);
3108
3348
  return undefined;
@@ -3135,7 +3375,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3135
3375
  await runner.update(event.def, { dry: event.dry });
3136
3376
  }
3137
3377
  if (event.commit) {
3138
- await saveUiRuntimeMetadata();
3378
+ await saveUiRuntimeMetadata(wb, runner);
3139
3379
  const history = await runner
3140
3380
  .commit(event.reason ?? reason)
3141
3381
  .catch((err) => {
@@ -3156,7 +3396,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3156
3396
  setSelectedNodeId(sel.nodes?.[0]);
3157
3397
  setSelectedEdgeId(sel.edges?.[0]);
3158
3398
  if (sel.commit) {
3159
- await saveUiRuntimeMetadata();
3399
+ await saveUiRuntimeMetadata(wb, runner);
3160
3400
  const history = await runner
3161
3401
  .commit(sel.reason ?? "selection")
3162
3402
  .catch((err) => {
@@ -3195,7 +3435,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3195
3435
  reason = "selection";
3196
3436
  }
3197
3437
  }
3198
- await saveUiRuntimeMetadata();
3438
+ await saveUiRuntimeMetadata(wb, runner);
3199
3439
  const history = await runner
3200
3440
  .commit(event.reason ?? reason)
3201
3441
  .catch((err) => {
@@ -3215,7 +3455,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3215
3455
  wb.setRegistry(newReg);
3216
3456
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
3217
3457
  try {
3218
- await runner.update(wb.export());
3458
+ await runner.update(wb.def);
3219
3459
  }
3220
3460
  catch {
3221
3461
  console.error("Failed to update graph definition after registry changed");
@@ -3238,7 +3478,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3238
3478
  setNodeStatus(() => {
3239
3479
  const next = {};
3240
3480
  const metadata = wb.getRuntimeState() ?? { nodes: {} };
3241
- for (const n of def.nodes) {
3481
+ for (const n of wb.def.nodes) {
3242
3482
  const nodeMeta = metadata.nodes[n.nodeId];
3243
3483
  next[n.nodeId] = {
3244
3484
  activeRuns: 0,
@@ -3253,6 +3493,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3253
3493
  errorRunsRef.current = {};
3254
3494
  }
3255
3495
  });
3496
+ const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
3497
+ const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
3498
+ setNodeStatus((prev) => {
3499
+ const next = { ...prev };
3500
+ const metadata = workbenchRuntimeState;
3501
+ let changed = false;
3502
+ // If nodeId is specified, only update that node; otherwise update all nodes
3503
+ const nodesToUpdate = event.nodeId
3504
+ ? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
3505
+ : wb.def.nodes;
3506
+ for (const n of nodesToUpdate) {
3507
+ const cur = next[n.nodeId];
3508
+ if (!cur)
3509
+ continue;
3510
+ const nodeMeta = metadata.nodes[n.nodeId];
3511
+ const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
3512
+ if (cur.invalidated !== newInvalidated) {
3513
+ next[n.nodeId] = { ...cur, invalidated: newInvalidated };
3514
+ changed = true;
3515
+ }
3516
+ }
3517
+ return changed ? next : prev;
3518
+ });
3519
+ });
3256
3520
  wb.refreshValidation();
3257
3521
  return () => {
3258
3522
  offRunnerValue();
@@ -3270,13 +3534,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3270
3534
  offRunnerRegistry();
3271
3535
  offRunnerTransport();
3272
3536
  offFlowViewport();
3537
+ offWbRuntimeMetadataChanged();
3273
3538
  };
3274
3539
  }, [runner, wb, setRegistry]);
3275
3540
  const isRunning = useCallback(() => runner.isRunning(), [runner]);
3276
3541
  const engineKind = useCallback(() => runner.getRunningEngine(), [runner]);
3277
3542
  const start = useCallback((engine) => {
3278
3543
  try {
3279
- runner.launch(wb.export(), { engine });
3544
+ runner.launch(wb.def, { engine });
3280
3545
  }
3281
3546
  catch { }
3282
3547
  }, [runner, wb]);
@@ -3351,7 +3616,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3351
3616
  runner,
3352
3617
  registry,
3353
3618
  setRegistry,
3354
- def,
3355
3619
  selectedNodeId,
3356
3620
  selectedEdgeId,
3357
3621
  setSelection,
@@ -3387,12 +3651,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3387
3651
  triggerExternal,
3388
3652
  uiVersion,
3389
3653
  overrides,
3654
+ getNodeDisplayName,
3655
+ setNodeName,
3390
3656
  }), [
3391
3657
  wb,
3392
3658
  runner,
3393
3659
  registry,
3394
3660
  setRegistry,
3395
- def,
3396
3661
  selectedNodeId,
3397
3662
  selectedEdgeId,
3398
3663
  setSelection,
@@ -3427,142 +3692,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3427
3692
  runner,
3428
3693
  uiVersion,
3429
3694
  overrides,
3695
+ getNodeDisplayName,
3696
+ setNodeName,
3430
3697
  ]);
3431
3698
  return (jsx(WorkbenchContext.Provider, { value: value, children: children }));
3432
3699
  }
3433
3700
 
3434
3701
  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
3702
  return {
3504
3703
  onDelete: () => {
3505
3704
  wb.removeNode(nodeId, { commit: true });
3506
3705
  onClose();
3507
3706
  },
3508
3707
  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,
3708
+ wb.duplicateNode(nodeId, runner, {
3709
+ commit: true,
3710
+ reason: "duplicate-node",
3528
3711
  });
3529
- // Select the newly duplicated node
3530
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
3531
3712
  onClose();
3532
3713
  },
3533
3714
  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,
3715
+ wb.duplicateNodeWithEdges(nodeId, runner, {
3716
+ commit: true,
3717
+ reason: "duplicate-node-with-edges",
3551
3718
  });
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
3719
  onClose();
3567
3720
  },
3568
3721
  onRunPull: async () => {
@@ -3573,7 +3726,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3573
3726
  onClose();
3574
3727
  },
3575
3728
  onBake: async (handleId) => {
3576
- await doBake(handleId);
3729
+ const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3730
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3731
+ const raw = outputsMap?.[nodeId]?.[handleId];
3732
+ await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
3733
+ commit: true,
3734
+ reason: "bake",
3735
+ });
3577
3736
  onClose();
3578
3737
  },
3579
3738
  onCopy: () => {
@@ -3630,42 +3789,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
3630
3789
  function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3631
3790
  const onDuplicate = runner
3632
3791
  ? () => {
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
- }
3792
+ wb.duplicateSelection(runner, {
3793
+ commit: true,
3794
+ reason: "duplicate-selection",
3795
+ });
3669
3796
  onClose();
3670
3797
  }
3671
3798
  : undefined;
@@ -3700,9 +3827,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3700
3827
  const canRedo = history ? history.redoCount > 0 : undefined;
3701
3828
  const onSelectAll = wb
3702
3829
  ? () => {
3703
- const def = wb.export();
3704
- const allNodeIds = def.nodes.map((n) => n.nodeId);
3705
- const allEdgeIds = def.edges.map((e) => e.id);
3830
+ const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
3831
+ const allEdgeIds = wb.def.edges.map((e) => e.id);
3706
3832
  wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
3707
3833
  onClose();
3708
3834
  }
@@ -3721,8 +3847,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3721
3847
  }
3722
3848
  function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3723
3849
  try {
3724
- const def = wb.export();
3725
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3850
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
3726
3851
  if (!node)
3727
3852
  return [];
3728
3853
  const desc = registry.nodes.get(node.typeId);
@@ -3821,13 +3946,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3821
3946
  return String(value ?? "");
3822
3947
  }
3823
3948
  };
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();
3949
+ 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
3950
  const nodeValidationIssues = validationByNode.issues;
3826
3951
  const edgeValidationIssues = validationByEdge.issues;
3827
3952
  const nodeValidationHandles = validationByNode;
3828
3953
  const globalValidationIssues = validationGlobal;
3829
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3830
- const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
3954
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
3955
+ const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
3831
3956
  // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
3832
3957
  const effectiveHandles = selectedNode
3833
3958
  ? computeEffectiveHandles(selectedNode, registry)
@@ -3959,7 +4084,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3959
4084
  setOriginals(nextOriginals);
3960
4085
  }, [selectedNodeId, selectedNode, registry, valuesTick]);
3961
4086
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
3962
- const { wb } = useWorkbenchContext();
3963
4087
  const deleteEdgeById = (edgeId) => {
3964
4088
  if (!edgeId)
3965
4089
  return;
@@ -3986,9 +4110,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3986
4110
  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
4111
  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
4112
  const typeId = getInputTypeId(effectiveHandles.inputs, h);
3989
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
4113
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
3990
4114
  e.target.handle === h);
3991
- const inbound = new Set(def.edges
4115
+ const inbound = new Set(wb.def.edges
3992
4116
  .filter((e) => e.target.nodeId === selectedNodeId &&
3993
4117
  e.target.handle === h)
3994
4118
  .map((e) => e.target.handle));
@@ -4158,11 +4282,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
4158
4282
  position: "relative",
4159
4283
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
4160
4284
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
4161
- }, children: [jsx(DefaultNodeHeader, { id: id, title: typeId, validation: validation, showId: data.showValues }), jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4285
+ }, children: [jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4162
4286
  });
4163
4287
  DefaultNode.displayName = "DefaultNode";
4164
- function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
4288
+ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4165
4289
  const ctx = useWorkbenchContext();
4290
+ const [isEditing, setIsEditing] = React.useState(false);
4291
+ const [editValue, setEditValue] = React.useState("");
4292
+ const inputRef = React.useRef(null);
4293
+ // Use getNodeDisplayName if typeId is provided, otherwise use title prop
4294
+ const displayName = typeId ? ctx.getNodeDisplayName(id) : (title ?? id);
4295
+ const effectiveTypeId = typeId ?? title ?? id;
4296
+ // Get the default display name (without custom name) for comparison
4297
+ const getDefaultDisplayName = React.useCallback(() => {
4298
+ if (!typeId)
4299
+ return title ?? id;
4300
+ const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
4301
+ if (!node)
4302
+ return id;
4303
+ const desc = ctx.registry.nodes.get(node.typeId);
4304
+ return desc?.displayName || node.typeId;
4305
+ }, [ctx, id, typeId, title]);
4166
4306
  const handleInvalidate = React.useCallback(() => {
4167
4307
  try {
4168
4308
  if (onInvalidate)
@@ -4175,10 +4315,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
4175
4315
  }
4176
4316
  catch { }
4177
4317
  }, [ctx, id, onInvalidate]);
4318
+ const handleDoubleClick = React.useCallback((e) => {
4319
+ // Only allow editing if typeId is provided (enables renaming)
4320
+ if (!typeId)
4321
+ return;
4322
+ e.stopPropagation();
4323
+ setIsEditing(true);
4324
+ setEditValue(displayName);
4325
+ }, [typeId, displayName]);
4326
+ const handleSave = React.useCallback(() => {
4327
+ if (!typeId)
4328
+ return;
4329
+ const trimmed = editValue.trim();
4330
+ const defaultDisplayName = getDefaultDisplayName();
4331
+ // If the trimmed value matches the default display name or typeId, clear the custom name
4332
+ ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
4333
+ ? undefined
4334
+ : trimmed);
4335
+ setIsEditing(false);
4336
+ }, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
4337
+ const handleCancel = React.useCallback(() => {
4338
+ setIsEditing(false);
4339
+ setEditValue(displayName);
4340
+ }, [displayName]);
4341
+ const handleKeyDown = React.useCallback((e) => {
4342
+ if (e.key === "Enter") {
4343
+ e.preventDefault();
4344
+ e.stopPropagation();
4345
+ handleSave();
4346
+ }
4347
+ else if (e.key === "Escape") {
4348
+ e.preventDefault();
4349
+ e.stopPropagation();
4350
+ handleCancel();
4351
+ }
4352
+ }, [handleSave, handleCancel]);
4353
+ React.useEffect(() => {
4354
+ if (isEditing && inputRef.current) {
4355
+ inputRef.current.focus();
4356
+ inputRef.current.select();
4357
+ }
4358
+ }, [isEditing]);
4178
4359
  return (jsxs("div", { className: "flex items-center justify-center px-2 border-b border-solid border-gray-500 dark:border-gray-400 text-gray-600 dark:text-gray-300", style: {
4179
4360
  maxHeight: NODE_HEADER_HEIGHT_PX,
4180
4361
  minHeight: NODE_HEADER_HEIGHT_PX,
4181
- }, children: [jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: title }), jsxs("div", { className: "flex items-center gap-1", children: [jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4362
+ }, children: [isEditing ? (jsx("input", { ref: inputRef, type: "text", value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: handleSave, onKeyDown: handleKeyDown, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), className: "flex-1 h-full text-sm bg-transparent border border-blue-500 rounded px-1 outline-none wb-nodrag", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` } })) : (jsx("strong", { className: `flex-1 h-full text-sm select-none truncate ${typeId ? "cursor-text" : ""}`, style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, onDoubleClick: handleDoubleClick, title: typeId ? "Double-click to rename" : undefined, children: displayName })), jsxs("div", { className: "flex items-center gap-1", children: [jsx("button", { className: "w-4 h-4 border border-gray-400 rounded text-[10px] leading-3 flex items-center justify-center", title: "Invalidate and re-run", onClick: (e) => {
4182
4363
  e.stopPropagation();
4183
4364
  handleInvalidate();
4184
4365
  }, children: jsx(ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
@@ -4564,10 +4745,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4564
4745
  const { nodeTypes, resolveNodeType } = useMemo(() => {
4565
4746
  // Build nodeTypes map using UI extension registry
4566
4747
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
4567
- const def = wb.export();
4568
4748
  const ids = new Set([
4569
4749
  ...Array.from(registry.nodes.keys()),
4570
- ...def.nodes.map((n) => n.typeId),
4750
+ ...wb.def.nodes.map((n) => n.typeId),
4571
4751
  ]);
4572
4752
  for (const typeId of ids) {
4573
4753
  const renderer = ui.getNodeRenderer(typeId);
@@ -4586,14 +4766,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4586
4766
  // Include uiVersion to recompute when custom renderers are registered
4587
4767
  }, [wb, registry, uiVersion, ui]);
4588
4768
  const { nodes, edges } = useMemo(() => {
4589
- const def = wb.export();
4590
4769
  const sel = wb.getSelection();
4591
4770
  // Merge defaults with inputs for node display (defaults shown in lighter gray)
4592
4771
  const inputsWithDefaults = {};
4593
- for (const n of def.nodes) {
4772
+ for (const n of wb.def.nodes) {
4594
4773
  const nodeInputs = inputsMap[n.nodeId] ?? {};
4595
4774
  const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
4596
- const inbound = new Set(def.edges
4775
+ const inbound = new Set(wb.def.edges
4597
4776
  .filter((e) => e.target.nodeId === n.nodeId)
4598
4777
  .map((e) => e.target.handle));
4599
4778
  const merged = { ...nodeInputs };
@@ -4606,7 +4785,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4606
4785
  inputsWithDefaults[n.nodeId] = merged;
4607
4786
  }
4608
4787
  }
4609
- const out = toReactFlow(def, wb.getPositions(), registry, {
4788
+ const out = toReactFlow(wb.def, wb.getPositions(), registry, {
4610
4789
  showValues,
4611
4790
  inputs: inputsWithDefaults,
4612
4791
  inputDefaults: inputDefaultsMap,
@@ -5130,11 +5309,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5130
5309
  });
5131
5310
 
5132
5311
  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();
5312
+ const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5134
5313
  const [transportStatus, setTransportStatus] = useState({
5135
5314
  state: "local",
5136
5315
  });
5137
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
5316
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
5138
5317
  const effectiveHandles = selectedNode
5139
5318
  ? computeEffectiveHandles(selectedNode, registry)
5140
5319
  : { inputs: {}, outputs: {}, inputDefaults: {} };
@@ -5162,7 +5341,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5162
5341
  if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
5163
5342
  return;
5164
5343
  try {
5165
- runner.launch(wb.export(), {
5344
+ runner.launch(wb.def, {
5166
5345
  engine: kind,
5167
5346
  invalidate: evt.shiftKey,
5168
5347
  });
@@ -5230,7 +5409,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5230
5409
  const setInitialGraph = async (d, inputs) => {
5231
5410
  await wb.load(d);
5232
5411
  try {
5233
- runner.build(wb.export());
5412
+ runner.build(wb.def);
5234
5413
  }
5235
5414
  catch { }
5236
5415
  if (inputs) {
@@ -5245,36 +5424,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5245
5424
  useEffect(() => {
5246
5425
  if (!onChange)
5247
5426
  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", () => {
5427
+ const offGraphChanged = wb.on("graphChanged", () => {
5257
5428
  try {
5258
- const cur = wb.export();
5429
+ const cur = wb.def;
5259
5430
  const inputs = runner.getInputs(cur);
5260
5431
  onChange({ def: cur, inputs });
5261
5432
  }
5262
5433
  catch { }
5263
5434
  });
5264
- const off3 = wb.on("graphUiChanged", (evt) => {
5435
+ const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
5265
5436
  if (!evt.commit)
5266
5437
  return;
5267
5438
  try {
5268
- const cur = wb.export();
5439
+ const cur = wb.def;
5269
5440
  const inputs = runner.getInputs(cur);
5270
5441
  onChange({ def: cur, inputs });
5271
5442
  }
5272
5443
  catch { }
5273
5444
  });
5274
5445
  return () => {
5275
- off1();
5276
- off2();
5277
- off3();
5446
+ offGraphChanged();
5447
+ offGraphUiChanged();
5278
5448
  };
5279
5449
  }, [wb, runner, onChange]);
5280
5450
  const applyExample = useCallback(async (key) => {
@@ -5297,7 +5467,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5297
5467
  }
5298
5468
  await wb.load(def);
5299
5469
  // Build a local runtime so seeded defaults are visible pre-run
5300
- runner.build(wb.export());
5470
+ runner.build(wb.def);
5301
5471
  // Set initial inputs if provided
5302
5472
  if (inputs) {
5303
5473
  for (const [nodeId, map] of Object.entries(inputs)) {
@@ -5382,11 +5552,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5382
5552
  // Only auto-launch for local backend; require explicit Start for remote
5383
5553
  if (backendKind !== "local")
5384
5554
  return;
5385
- const d = wb.export();
5386
- if (!d.nodes || d.nodes.length === 0)
5555
+ if (!wb.def.nodes || wb.def.nodes.length === 0)
5387
5556
  return;
5388
5557
  try {
5389
- runner.launch(d, { engine: engine });
5558
+ runner.launch(wb.def, { engine: engine });
5390
5559
  }
5391
5560
  catch {
5392
5561
  // ignore
@@ -5396,7 +5565,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5396
5565
  if (!selectedNodeId)
5397
5566
  return;
5398
5567
  // 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);
5568
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
5400
5569
  if (isLinked)
5401
5570
  return;
5402
5571
  // If raw is undefined, pass it through to delete the input value
@@ -5481,7 +5650,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5481
5650
  }
5482
5651
  }
5483
5652
  wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
5484
- }, [selectedNodeId, def.edges, effectiveHandles, wb]);
5653
+ }, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
5485
5654
  const setInput = useMemo(() => {
5486
5655
  if (overrides?.setInput) {
5487
5656
  return overrides.setInput(baseSetInput, {