@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/cjs/index.cjs CHANGED
@@ -123,52 +123,53 @@ class AbstractWorkbench {
123
123
  class InMemoryWorkbench extends AbstractWorkbench {
124
124
  constructor() {
125
125
  super(...arguments);
126
- this.def = { nodes: [], edges: [] };
126
+ this._def = { nodes: [], edges: [] };
127
127
  this.listeners = new Map();
128
128
  this.positions = {};
129
129
  this.selection = {
130
130
  nodes: [],
131
131
  edges: [],
132
132
  };
133
- this.viewport = null;
133
+ this.nodeNames = {};
134
134
  this.runtimeState = null;
135
+ this.viewport = null;
135
136
  this.historyState = undefined;
136
137
  this.copiedData = null;
137
138
  }
139
+ get def() {
140
+ return this._def;
141
+ }
138
142
  setRegistry(registry) {
139
143
  this.registry = registry;
140
144
  }
141
145
  async load(def) {
142
- this.def = { nodes: [...def.nodes], edges: [...def.edges] };
146
+ this._def = { nodes: [...def.nodes], edges: [...def.edges] };
143
147
  if (this.layout) {
144
- const { positions } = await this.layout.layout(this.def);
148
+ const { positions } = await this.layout.layout(this._def);
145
149
  this.positions = positions;
146
150
  }
147
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
148
- const defEdgeIds = new Set(this.def.edges.map((e) => e.id));
151
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
152
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
149
153
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
150
154
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
151
155
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
152
156
  this.positions = filteredPositions;
153
157
  this.selection = { nodes: filteredNodes, edges: filteredEdges };
154
- this.emit("graphChanged", { def: this.def });
158
+ this.emit("graphChanged", { def: this._def });
155
159
  this.refreshValidation();
156
160
  }
157
- export() {
158
- return this.def;
159
- }
160
161
  refreshValidation() {
161
162
  this.emit("validationChanged", this.validate());
162
163
  }
163
164
  validate() {
164
165
  if (this.registry) {
165
166
  const builder = new sparkGraph.GraphBuilder(this.registry);
166
- const report = builder.validate(this.def);
167
+ const report = builder.validate(this._def);
167
168
  return report;
168
169
  }
169
170
  const issues = [];
170
171
  const nodeIds = new Set();
171
- for (const n of this.def.nodes) {
172
+ for (const n of this._def.nodes) {
172
173
  if (nodeIds.has(n.nodeId)) {
173
174
  issues.push({
174
175
  level: "error",
@@ -180,7 +181,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
180
181
  nodeIds.add(n.nodeId);
181
182
  }
182
183
  const edgeIds = new Set();
183
- for (const e of this.def.edges) {
184
+ for (const e of this._def.edges) {
184
185
  if (edgeIds.has(e.id)) {
185
186
  issues.push({
186
187
  level: "error",
@@ -195,15 +196,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
195
196
  }
196
197
  setInputs(nodeId, inputs, options) {
197
198
  this.emit("graphChanged", {
198
- def: this.def,
199
+ def: this._def,
199
200
  change: { type: "setInputs", nodeId, inputs },
200
201
  ...options,
201
202
  });
202
203
  }
203
204
  addNode(node, options) {
204
205
  const id = node.nodeId ??
205
- this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
206
- this.def.nodes.push({
206
+ this.genId("n", new Set(this._def.nodes.map((n) => n.nodeId)));
207
+ this._def.nodes.push({
207
208
  nodeId: id,
208
209
  typeId: node.typeId,
209
210
  params: node.params,
@@ -212,7 +213,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
212
213
  if (node.position)
213
214
  this.positions[id] = node.position;
214
215
  this.emit("graphChanged", {
215
- def: this.def,
216
+ def: this._def,
216
217
  change: {
217
218
  type: "addNode",
218
219
  nodeId: id,
@@ -225,26 +226,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
225
226
  return id;
226
227
  }
227
228
  removeNode(nodeId, options) {
228
- this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
229
- this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
229
+ this._def.nodes = this._def.nodes.filter((n) => n.nodeId !== nodeId);
230
+ this._def.edges = this._def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
230
231
  delete this.positions[nodeId];
232
+ delete this.nodeNames[nodeId];
231
233
  this.emit("graphChanged", {
232
- def: this.def,
234
+ def: this._def,
233
235
  change: { type: "removeNode", nodeId },
234
236
  ...options,
235
237
  });
236
238
  this.refreshValidation();
237
239
  }
238
240
  connect(edge, options) {
239
- const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
240
- this.def.edges.push({
241
+ const id = edge.id ?? this.genId("e", new Set(this._def.edges.map((e) => e.id)));
242
+ this._def.edges.push({
241
243
  id,
242
244
  source: { ...edge.source },
243
245
  target: { ...edge.target },
244
246
  typeId: edge.typeId,
245
247
  });
246
248
  this.emit("graphChanged", {
247
- def: this.def,
249
+ def: this._def,
248
250
  change: { type: "connect", edgeId: id },
249
251
  ...options,
250
252
  });
@@ -252,16 +254,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
252
254
  return id;
253
255
  }
254
256
  disconnect(edgeId, options) {
255
- this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
257
+ this._def.edges = this._def.edges.filter((e) => e.id !== edgeId);
256
258
  this.emit("graphChanged", {
257
- def: this.def,
259
+ def: this._def,
258
260
  change: { type: "disconnect", edgeId },
259
261
  ...options,
260
262
  });
261
263
  this.emit("validationChanged", this.validate());
262
264
  }
263
265
  updateEdgeType(edgeId, typeId) {
264
- const e = this.def.edges.find((x) => x.id === edgeId);
266
+ const e = this._def.edges.find((x) => x.id === edgeId);
265
267
  if (!e)
266
268
  return;
267
269
  if (!typeId)
@@ -269,18 +271,18 @@ class InMemoryWorkbench extends AbstractWorkbench {
269
271
  else
270
272
  e.typeId = typeId;
271
273
  this.emit("graphChanged", {
272
- def: this.def,
274
+ def: this._def,
273
275
  change: { type: "updateEdgeType", edgeId, typeId },
274
276
  });
275
277
  this.refreshValidation();
276
278
  }
277
279
  updateParams(nodeId, params) {
278
- const n = this.def.nodes.find((n) => n.nodeId === nodeId);
280
+ const n = this._def.nodes.find((n) => n.nodeId === nodeId);
279
281
  if (!n)
280
282
  return;
281
283
  n.params = { ...(n.params ?? {}), ...params };
282
284
  this.emit("graphChanged", {
283
- def: this.def,
285
+ def: this._def,
284
286
  change: { type: "updateParams", nodeId },
285
287
  });
286
288
  }
@@ -288,7 +290,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
288
290
  setPositions(map, options) {
289
291
  this.positions = { ...this.positions, ...map };
290
292
  this.emit("graphUiChanged", {
291
- def: this.def,
293
+ def: this._def,
292
294
  change: { type: "moveNodes" },
293
295
  ...options,
294
296
  });
@@ -296,17 +298,20 @@ class InMemoryWorkbench extends AbstractWorkbench {
296
298
  getPositions() {
297
299
  return { ...this.positions };
298
300
  }
299
- setSelection(sel, options) {
301
+ setSelectionInternal(sel, options) {
300
302
  if (lod.isEqual(this.selection, sel))
301
303
  return;
302
304
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
303
305
  this.emit("selectionChanged", this.selection);
304
306
  this.emit("graphUiChanged", {
305
- def: this.def,
307
+ def: this._def,
306
308
  change: { type: "selection" },
307
309
  ...options,
308
310
  });
309
311
  }
312
+ setSelection(sel, options) {
313
+ this.setSelectionInternal(sel, options);
314
+ }
310
315
  getSelection() {
311
316
  return {
312
317
  nodes: [...this.selection.nodes],
@@ -327,14 +332,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
327
332
  this.disconnect(edgeId);
328
333
  }
329
334
  // Clear selection
330
- this.setSelection({ nodes: [], edges: [] }, options);
335
+ this.setSelectionInternal({ nodes: [], edges: [] }, options);
331
336
  }
332
337
  setViewport(viewport) {
333
338
  if (lod.isEqual(this.viewport, viewport))
334
339
  return;
335
340
  this.viewport = { ...viewport };
336
341
  this.emit("graphUiChanged", {
337
- def: this.def,
342
+ def: this._def,
338
343
  change: { type: "viewport" },
339
344
  });
340
345
  }
@@ -342,11 +347,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
342
347
  return this.viewport ? { ...this.viewport } : null;
343
348
  }
344
349
  getUIState() {
345
- const defNodeIds = new Set(this.def.nodes.map((n) => n.nodeId));
346
- const defEdgeIds = new Set(this.def.edges.map((e) => e.id));
350
+ const defNodeIds = new Set(this._def.nodes.map((n) => n.nodeId));
351
+ const defEdgeIds = new Set(this._def.edges.map((e) => e.id));
347
352
  const filteredPositions = Object.fromEntries(Object.entries(this.positions).filter(([id]) => defNodeIds.has(id)));
348
353
  const filteredNodes = this.selection.nodes.filter((id) => defNodeIds.has(id));
349
354
  const filteredEdges = this.selection.edges.filter((id) => defEdgeIds.has(id));
355
+ const filteredNodeNames = Object.fromEntries(Object.entries(this.nodeNames).filter(([id]) => defNodeIds.has(id)));
350
356
  return {
351
357
  positions: Object.keys(filteredPositions).length > 0
352
358
  ? filteredPositions
@@ -358,6 +364,9 @@ class InMemoryWorkbench extends AbstractWorkbench {
358
364
  }
359
365
  : undefined,
360
366
  viewport: this.viewport ? { ...this.viewport } : undefined,
367
+ nodeNames: Object.keys(filteredNodeNames).length > 0
368
+ ? filteredNodeNames
369
+ : undefined,
361
370
  };
362
371
  }
363
372
  setUIState(ui) {
@@ -376,12 +385,16 @@ class InMemoryWorkbench extends AbstractWorkbench {
376
385
  if (ui.viewport) {
377
386
  this.viewport = { ...ui.viewport };
378
387
  }
388
+ if (ui.nodeNames !== undefined) {
389
+ this.nodeNames = { ...ui.nodeNames };
390
+ }
379
391
  }
380
392
  getRuntimeState() {
381
393
  return this.runtimeState ? { ...this.runtimeState } : null;
382
394
  }
383
395
  setRuntimeState(runtime) {
384
396
  this.runtimeState = runtime ? { ...runtime } : null;
397
+ this.emit("runtimeMetadataChanged", {});
385
398
  }
386
399
  getHistory() {
387
400
  return this.historyState;
@@ -398,6 +411,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
398
411
  const nodeMeta = current.nodes[nodeId] ?? {};
399
412
  const updated = updater({ ...nodeMeta });
400
413
  this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
414
+ this.emit("runtimeMetadataChanged", { nodeId });
401
415
  }
402
416
  on(event, handler) {
403
417
  if (!this.listeners.has(event))
@@ -422,11 +436,10 @@ class InMemoryWorkbench extends AbstractWorkbench {
422
436
  const selection = this.getSelection();
423
437
  if (selection.nodes.length === 0)
424
438
  return null;
425
- const def = this.export();
426
439
  const positions = this.getPositions();
427
440
  const selectedNodeSet = new Set(selection.nodes);
428
441
  // Collect nodes to copy
429
- const nodesToCopy = def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
442
+ const nodesToCopy = this.def.nodes.filter((n) => selectedNodeSet.has(n.nodeId));
430
443
  if (nodesToCopy.length === 0)
431
444
  return null;
432
445
  // Calculate bounds
@@ -450,12 +463,12 @@ class InMemoryWorkbench extends AbstractWorkbench {
450
463
  const centerY = (bounds.minY + bounds.maxY) / 2;
451
464
  // Get inputs for each node
452
465
  // Include values from inbound edges if those edges are selected
453
- const allInputs = runner.getInputs(def);
466
+ const allInputs = runner.getInputs(this.def);
454
467
  const selectedEdgeSet = new Set(selection.edges);
455
468
  const copiedNodes = nodesToCopy.map((node) => {
456
469
  const pos = positions[node.nodeId] || { x: 0, y: 0 };
457
470
  // Get all inbound edges for this node
458
- const inboundEdges = def.edges.filter((e) => e.target.nodeId === node.nodeId);
471
+ const inboundEdges = this.def.edges.filter((e) => e.target.nodeId === node.nodeId);
459
472
  // Build set of handles that have inbound edges
460
473
  // But only exclude handles whose edges are NOT selected
461
474
  const inboundHandlesToExclude = new Set(inboundEdges
@@ -479,7 +492,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
479
492
  };
480
493
  });
481
494
  // Collect edges between copied nodes
482
- const copiedEdges = def.edges
495
+ const copiedEdges = this.def.edges
483
496
  .filter((edge) => {
484
497
  return (selectedNodeSet.has(edge.source.nodeId) &&
485
498
  selectedNodeSet.has(edge.target.nodeId));
@@ -497,6 +510,193 @@ class InMemoryWorkbench extends AbstractWorkbench {
497
510
  bounds,
498
511
  };
499
512
  }
513
+ /**
514
+ * Duplicate all selected nodes.
515
+ * Returns the list of newly created node IDs.
516
+ * Each duplicated node is offset by 24px in both x and y directions.
517
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
518
+ */
519
+ duplicateSelection(runner, options) {
520
+ const selection = this.getSelection();
521
+ if (selection.nodes.length === 0)
522
+ return [];
523
+ const positions = this.getPositions();
524
+ const newNodes = [];
525
+ // Get inputs without bindings (literal values only)
526
+ const allInputs = runner.getInputs(this.def) || {};
527
+ // Duplicate each selected node
528
+ for (const nodeId of selection.nodes) {
529
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
530
+ if (!n)
531
+ continue;
532
+ const pos = positions[nodeId] || { x: 0, y: 0 };
533
+ const inboundHandles = new Set(this.def.edges
534
+ .filter((e) => e.target.nodeId === nodeId)
535
+ .map((e) => e.target.handle));
536
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
537
+ const newNodeId = this.addNode({
538
+ typeId: n.typeId,
539
+ params: n.params,
540
+ position: { x: pos.x + 24, y: pos.y + 24 },
541
+ resolvedHandles: n.resolvedHandles,
542
+ }, {
543
+ inputs: inputsWithoutBindings,
544
+ copyOutputsFrom: nodeId,
545
+ dry: true,
546
+ });
547
+ newNodes.push(newNodeId);
548
+ }
549
+ // Select all newly duplicated nodes
550
+ if (newNodes.length > 0) {
551
+ this.setSelectionInternal({ nodes: newNodes, edges: [] }, options || { commit: true, reason: "duplicate-selection" });
552
+ }
553
+ return newNodes;
554
+ }
555
+ /**
556
+ * Bake an output value from a node into a new node.
557
+ * Creates a new node based on the output type's bakeTarget configuration.
558
+ * Returns the ID of the last created node (or undefined if none created).
559
+ */
560
+ async bake(registry, runner, outputValue, outputTypeId, nodePosition, options) {
561
+ try {
562
+ if (!outputTypeId || outputValue === undefined)
563
+ return undefined;
564
+ const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
565
+ const coerceIfNeeded = async (fromType, toType, value) => {
566
+ if (!toType || toType === fromType || !runner?.coerce)
567
+ return value;
568
+ try {
569
+ return await runner.coerce(fromType, toType, value);
570
+ }
571
+ catch {
572
+ return value;
573
+ }
574
+ };
575
+ const pos = nodePosition;
576
+ const isArray = outputTypeId.endsWith("[]");
577
+ const baseTypeId = isArray ? outputTypeId.slice(0, -2) : outputTypeId;
578
+ const tArr = isArray ? registry.types.get(outputTypeId) : undefined;
579
+ const tElem = registry.types.get(baseTypeId);
580
+ const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
581
+ const arrTarget = isArray ? tArr?.bakeTarget : undefined;
582
+ const elemTarget = isArray ? tElem?.bakeTarget : undefined;
583
+ let newNodeId;
584
+ if (singleTarget) {
585
+ const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
586
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
587
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
588
+ newNodeId = this.addNode({
589
+ typeId: singleTarget.nodeTypeId,
590
+ position: { x: pos.x + 180, y: pos.y },
591
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
592
+ }
593
+ else if (isArray && arrTarget) {
594
+ const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
595
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
596
+ const coerced = await coerceIfNeeded(outputTypeId, inType, unwrap(outputValue));
597
+ newNodeId = this.addNode({
598
+ typeId: arrTarget.nodeTypeId,
599
+ position: { x: pos.x + 180, y: pos.y },
600
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
601
+ }
602
+ else if (isArray && elemTarget) {
603
+ const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
604
+ const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
605
+ const src = unwrap(outputValue);
606
+ const items = Array.isArray(src) ? src : [src];
607
+ const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
608
+ const COLS = 4;
609
+ const DX = 180;
610
+ const DY = 160;
611
+ for (let idx = 0; idx < coercedItems.length; idx++) {
612
+ const col = idx % COLS;
613
+ const row = Math.floor(idx / COLS);
614
+ newNodeId = this.addNode({
615
+ typeId: elemTarget.nodeTypeId,
616
+ position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
617
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
618
+ }
619
+ }
620
+ if (newNodeId) {
621
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "bake" });
622
+ }
623
+ return newNodeId;
624
+ }
625
+ catch {
626
+ return undefined;
627
+ }
628
+ }
629
+ /**
630
+ * Duplicate a single node.
631
+ * Returns the ID of the newly created node.
632
+ * The duplicated node is offset by 24px in both x and y directions.
633
+ * Copies inputs without bindings and uses copyOutputsFrom to copy outputs.
634
+ */
635
+ duplicateNode(nodeId, runner, options) {
636
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
637
+ if (!n)
638
+ return undefined;
639
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
640
+ // Get inputs without bindings (literal values only)
641
+ const allInputs = runner.getInputs(this.def)[nodeId] || {};
642
+ const inboundHandles = new Set(this.def.edges
643
+ .filter((e) => e.target.nodeId === nodeId)
644
+ .map((e) => e.target.handle));
645
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
646
+ const newNodeId = this.addNode({
647
+ typeId: n.typeId,
648
+ params: n.params,
649
+ position: { x: pos.x + 24, y: pos.y + 24 },
650
+ resolvedHandles: n.resolvedHandles,
651
+ }, {
652
+ inputs: inputsWithoutBindings,
653
+ copyOutputsFrom: nodeId,
654
+ dry: true,
655
+ });
656
+ // Select the newly duplicated node
657
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node" });
658
+ return newNodeId;
659
+ }
660
+ /**
661
+ * Duplicate a node and all its incoming edges.
662
+ * Returns the ID of the newly created node.
663
+ * The duplicated node is offset by 24px in both x and y directions.
664
+ * All incoming edges are duplicated to point to the new node.
665
+ */
666
+ duplicateNodeWithEdges(nodeId, runner, options) {
667
+ const n = this.def.nodes.find((n) => n.nodeId === nodeId);
668
+ if (!n)
669
+ return undefined;
670
+ const pos = this.getPositions()[nodeId] || { x: 0, y: 0 };
671
+ // Get all inputs (including those with bindings, since edges will be duplicated)
672
+ const inputs = runner.getInputs(this.def)[nodeId] || {};
673
+ // Add the duplicated node
674
+ const newNodeId = this.addNode({
675
+ typeId: n.typeId,
676
+ params: n.params,
677
+ position: { x: pos.x + 24, y: pos.y + 24 },
678
+ resolvedHandles: n.resolvedHandles,
679
+ }, {
680
+ inputs,
681
+ copyOutputsFrom: nodeId,
682
+ dry: true,
683
+ });
684
+ // Find all incoming edges (edges where target is the original node)
685
+ const incomingEdges = this.def.edges.filter((e) => e.target.nodeId === nodeId);
686
+ // Duplicate each incoming edge to point to the new node
687
+ for (const edge of incomingEdges) {
688
+ this.connect({
689
+ source: edge.source, // Keep the same source
690
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
691
+ typeId: edge.typeId,
692
+ }, { dry: true });
693
+ }
694
+ // Select the newly duplicated node
695
+ if (newNodeId) {
696
+ this.setSelectionInternal({ nodes: [newNodeId], edges: [] }, options || { commit: true, reason: "duplicate-node-with-edges" });
697
+ }
698
+ return newNodeId;
699
+ }
500
700
  /**
501
701
  * Paste copied graph data at the specified center position.
502
702
  * Returns the mapping from original node IDs to new node IDs.
@@ -542,7 +742,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
542
742
  }
543
743
  }
544
744
  // Select the newly pasted nodes
545
- this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
745
+ this.setSelectionInternal({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
546
746
  return { nodeIdMap, edgeIds };
547
747
  }
548
748
  /**
@@ -557,6 +757,29 @@ class InMemoryWorkbench extends AbstractWorkbench {
557
757
  setCopiedData(data) {
558
758
  this.copiedData = data;
559
759
  }
760
+ /**
761
+ * Get the custom name for a node, if set.
762
+ */
763
+ getNodeName(nodeId) {
764
+ return this.nodeNames[nodeId];
765
+ }
766
+ /**
767
+ * Set a custom name for a node. Empty string or undefined removes the custom name.
768
+ * This is included in undo/redo history via extData.ui.
769
+ */
770
+ setNodeName(nodeId, name, options) {
771
+ if (name === undefined || name.trim() === "") {
772
+ delete this.nodeNames[nodeId];
773
+ }
774
+ else {
775
+ this.nodeNames[nodeId] = name.trim();
776
+ }
777
+ this.emit("graphUiChanged", {
778
+ def: this._def,
779
+ change: { type: "nodeName", nodeId },
780
+ ...options,
781
+ });
782
+ }
560
783
  }
561
784
 
562
785
  class CLIWorkbench {
@@ -567,8 +790,8 @@ class CLIWorkbench {
567
790
  async load(def) {
568
791
  await this.wb.load(def);
569
792
  }
570
- print(def, options) {
571
- const d = def ?? this.wb.export();
793
+ print(options) {
794
+ const d = this.wb.def;
572
795
  const detail = !!options?.detail;
573
796
  const lines = [];
574
797
  lines.push(`Nodes (${d.nodes.length})`);
@@ -1937,8 +2160,7 @@ function useWorkbenchBridge(wb) {
1937
2160
  if (!params.sourceHandle || !params.targetHandle)
1938
2161
  return;
1939
2162
  // Prevent duplicate edges between the same endpoints
1940
- const def = wb.export();
1941
- const exists = def.edges.some((e) => e.source.nodeId === params.source &&
2163
+ const exists = wb.def.edges.some((e) => e.source.nodeId === params.source &&
1942
2164
  e.source.handle === params.sourceHandle &&
1943
2165
  e.target.nodeId === params.target &&
1944
2166
  e.target.handle === params.targetHandle);
@@ -1988,7 +2210,10 @@ function useWorkbenchBridge(wb) {
1988
2210
  }
1989
2211
  }
1990
2212
  if (selectionChanged) {
1991
- wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
2213
+ wb.setSelection({
2214
+ nodes: Array.from(nextNodeIds),
2215
+ edges: current.edges,
2216
+ });
1992
2217
  }
1993
2218
  if (Object.keys(positions).length > 0) {
1994
2219
  wb.setPositions(positions, { commit });
@@ -2023,7 +2248,10 @@ function useWorkbenchBridge(wb) {
2023
2248
  }
2024
2249
  }
2025
2250
  if (selectionChanged) {
2026
- wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
2251
+ wb.setSelection({
2252
+ nodes: current.nodes,
2253
+ edges: Array.from(nextEdgeIds),
2254
+ });
2027
2255
  }
2028
2256
  }, [wb]);
2029
2257
  const onNodesDelete = React.useCallback((nodes) => {
@@ -2469,7 +2697,6 @@ function isSnapshotPayload(parsed) {
2469
2697
  }
2470
2698
  async function download(wb, runner) {
2471
2699
  try {
2472
- const def = wb.export();
2473
2700
  const fullUiState = wb.getUIState();
2474
2701
  const uiState = excludeViewportFromUIState(fullUiState);
2475
2702
  const runtimeState = wb.getRuntimeState();
@@ -2478,7 +2705,7 @@ async function download(wb, runner) {
2478
2705
  const fullSnapshot = await runner.snapshotFull();
2479
2706
  snapshot = {
2480
2707
  ...fullSnapshot,
2481
- def,
2708
+ def: wb.def,
2482
2709
  extData: {
2483
2710
  ...(fullSnapshot.extData || {}),
2484
2711
  ui: Object.keys(uiState || {}).length > 0 ? uiState : undefined,
@@ -2487,9 +2714,9 @@ async function download(wb, runner) {
2487
2714
  };
2488
2715
  }
2489
2716
  else {
2490
- const inputs = runner.getInputs(def);
2717
+ const inputs = runner.getInputs(wb.def);
2491
2718
  snapshot = {
2492
- def,
2719
+ def: wb.def,
2493
2720
  inputs,
2494
2721
  outputs: {},
2495
2722
  environment: {},
@@ -2528,7 +2755,7 @@ async function upload(parsed, wb, runner) {
2528
2755
  }
2529
2756
  if (runner.isRunning()) {
2530
2757
  await runner.applySnapshotFull({
2531
- def,
2758
+ def: wb.def,
2532
2759
  environment,
2533
2760
  inputs,
2534
2761
  outputs,
@@ -2536,7 +2763,7 @@ async function upload(parsed, wb, runner) {
2536
2763
  });
2537
2764
  }
2538
2765
  else {
2539
- runner.build(wb.export());
2766
+ runner.build(wb.def);
2540
2767
  if (inputs && typeof inputs === "object") {
2541
2768
  for (const [nodeId, map] of Object.entries(inputs)) {
2542
2769
  runner.setInputs(nodeId, map, { dry: true });
@@ -2632,14 +2859,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2632
2859
  const versionTick = useWorkbenchVersionTick(runner);
2633
2860
  const valuesTick = versionTick + graphTick + graphUiTick;
2634
2861
  // Def and IO values
2635
- const def = wb.export();
2636
- const inputsMap = React.useMemo(() => runner.getInputs(def), [runner, def, valuesTick]);
2637
- const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(def), [runner, def, valuesTick]);
2638
- const outputsMap = React.useMemo(() => runner.getOutputs(def), [runner, def, valuesTick]);
2862
+ const inputsMap = React.useMemo(() => runner.getInputs(wb.def), [runner, wb, wb.def, valuesTick]);
2863
+ const inputDefaultsMap = React.useMemo(() => runner.getInputDefaults(wb.def), [runner, wb, wb.def, valuesTick]);
2864
+ const outputsMap = React.useMemo(() => runner.getOutputs(wb.def), [runner, wb, wb.def, valuesTick]);
2639
2865
  const outputTypesMap = React.useMemo(() => {
2640
2866
  const out = {};
2641
2867
  // Local: runtimeTypeId is not stored; derive from typed wrapper in outputsMap
2642
- for (const n of def.nodes) {
2868
+ for (const n of wb.def.nodes) {
2643
2869
  const effectiveHandles = computeEffectiveHandles(n, registry);
2644
2870
  const outputsDecl = effectiveHandles.outputs;
2645
2871
  const handles = Object.keys(outputsDecl);
@@ -2654,14 +2880,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2654
2880
  out[n.nodeId] = cur;
2655
2881
  }
2656
2882
  return out;
2657
- }, [def, outputsMap, registry]);
2883
+ }, [wb, wb.def, outputsMap, registry]);
2658
2884
  // Initialize nodes and derive invalidated status from persisted metadata
2659
2885
  React.useEffect(() => {
2660
2886
  const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
2661
2887
  setNodeStatus((prev) => {
2662
2888
  const next = { ...prev };
2663
2889
  const metadata = workbenchRuntimeState;
2664
- for (const n of def.nodes) {
2890
+ for (const n of wb.def.nodes) {
2665
2891
  const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
2666
2892
  const nodeMeta = metadata.nodes[n.nodeId];
2667
2893
  const updates = {};
@@ -2683,18 +2909,17 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2683
2909
  }
2684
2910
  return next;
2685
2911
  });
2686
- }, [def, wb]);
2912
+ }, [wb.def, wb]);
2687
2913
  // Auto layout (simple layered layout)
2688
2914
  const runAutoLayout = React.useCallback(() => {
2689
- const cur = wb.export();
2690
2915
  // Build DAG layers by indegree
2691
2916
  const indegree = {};
2692
2917
  const adj = {};
2693
- for (const n of cur.nodes) {
2918
+ for (const n of wb.def.nodes) {
2694
2919
  indegree[n.nodeId] = 0;
2695
2920
  adj[n.nodeId] = [];
2696
2921
  }
2697
- for (const e of cur.edges) {
2922
+ for (const e of wb.def.edges) {
2698
2923
  indegree[e.target.nodeId] = (indegree[e.target.nodeId] ?? 0) + 1;
2699
2924
  adj[e.source.nodeId].push(e.target.nodeId);
2700
2925
  }
@@ -2725,7 +2950,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2725
2950
  let maxWidth = 0;
2726
2951
  const heights = {};
2727
2952
  for (const id of layer) {
2728
- const node = cur.nodes.find((n) => n.nodeId === id);
2953
+ const node = wb.def.nodes.find((n) => n.nodeId === id);
2729
2954
  if (!node)
2730
2955
  continue;
2731
2956
  // Prefer showValues sizing similar to node rendering
@@ -2751,26 +2976,39 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2751
2976
  curX += maxWidth + H_GAP;
2752
2977
  }
2753
2978
  wb.setPositions(pos, { commit: true, reason: "auto-layout" });
2754
- }, [wb, registry, overrides?.getDefaultNodeSize]);
2979
+ }, [wb, wb.def, registry, overrides?.getDefaultNodeSize]);
2755
2980
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2756
2981
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2982
+ const getNodeDisplayName = React.useCallback((nodeId) => {
2983
+ const customName = wb.getNodeName(nodeId);
2984
+ if (customName)
2985
+ return customName;
2986
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
2987
+ if (!node)
2988
+ return nodeId;
2989
+ const desc = registry.nodes.get(node.typeId);
2990
+ return desc?.displayName || node.typeId;
2991
+ }, [wb, registry]);
2992
+ const setNodeName = React.useCallback((nodeId, name) => {
2993
+ wb.setNodeName(nodeId, name, { commit: true, reason: "rename-node" });
2994
+ }, [wb]);
2757
2995
  // Helper to save runtime metadata and UI state to extData
2758
- const saveUiRuntimeMetadata = React.useCallback(async () => {
2996
+ const saveUiRuntimeMetadata = React.useCallback(async (workbench, graphRunner) => {
2759
2997
  try {
2760
- const current = wb.getRuntimeState() ?? { nodes: {} };
2998
+ const current = workbench.getRuntimeState() ?? { nodes: {} };
2761
2999
  const metadata = { nodes: { ...current.nodes } };
2762
3000
  // Clean up metadata for nodes that no longer exist
2763
- const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
3001
+ const nodeIds = new Set(workbench.def.nodes.map((n) => n.nodeId));
2764
3002
  for (const nodeId of Object.keys(metadata.nodes)) {
2765
3003
  if (!nodeIds.has(nodeId)) {
2766
3004
  delete metadata.nodes[nodeId];
2767
3005
  }
2768
3006
  }
2769
3007
  // Save cleaned metadata to workbench state
2770
- wb.setRuntimeState(metadata);
2771
- const fullUiState = wb.getUIState();
3008
+ workbench.setRuntimeState(metadata);
3009
+ const fullUiState = workbench.getUIState();
2772
3010
  const uiWithoutViewport = excludeViewportFromUIState(fullUiState);
2773
- await runner.setExtData?.({
3011
+ await graphRunner.setExtData?.({
2774
3012
  ...(Object.keys(uiWithoutViewport || {}).length > 0
2775
3013
  ? { ui: uiWithoutViewport }
2776
3014
  : {}),
@@ -2780,7 +3018,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2780
3018
  catch (err) {
2781
3019
  console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
2782
3020
  }
2783
- }, [wb, def, runner]);
3021
+ }, []);
2784
3022
  // Subscribe to runner/workbench events
2785
3023
  React.useEffect(() => {
2786
3024
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -2806,9 +3044,8 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2806
3044
  });
2807
3045
  // Helper to apply resolved handles from event payload to workbench
2808
3046
  const applyResolvedHandles = (resolvedHandles) => {
2809
- const cur = wb.export();
2810
3047
  let changed = false;
2811
- for (const n of cur.nodes) {
3048
+ for (const n of wb.def.nodes) {
2812
3049
  const updated = resolvedHandles[n.nodeId];
2813
3050
  if (updated) {
2814
3051
  const before = JSON.stringify(n.resolvedHandles || {});
@@ -3097,6 +3334,9 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3097
3334
  else if (changeType === "updateEdgeType") {
3098
3335
  reason = "update-edge-type";
3099
3336
  }
3337
+ else if (changeType === "setInputs") {
3338
+ reason = "set-inputs";
3339
+ }
3100
3340
  }
3101
3341
  if (event.change?.type === "setInputs") {
3102
3342
  const { nodeId, inputs } = event.change;
@@ -3104,7 +3344,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3104
3344
  }
3105
3345
  if (!runner.isRunning()) {
3106
3346
  if (event.commit) {
3107
- await saveUiRuntimeMetadata();
3347
+ await saveUiRuntimeMetadata(wb, runner);
3108
3348
  const history = await runner.commit(reason).catch((err) => {
3109
3349
  console.error("[WorkbenchContext] Error committing:", err);
3110
3350
  return undefined;
@@ -3137,7 +3377,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3137
3377
  await runner.update(event.def, { dry: event.dry });
3138
3378
  }
3139
3379
  if (event.commit) {
3140
- await saveUiRuntimeMetadata();
3380
+ await saveUiRuntimeMetadata(wb, runner);
3141
3381
  const history = await runner
3142
3382
  .commit(event.reason ?? reason)
3143
3383
  .catch((err) => {
@@ -3158,7 +3398,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3158
3398
  setSelectedNodeId(sel.nodes?.[0]);
3159
3399
  setSelectedEdgeId(sel.edges?.[0]);
3160
3400
  if (sel.commit) {
3161
- await saveUiRuntimeMetadata();
3401
+ await saveUiRuntimeMetadata(wb, runner);
3162
3402
  const history = await runner
3163
3403
  .commit(sel.reason ?? "selection")
3164
3404
  .catch((err) => {
@@ -3197,7 +3437,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3197
3437
  reason = "selection";
3198
3438
  }
3199
3439
  }
3200
- await saveUiRuntimeMetadata();
3440
+ await saveUiRuntimeMetadata(wb, runner);
3201
3441
  const history = await runner
3202
3442
  .commit(event.reason ?? reason)
3203
3443
  .catch((err) => {
@@ -3217,7 +3457,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3217
3457
  wb.setRegistry(newReg);
3218
3458
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
3219
3459
  try {
3220
- await runner.update(wb.export());
3460
+ await runner.update(wb.def);
3221
3461
  }
3222
3462
  catch {
3223
3463
  console.error("Failed to update graph definition after registry changed");
@@ -3240,7 +3480,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3240
3480
  setNodeStatus(() => {
3241
3481
  const next = {};
3242
3482
  const metadata = wb.getRuntimeState() ?? { nodes: {} };
3243
- for (const n of def.nodes) {
3483
+ for (const n of wb.def.nodes) {
3244
3484
  const nodeMeta = metadata.nodes[n.nodeId];
3245
3485
  next[n.nodeId] = {
3246
3486
  activeRuns: 0,
@@ -3255,6 +3495,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3255
3495
  errorRunsRef.current = {};
3256
3496
  }
3257
3497
  });
3498
+ const offWbRuntimeMetadataChanged = wb.on("runtimeMetadataChanged", (event) => {
3499
+ const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
3500
+ setNodeStatus((prev) => {
3501
+ const next = { ...prev };
3502
+ const metadata = workbenchRuntimeState;
3503
+ let changed = false;
3504
+ // If nodeId is specified, only update that node; otherwise update all nodes
3505
+ const nodesToUpdate = event.nodeId
3506
+ ? wb.def.nodes.filter((n) => n.nodeId === event.nodeId)
3507
+ : wb.def.nodes;
3508
+ for (const n of nodesToUpdate) {
3509
+ const cur = next[n.nodeId];
3510
+ if (!cur)
3511
+ continue;
3512
+ const nodeMeta = metadata.nodes[n.nodeId];
3513
+ const newInvalidated = computeInvalidatedFromMetadata(nodeMeta);
3514
+ if (cur.invalidated !== newInvalidated) {
3515
+ next[n.nodeId] = { ...cur, invalidated: newInvalidated };
3516
+ changed = true;
3517
+ }
3518
+ }
3519
+ return changed ? next : prev;
3520
+ });
3521
+ });
3258
3522
  wb.refreshValidation();
3259
3523
  return () => {
3260
3524
  offRunnerValue();
@@ -3272,13 +3536,14 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3272
3536
  offRunnerRegistry();
3273
3537
  offRunnerTransport();
3274
3538
  offFlowViewport();
3539
+ offWbRuntimeMetadataChanged();
3275
3540
  };
3276
3541
  }, [runner, wb, setRegistry]);
3277
3542
  const isRunning = React.useCallback(() => runner.isRunning(), [runner]);
3278
3543
  const engineKind = React.useCallback(() => runner.getRunningEngine(), [runner]);
3279
3544
  const start = React.useCallback((engine) => {
3280
3545
  try {
3281
- runner.launch(wb.export(), { engine });
3546
+ runner.launch(wb.def, { engine });
3282
3547
  }
3283
3548
  catch { }
3284
3549
  }, [runner, wb]);
@@ -3353,7 +3618,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3353
3618
  runner,
3354
3619
  registry,
3355
3620
  setRegistry,
3356
- def,
3357
3621
  selectedNodeId,
3358
3622
  selectedEdgeId,
3359
3623
  setSelection,
@@ -3389,12 +3653,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3389
3653
  triggerExternal,
3390
3654
  uiVersion,
3391
3655
  overrides,
3656
+ getNodeDisplayName,
3657
+ setNodeName,
3392
3658
  }), [
3393
3659
  wb,
3394
3660
  runner,
3395
3661
  registry,
3396
3662
  setRegistry,
3397
- def,
3398
3663
  selectedNodeId,
3399
3664
  selectedEdgeId,
3400
3665
  setSelection,
@@ -3429,142 +3694,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3429
3694
  runner,
3430
3695
  uiVersion,
3431
3696
  overrides,
3697
+ getNodeDisplayName,
3698
+ setNodeName,
3432
3699
  ]);
3433
3700
  return (jsxRuntime.jsx(WorkbenchContext.Provider, { value: value, children: children }));
3434
3701
  }
3435
3702
 
3436
3703
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose, getDefaultNodeSize, onCopyResult) {
3437
- const doBake = async (handleId) => {
3438
- try {
3439
- const typeId = outputTypesMap?.[nodeId]?.[handleId];
3440
- const raw = outputsMap?.[nodeId]?.[handleId];
3441
- let newNodeId;
3442
- if (!typeId || raw === undefined)
3443
- return;
3444
- const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
3445
- const coerceIfNeeded = async (fromType, toType, value) => {
3446
- if (!toType || toType === fromType || !runner?.coerce)
3447
- return value;
3448
- try {
3449
- return await runner.coerce(fromType, toType, value);
3450
- }
3451
- catch {
3452
- return value;
3453
- }
3454
- };
3455
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3456
- const isArray = typeId.endsWith("[]");
3457
- const baseTypeId = isArray ? typeId.slice(0, -2) : typeId;
3458
- const tArr = isArray ? registry.types.get(typeId) : undefined;
3459
- const tElem = registry.types.get(baseTypeId);
3460
- const singleTarget = !isArray ? tElem?.bakeTarget : undefined;
3461
- const arrTarget = isArray ? tArr?.bakeTarget : undefined;
3462
- const elemTarget = isArray ? tElem?.bakeTarget : undefined;
3463
- if (singleTarget) {
3464
- const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3465
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3466
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3467
- newNodeId = wb.addNode({
3468
- typeId: singleTarget.nodeTypeId,
3469
- position: { x: pos.x + 180, y: pos.y },
3470
- }, { inputs: { [singleTarget.inputHandle]: coerced } });
3471
- }
3472
- else if (isArray && arrTarget) {
3473
- const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3474
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3475
- const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3476
- newNodeId = wb.addNode({
3477
- typeId: arrTarget.nodeTypeId,
3478
- position: { x: pos.x + 180, y: pos.y },
3479
- }, { inputs: { [arrTarget.inputHandle]: coerced } });
3480
- }
3481
- else if (isArray && elemTarget) {
3482
- const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3483
- const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3484
- const src = unwrap(raw);
3485
- const items = Array.isArray(src) ? src : [src];
3486
- const coercedItems = await Promise.all(items.map((v) => coerceIfNeeded(baseTypeId, inType, v)));
3487
- const COLS = 4;
3488
- const DX = 180;
3489
- const DY = 160;
3490
- for (let idx = 0; idx < coercedItems.length; idx++) {
3491
- const col = idx % COLS;
3492
- const row = Math.floor(idx / COLS);
3493
- newNodeId = wb.addNode({
3494
- typeId: elemTarget.nodeTypeId,
3495
- position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3496
- }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3497
- }
3498
- }
3499
- if (newNodeId) {
3500
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
3501
- }
3502
- }
3503
- catch { }
3504
- };
3505
3704
  return {
3506
3705
  onDelete: () => {
3507
3706
  wb.removeNode(nodeId, { commit: true });
3508
3707
  onClose();
3509
3708
  },
3510
3709
  onDuplicate: async () => {
3511
- const def = wb.export();
3512
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3513
- if (!n)
3514
- return onClose();
3515
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3516
- const inboundHandles = new Set(def.edges
3517
- .filter((e) => e.target.nodeId === nodeId)
3518
- .map((e) => e.target.handle));
3519
- const allInputs = runner.getInputs(def)[nodeId] || {};
3520
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3521
- const newNodeId = wb.addNode({
3522
- typeId: n.typeId,
3523
- params: n.params,
3524
- position: { x: pos.x + 24, y: pos.y + 24 },
3525
- resolvedHandles: n.resolvedHandles,
3526
- }, {
3527
- inputs: inputsWithoutBindings,
3528
- copyOutputsFrom: nodeId,
3529
- dry: true,
3710
+ wb.duplicateNode(nodeId, runner, {
3711
+ commit: true,
3712
+ reason: "duplicate-node",
3530
3713
  });
3531
- // Select the newly duplicated node
3532
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
3533
3714
  onClose();
3534
3715
  },
3535
3716
  onDuplicateWithEdges: async () => {
3536
- const def = wb.export();
3537
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3538
- if (!n)
3539
- return onClose();
3540
- const pos = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3541
- // Get inputs without bindings (literal values only)
3542
- const inputs = runner.getInputs(def)[nodeId] || {};
3543
- // Add the duplicated node
3544
- const newNodeId = wb.addNode({
3545
- typeId: n.typeId,
3546
- params: n.params,
3547
- position: { x: pos.x + 24, y: pos.y + 24 },
3548
- resolvedHandles: n.resolvedHandles,
3549
- }, {
3550
- inputs,
3551
- copyOutputsFrom: nodeId,
3552
- dry: true,
3717
+ wb.duplicateNodeWithEdges(nodeId, runner, {
3718
+ commit: true,
3719
+ reason: "duplicate-node-with-edges",
3553
3720
  });
3554
- // Find all incoming edges (edges where target is the original node)
3555
- const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3556
- // Duplicate each incoming edge to point to the new node
3557
- for (const edge of incomingEdges) {
3558
- wb.connect({
3559
- source: edge.source, // Keep the same source
3560
- target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3561
- typeId: edge.typeId,
3562
- }, { dry: true });
3563
- }
3564
- // Select the newly duplicated node and edges
3565
- if (newNodeId) {
3566
- wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
3567
- }
3568
3721
  onClose();
3569
3722
  },
3570
3723
  onRunPull: async () => {
@@ -3575,7 +3728,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3575
3728
  onClose();
3576
3729
  },
3577
3730
  onBake: async (handleId) => {
3578
- await doBake(handleId);
3731
+ const nodePosition = wb.getPositions()[nodeId] || { x: 0, y: 0 };
3732
+ const typeId = outputTypesMap?.[nodeId]?.[handleId];
3733
+ const raw = outputsMap?.[nodeId]?.[handleId];
3734
+ await wb.bake(registry, runner, raw, typeId || "", nodePosition, {
3735
+ commit: true,
3736
+ reason: "bake",
3737
+ });
3579
3738
  onClose();
3580
3739
  },
3581
3740
  onCopy: () => {
@@ -3632,42 +3791,10 @@ function createNodeCopyHandler(wb, runner, nodeId, getDefaultNodeSize, onCopyRes
3632
3791
  function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onCopyResult, runner) {
3633
3792
  const onDuplicate = runner
3634
3793
  ? () => {
3635
- const selection = wb.getSelection();
3636
- if (selection.nodes.length === 0) {
3637
- onClose();
3638
- return;
3639
- }
3640
- const def = wb.export();
3641
- const positions = wb.getPositions();
3642
- const newNodes = [];
3643
- // Duplicate each selected node
3644
- for (const nodeId of selection.nodes) {
3645
- const n = def.nodes.find((n) => n.nodeId === nodeId);
3646
- if (!n)
3647
- continue;
3648
- const pos = positions[nodeId] || { x: 0, y: 0 };
3649
- // Get inputs without bindings (literal values only)
3650
- const allInputs = runner.getInputs(def)[nodeId] || {};
3651
- const inboundHandles = new Set(def.edges
3652
- .filter((e) => e.target.nodeId === nodeId)
3653
- .map((e) => e.target.handle));
3654
- const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3655
- const newNodeId = wb.addNode({
3656
- typeId: n.typeId,
3657
- params: n.params,
3658
- position: { x: pos.x + 24, y: pos.y + 24 },
3659
- resolvedHandles: n.resolvedHandles,
3660
- }, {
3661
- inputs: inputsWithoutBindings,
3662
- copyOutputsFrom: nodeId,
3663
- dry: true,
3664
- });
3665
- newNodes.push(newNodeId);
3666
- }
3667
- // Select all newly duplicated nodes
3668
- if (newNodes.length > 0) {
3669
- wb.setSelection({ nodes: newNodes, edges: [] }, { commit: true, reason: "duplicate-selection" });
3670
- }
3794
+ wb.duplicateSelection(runner, {
3795
+ commit: true,
3796
+ reason: "duplicate-selection",
3797
+ });
3671
3798
  onClose();
3672
3799
  }
3673
3800
  : undefined;
@@ -3702,9 +3829,8 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3702
3829
  const canRedo = history ? history.redoCount > 0 : undefined;
3703
3830
  const onSelectAll = wb
3704
3831
  ? () => {
3705
- const def = wb.export();
3706
- const allNodeIds = def.nodes.map((n) => n.nodeId);
3707
- const allEdgeIds = def.edges.map((e) => e.id);
3832
+ const allNodeIds = wb.def.nodes.map((n) => n.nodeId);
3833
+ const allEdgeIds = wb.def.edges.map((e) => e.id);
3708
3834
  wb.setSelection({ nodes: allNodeIds, edges: allEdgeIds }, { commit: true, reason: "select-all" });
3709
3835
  onClose();
3710
3836
  }
@@ -3723,8 +3849,7 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3723
3849
  }
3724
3850
  function getBakeableOutputs(nodeId, wb, registry, outputTypesMap) {
3725
3851
  try {
3726
- const def = wb.export();
3727
- const node = def.nodes.find((n) => n.nodeId === nodeId);
3852
+ const node = wb.def.nodes.find((n) => n.nodeId === nodeId);
3728
3853
  if (!node)
3729
3854
  return [];
3730
3855
  const desc = registry.nodes.get(node.typeId);
@@ -3823,13 +3948,13 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3823
3948
  return String(value ?? "");
3824
3949
  }
3825
3950
  };
3826
- 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();
3951
+ 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();
3827
3952
  const nodeValidationIssues = validationByNode.issues;
3828
3953
  const edgeValidationIssues = validationByEdge.issues;
3829
3954
  const nodeValidationHandles = validationByNode;
3830
3955
  const globalValidationIssues = validationGlobal;
3831
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
3832
- const selectedEdge = def.edges.find((e) => e.id === selectedEdgeId);
3956
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
3957
+ const selectedEdge = wb.def.edges.find((e) => e.id === selectedEdgeId);
3833
3958
  // Use computeEffectiveHandles to merge registry defaults with dynamically resolved handles
3834
3959
  const effectiveHandles = selectedNode
3835
3960
  ? computeEffectiveHandles(selectedNode, registry)
@@ -3961,7 +4086,6 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3961
4086
  setOriginals(nextOriginals);
3962
4087
  }, [selectedNodeId, selectedNode, registry, valuesTick]);
3963
4088
  const widthClass = debug ? "w-[480px]" : "w-[320px]";
3964
- const { wb } = useWorkbenchContext();
3965
4089
  const deleteEdgeById = (edgeId) => {
3966
4090
  if (!edgeId)
3967
4091
  return;
@@ -3988,9 +4112,9 @@ function Inspector({ debug, autoScroll, hideWorkbench, onAutoScrollChange, onHid
3988
4112
  selectedNodeStatus.activeRunIds.length > 0 ? (jsxRuntime.jsxs("div", { className: "mt-1", children: [jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600", children: "RunIds:" }), jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: selectedNodeStatus.activeRunIds.map((runId, idx) => (jsxRuntime.jsx("span", { className: "text-[10px] px-1.5 py-0.5 bg-blue-100 border border-blue-300 rounded font-mono", children: runId }, idx))) })] })) : (jsxRuntime.jsx("div", { className: "text-[10px] text-blue-600 mt-1", children: "RunIds not available (some runs may have started without runId)" }))] })), !!selectedNodeStatus?.lastError && (jsxRuntime.jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-2 py-1 break-words", children: String(selectedNodeStatus.lastError?.message ??
3989
4113
  selectedNodeStatus.lastError) }))] })), jsxRuntime.jsxs("div", { className: "mb-2", children: [jsxRuntime.jsx("div", { className: "font-semibold mb-1", children: "Inputs" }), inputHandles.length === 0 ? (jsxRuntime.jsx("div", { className: "text-gray-500", children: "No inputs" })) : (inputHandles.map((h) => {
3990
4114
  const typeId = sparkGraph.getInputTypeId(effectiveHandles.inputs, h);
3991
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId &&
4115
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId &&
3992
4116
  e.target.handle === h);
3993
- const inbound = new Set(def.edges
4117
+ const inbound = new Set(wb.def.edges
3994
4118
  .filter((e) => e.target.nodeId === selectedNodeId &&
3995
4119
  e.target.handle === h)
3996
4120
  .map((e) => e.target.handle));
@@ -4160,11 +4284,27 @@ const DefaultNode = React.memo(function DefaultNode({ id, data, selected, isConn
4160
4284
  position: "relative",
4161
4285
  minWidth: typeof data.renderWidth === "number" ? data.renderWidth : undefined,
4162
4286
  minHeight: typeof data.renderHeight === "number" ? data.renderHeight : undefined,
4163
- }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, title: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4287
+ }, children: [jsxRuntime.jsx(DefaultNodeHeader, { id: id, typeId: typeId, validation: validation, showId: data.showValues }), jsxRuntime.jsx(DefaultNodeContent, { data: data, isConnectable: isConnectable })] }));
4164
4288
  });
4165
4289
  DefaultNode.displayName = "DefaultNode";
4166
- function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate, }) {
4290
+ function DefaultNodeHeader({ id, typeId, title, validation, right, showId, onInvalidate, }) {
4167
4291
  const ctx = useWorkbenchContext();
4292
+ const [isEditing, setIsEditing] = React.useState(false);
4293
+ const [editValue, setEditValue] = React.useState("");
4294
+ const inputRef = React.useRef(null);
4295
+ // Use getNodeDisplayName if typeId is provided, otherwise use title prop
4296
+ const displayName = typeId ? ctx.getNodeDisplayName(id) : (title ?? id);
4297
+ const effectiveTypeId = typeId ?? title ?? id;
4298
+ // Get the default display name (without custom name) for comparison
4299
+ const getDefaultDisplayName = React.useCallback(() => {
4300
+ if (!typeId)
4301
+ return title ?? id;
4302
+ const node = ctx.wb.def.nodes.find((n) => n.nodeId === id);
4303
+ if (!node)
4304
+ return id;
4305
+ const desc = ctx.registry.nodes.get(node.typeId);
4306
+ return desc?.displayName || node.typeId;
4307
+ }, [ctx, id, typeId, title]);
4168
4308
  const handleInvalidate = React.useCallback(() => {
4169
4309
  try {
4170
4310
  if (onInvalidate)
@@ -4177,10 +4317,51 @@ function DefaultNodeHeader({ id, title, validation, right, showId, onInvalidate,
4177
4317
  }
4178
4318
  catch { }
4179
4319
  }, [ctx, id, onInvalidate]);
4320
+ const handleDoubleClick = React.useCallback((e) => {
4321
+ // Only allow editing if typeId is provided (enables renaming)
4322
+ if (!typeId)
4323
+ return;
4324
+ e.stopPropagation();
4325
+ setIsEditing(true);
4326
+ setEditValue(displayName);
4327
+ }, [typeId, displayName]);
4328
+ const handleSave = React.useCallback(() => {
4329
+ if (!typeId)
4330
+ return;
4331
+ const trimmed = editValue.trim();
4332
+ const defaultDisplayName = getDefaultDisplayName();
4333
+ // If the trimmed value matches the default display name or typeId, clear the custom name
4334
+ ctx.setNodeName(id, trimmed === defaultDisplayName || trimmed === effectiveTypeId
4335
+ ? undefined
4336
+ : trimmed);
4337
+ setIsEditing(false);
4338
+ }, [ctx, id, editValue, getDefaultDisplayName, effectiveTypeId, typeId]);
4339
+ const handleCancel = React.useCallback(() => {
4340
+ setIsEditing(false);
4341
+ setEditValue(displayName);
4342
+ }, [displayName]);
4343
+ const handleKeyDown = React.useCallback((e) => {
4344
+ if (e.key === "Enter") {
4345
+ e.preventDefault();
4346
+ e.stopPropagation();
4347
+ handleSave();
4348
+ }
4349
+ else if (e.key === "Escape") {
4350
+ e.preventDefault();
4351
+ e.stopPropagation();
4352
+ handleCancel();
4353
+ }
4354
+ }, [handleSave, handleCancel]);
4355
+ React.useEffect(() => {
4356
+ if (isEditing && inputRef.current) {
4357
+ inputRef.current.focus();
4358
+ inputRef.current.select();
4359
+ }
4360
+ }, [isEditing]);
4180
4361
  return (jsxRuntime.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: {
4181
4362
  maxHeight: NODE_HEADER_HEIGHT_PX,
4182
4363
  minHeight: NODE_HEADER_HEIGHT_PX,
4183
- }, children: [jsxRuntime.jsx("strong", { className: "flex-1 h-full text-sm", style: { lineHeight: `${NODE_HEADER_HEIGHT_PX}px` }, children: title }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.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) => {
4364
+ }, children: [isEditing ? (jsxRuntime.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` } })) : (jsxRuntime.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 })), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [jsxRuntime.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) => {
4184
4365
  e.stopPropagation();
4185
4366
  handleInvalidate();
4186
4367
  }, children: jsxRuntime.jsx(react$1.ArrowClockwiseIcon, { size: 10 }) }), right, validation.issues && validation.issues.length > 0 && (jsxRuntime.jsx(IssueBadge, { level: validation.issues.some((i) => i.level === "error")
@@ -4566,10 +4747,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4566
4747
  const { nodeTypes, resolveNodeType } = React.useMemo(() => {
4567
4748
  // Build nodeTypes map using UI extension registry
4568
4749
  const custom = new Map(); // Include all types present in registry AND current graph to avoid timing issues
4569
- const def = wb.export();
4570
4750
  const ids = new Set([
4571
4751
  ...Array.from(registry.nodes.keys()),
4572
- ...def.nodes.map((n) => n.typeId),
4752
+ ...wb.def.nodes.map((n) => n.typeId),
4573
4753
  ]);
4574
4754
  for (const typeId of ids) {
4575
4755
  const renderer = ui.getNodeRenderer(typeId);
@@ -4588,14 +4768,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4588
4768
  // Include uiVersion to recompute when custom renderers are registered
4589
4769
  }, [wb, registry, uiVersion, ui]);
4590
4770
  const { nodes, edges } = React.useMemo(() => {
4591
- const def = wb.export();
4592
4771
  const sel = wb.getSelection();
4593
4772
  // Merge defaults with inputs for node display (defaults shown in lighter gray)
4594
4773
  const inputsWithDefaults = {};
4595
- for (const n of def.nodes) {
4774
+ for (const n of wb.def.nodes) {
4596
4775
  const nodeInputs = inputsMap[n.nodeId] ?? {};
4597
4776
  const nodeDefaults = inputDefaultsMap[n.nodeId] ?? {};
4598
- const inbound = new Set(def.edges
4777
+ const inbound = new Set(wb.def.edges
4599
4778
  .filter((e) => e.target.nodeId === n.nodeId)
4600
4779
  .map((e) => e.target.handle));
4601
4780
  const merged = { ...nodeInputs };
@@ -4608,7 +4787,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4608
4787
  inputsWithDefaults[n.nodeId] = merged;
4609
4788
  }
4610
4789
  }
4611
- const out = toReactFlow(def, wb.getPositions(), registry, {
4790
+ const out = toReactFlow(wb.def, wb.getPositions(), registry, {
4612
4791
  showValues,
4613
4792
  inputs: inputsWithDefaults,
4614
4793
  inputDefaults: inputDefaultsMap,
@@ -5132,11 +5311,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
5132
5311
  });
5133
5312
 
5134
5313
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {
5135
- const { wb, runner, registry, def, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5314
+ const { wb, runner, registry, selectedNodeId, runAutoLayout } = useWorkbenchContext();
5136
5315
  const [transportStatus, setTransportStatus] = React.useState({
5137
5316
  state: "local",
5138
5317
  });
5139
- const selectedNode = def.nodes.find((n) => n.nodeId === selectedNodeId);
5318
+ const selectedNode = wb.def.nodes.find((n) => n.nodeId === selectedNodeId);
5140
5319
  const effectiveHandles = selectedNode
5141
5320
  ? computeEffectiveHandles(selectedNode, registry)
5142
5321
  : { inputs: {}, outputs: {}, inputDefaults: {} };
@@ -5164,7 +5343,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5164
5343
  if (evt.shiftKey && !confirm("Invalidate and re-run graph?"))
5165
5344
  return;
5166
5345
  try {
5167
- runner.launch(wb.export(), {
5346
+ runner.launch(wb.def, {
5168
5347
  engine: kind,
5169
5348
  invalidate: evt.shiftKey,
5170
5349
  });
@@ -5232,7 +5411,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5232
5411
  const setInitialGraph = async (d, inputs) => {
5233
5412
  await wb.load(d);
5234
5413
  try {
5235
- runner.build(wb.export());
5414
+ runner.build(wb.def);
5236
5415
  }
5237
5416
  catch { }
5238
5417
  if (inputs) {
@@ -5247,36 +5426,27 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5247
5426
  React.useEffect(() => {
5248
5427
  if (!onChange)
5249
5428
  return;
5250
- const off1 = wb.on("graphChanged", () => {
5251
- try {
5252
- const cur = wb.export();
5253
- const inputs = runner.getInputs(cur);
5254
- onChange({ def: cur, inputs });
5255
- }
5256
- catch { }
5257
- });
5258
- const off2 = runner.on("value", () => {
5429
+ const offGraphChanged = wb.on("graphChanged", () => {
5259
5430
  try {
5260
- const cur = wb.export();
5431
+ const cur = wb.def;
5261
5432
  const inputs = runner.getInputs(cur);
5262
5433
  onChange({ def: cur, inputs });
5263
5434
  }
5264
5435
  catch { }
5265
5436
  });
5266
- const off3 = wb.on("graphUiChanged", (evt) => {
5437
+ const offGraphUiChanged = wb.on("graphUiChanged", (evt) => {
5267
5438
  if (!evt.commit)
5268
5439
  return;
5269
5440
  try {
5270
- const cur = wb.export();
5441
+ const cur = wb.def;
5271
5442
  const inputs = runner.getInputs(cur);
5272
5443
  onChange({ def: cur, inputs });
5273
5444
  }
5274
5445
  catch { }
5275
5446
  });
5276
5447
  return () => {
5277
- off1();
5278
- off2();
5279
- off3();
5448
+ offGraphChanged();
5449
+ offGraphUiChanged();
5280
5450
  };
5281
5451
  }, [wb, runner, onChange]);
5282
5452
  const applyExample = React.useCallback(async (key) => {
@@ -5299,7 +5469,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5299
5469
  }
5300
5470
  await wb.load(def);
5301
5471
  // Build a local runtime so seeded defaults are visible pre-run
5302
- runner.build(wb.export());
5472
+ runner.build(wb.def);
5303
5473
  // Set initial inputs if provided
5304
5474
  if (inputs) {
5305
5475
  for (const [nodeId, map] of Object.entries(inputs)) {
@@ -5384,11 +5554,10 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5384
5554
  // Only auto-launch for local backend; require explicit Start for remote
5385
5555
  if (backendKind !== "local")
5386
5556
  return;
5387
- const d = wb.export();
5388
- if (!d.nodes || d.nodes.length === 0)
5557
+ if (!wb.def.nodes || wb.def.nodes.length === 0)
5389
5558
  return;
5390
5559
  try {
5391
- runner.launch(d, { engine: engine });
5560
+ runner.launch(wb.def, { engine: engine });
5392
5561
  }
5393
5562
  catch {
5394
5563
  // ignore
@@ -5398,7 +5567,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5398
5567
  if (!selectedNodeId)
5399
5568
  return;
5400
5569
  // If selected input is wired (has inbound edge), ignore user input to respect runtime value
5401
- const isLinked = def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
5570
+ const isLinked = wb.def.edges.some((e) => e.target.nodeId === selectedNodeId && e.target.handle === handle);
5402
5571
  if (isLinked)
5403
5572
  return;
5404
5573
  // If raw is undefined, pass it through to delete the input value
@@ -5483,7 +5652,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
5483
5652
  }
5484
5653
  }
5485
5654
  wb.setInputs(selectedNodeId, { [handle]: value }, { commit: true });
5486
- }, [selectedNodeId, def.edges, effectiveHandles, wb]);
5655
+ }, [selectedNodeId, wb.def.edges, effectiveHandles, wb]);
5487
5656
  const setInput = React.useMemo(() => {
5488
5657
  if (overrides?.setInput) {
5489
5658
  return overrides.setInput(baseSetInput, {