@bian-womp/spark-workbench 0.2.62 → 0.2.64

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 (45) hide show
  1. package/lib/cjs/index.cjs +236 -112
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/AbstractWorkbench.d.ts +3 -1
  4. package/lib/cjs/src/core/AbstractWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +6 -0
  6. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  7. package/lib/cjs/src/core/contracts.d.ts +7 -0
  8. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  11. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +1 -0
  12. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  15. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +12 -9
  16. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  17. package/lib/cjs/src/runtime/IGraphRunner.d.ts +7 -3
  18. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  19. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +9 -0
  20. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  21. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +11 -4
  22. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  23. package/lib/esm/index.js +236 -112
  24. package/lib/esm/index.js.map +1 -1
  25. package/lib/esm/src/core/AbstractWorkbench.d.ts +3 -1
  26. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  27. package/lib/esm/src/core/InMemoryWorkbench.d.ts +6 -0
  28. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  29. package/lib/esm/src/core/contracts.d.ts +7 -0
  30. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  31. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +1 -1
  32. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  33. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +1 -0
  34. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  35. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  36. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  37. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +12 -9
  38. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/IGraphRunner.d.ts +7 -3
  40. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  41. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +9 -0
  42. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  43. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +11 -4
  44. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  45. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -188,7 +188,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
188
188
  }
189
189
  return { ok: issues.every((i) => i.level !== "error"), issues };
190
190
  }
191
- addNode(node) {
191
+ addNode(node, options) {
192
192
  const id = node.nodeId ??
193
193
  this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
194
194
  this.def.nodes.push({
@@ -201,7 +201,13 @@ class InMemoryWorkbench extends AbstractWorkbench {
201
201
  this.positions[id] = node.position;
202
202
  this.emit("graphChanged", {
203
203
  def: this.def,
204
- change: { type: "addNode", nodeId: id },
204
+ change: {
205
+ type: "addNode",
206
+ nodeId: id,
207
+ inputs: options?.inputs,
208
+ copyOutputsFrom: options?.copyOutputsFrom,
209
+ },
210
+ dry: options?.dry,
205
211
  });
206
212
  this.refreshValidation();
207
213
  return id;
@@ -216,7 +222,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
216
222
  });
217
223
  this.refreshValidation();
218
224
  }
219
- connect(edge) {
225
+ connect(edge, options) {
220
226
  const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
221
227
  this.def.edges.push({
222
228
  id,
@@ -227,6 +233,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
227
233
  this.emit("graphChanged", {
228
234
  def: this.def,
229
235
  change: { type: "connect", edgeId: id },
236
+ dry: options?.dry,
230
237
  });
231
238
  this.refreshValidation();
232
239
  return id;
@@ -424,35 +431,6 @@ class AbstractGraphRunner {
424
431
  this.stop();
425
432
  }
426
433
  }
427
- triggerExternal(nodeId, event, options) {
428
- this.engine?.triggerExternal(nodeId, event);
429
- }
430
- // Batch update multiple inputs on a node and trigger a single run
431
- setInputs(nodeId, inputs, options) {
432
- if (!inputs)
433
- return;
434
- if (!this.stagedInputs[nodeId])
435
- this.stagedInputs[nodeId] = {};
436
- for (const [handle, value] of Object.entries(inputs)) {
437
- if (value === undefined) {
438
- delete this.stagedInputs[nodeId][handle];
439
- }
440
- else {
441
- this.stagedInputs[nodeId][handle] = value;
442
- }
443
- }
444
- if (this.engine) {
445
- // Running: set all inputs
446
- this.engine.setInputs(nodeId, inputs);
447
- }
448
- else {
449
- // Not running: emit a single synthetic value event per handle; UI will coalesce
450
- console.warn("Engine does not exists");
451
- for (const [handle, value] of Object.entries(inputs)) {
452
- this.emit("value", { nodeId, handle, value, io: "input" });
453
- }
454
- }
455
- }
456
434
  async whenIdle() {
457
435
  await this.engine?.whenIdle();
458
436
  }
@@ -575,15 +553,19 @@ class LocalGraphRunner extends AbstractGraphRunner {
575
553
  if (!this.runtime)
576
554
  return;
577
555
  const wasPaused = this.runtime.isPaused();
578
- if (!wasPaused)
556
+ // Pause runtime if dry option is set (to prevent execution) or if not paused already
557
+ if (options?.dry && !wasPaused) {
579
558
  this.runtime.pause();
559
+ }
580
560
  try {
581
561
  this.runtime.update(def, this.registry);
582
562
  this.emit("invalidate", { reason: "graph-updated" });
583
563
  }
584
564
  finally {
585
- if (!wasPaused)
565
+ // Resume only if we paused it due to dry option
566
+ if (options?.dry && !wasPaused) {
586
567
  this.runtime.resume();
568
+ }
587
569
  }
588
570
  }
589
571
  launch(def, opts) {
@@ -675,6 +657,70 @@ class LocalGraphRunner extends AbstractGraphRunner {
675
657
  }
676
658
  return out;
677
659
  }
660
+ triggerExternal(nodeId, event, options) {
661
+ // Handle dry option: pause runtime before triggering, resume after
662
+ const wasPaused = this.runtime?.isPaused() ?? false;
663
+ if (options?.dry && !wasPaused && this.runtime) {
664
+ this.runtime.pause();
665
+ }
666
+ try {
667
+ this.engine?.triggerExternal(nodeId, event);
668
+ }
669
+ finally {
670
+ if (options?.dry && !wasPaused && this.runtime) {
671
+ this.runtime.resume();
672
+ }
673
+ }
674
+ }
675
+ // Batch update multiple inputs on a node and trigger a single run
676
+ setInputs(nodeId, inputs, options) {
677
+ if (!inputs)
678
+ return;
679
+ if (!this.stagedInputs[nodeId])
680
+ this.stagedInputs[nodeId] = {};
681
+ for (const [handle, value] of Object.entries(inputs)) {
682
+ if (value === undefined) {
683
+ delete this.stagedInputs[nodeId][handle];
684
+ }
685
+ else {
686
+ this.stagedInputs[nodeId][handle] = value;
687
+ }
688
+ }
689
+ // Handle dry option: pause runtime before setting inputs, resume after
690
+ const wasPaused = this.runtime?.isPaused() ?? false;
691
+ if (options?.dry && !wasPaused && this.runtime) {
692
+ this.runtime.pause();
693
+ }
694
+ try {
695
+ if (this.engine) {
696
+ this.engine.setInputs(nodeId, inputs);
697
+ }
698
+ else {
699
+ // Not running: emit a single synthetic value event per handle; UI will coalesce
700
+ console.warn("Engine does not exists");
701
+ for (const [handle, value] of Object.entries(inputs)) {
702
+ this.emit("value", { nodeId, handle, value, io: "input" });
703
+ }
704
+ }
705
+ }
706
+ finally {
707
+ if (options?.dry && !wasPaused && this.runtime) {
708
+ this.runtime.resume();
709
+ }
710
+ }
711
+ }
712
+ copyOutputs(fromNodeId, toNodeId, options) {
713
+ if (!this.runtime)
714
+ return;
715
+ // Get outputs from source node
716
+ const fromNode = this.runtime.getNodeData(fromNodeId);
717
+ if (!fromNode?.outputs)
718
+ return;
719
+ // Copy outputs to target node using hydrate
720
+ // hydrate already pauses internally, so we don't need to handle dry option here
721
+ // reemit: !options?.dry means don't propagate downstream if dry mode
722
+ this.runtime.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { reemit: !options?.dry });
723
+ }
678
724
  async snapshotFull() {
679
725
  const def = undefined; // UI will supply def/positions on download for local
680
726
  const inputs = this.getInputs(this.runtime
@@ -997,7 +1043,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
997
1043
  // Trigger update so validation/UI refreshes using last known graph
998
1044
  try {
999
1045
  if (this.lastDef)
1000
- this.update(this.lastDef);
1046
+ await this.update(this.lastDef);
1001
1047
  }
1002
1048
  catch {
1003
1049
  console.error("Failed to update graph definition after registry changed");
@@ -1012,16 +1058,18 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1012
1058
  });
1013
1059
  }
1014
1060
  build(def) { }
1015
- update(def, options) {
1016
- // Remote: forward update; ignore errors (fire-and-forget)
1017
- this.ensureClient().then(async (client) => {
1018
- try {
1019
- await client.update(def, options);
1020
- this.emit("invalidate", { reason: "graph-updated" });
1021
- this.lastDef = def;
1022
- }
1023
- catch { }
1024
- });
1061
+ async update(def, options) {
1062
+ // Remote: forward update and await completion
1063
+ const client = await this.ensureClient();
1064
+ try {
1065
+ await client.update(def, options);
1066
+ this.emit("invalidate", { reason: "graph-updated" });
1067
+ this.lastDef = def;
1068
+ }
1069
+ catch (err) {
1070
+ // Log error but don't throw to maintain compatibility
1071
+ console.error("[RemoteGraphRunner] Error updating graph:", err);
1072
+ }
1025
1073
  }
1026
1074
  launch(def, opts) {
1027
1075
  super.launch(def, opts);
@@ -1142,13 +1190,53 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1142
1190
  const client = await this.ensureClient();
1143
1191
  await client.flush();
1144
1192
  }
1145
- triggerExternal(nodeId, event, options) {
1146
- this.ensureClient().then(async (client) => {
1147
- try {
1148
- await client.getEngine().triggerExternal(nodeId, event);
1193
+ setInputs(nodeId, inputs, options) {
1194
+ // Update staged inputs (for getInputs to work correctly)
1195
+ if (!this.stagedInputs[nodeId])
1196
+ this.stagedInputs[nodeId] = {};
1197
+ for (const [handle, value] of Object.entries(inputs)) {
1198
+ if (value === undefined) {
1199
+ delete this.stagedInputs[nodeId][handle];
1149
1200
  }
1150
- catch { }
1151
- });
1201
+ else {
1202
+ this.stagedInputs[nodeId][handle] = value;
1203
+ }
1204
+ }
1205
+ // If engine exists, call directly; otherwise ensure client (fire-and-forget)
1206
+ if (this.engine) {
1207
+ this.engine.setInputs(nodeId, inputs, options);
1208
+ }
1209
+ else {
1210
+ this.ensureClient()
1211
+ .then((client) => {
1212
+ client.getEngine().setInputs(nodeId, inputs, options);
1213
+ })
1214
+ .catch(() => {
1215
+ // Emit synthetic events if connection fails
1216
+ for (const [handle, value] of Object.entries(inputs)) {
1217
+ this.emit("value", { nodeId, handle, value, io: "input" });
1218
+ }
1219
+ });
1220
+ }
1221
+ }
1222
+ async copyOutputs(fromNodeId, toNodeId, options) {
1223
+ const client = await this.ensureClient();
1224
+ await client.copyOutputs(fromNodeId, toNodeId, options);
1225
+ }
1226
+ triggerExternal(nodeId, event, options) {
1227
+ // If engine exists, call directly; otherwise ensure client (fire-and-forget)
1228
+ if (this.engine) {
1229
+ this.engine.triggerExternal(nodeId, event, options);
1230
+ }
1231
+ else {
1232
+ this.ensureClient()
1233
+ .then((client) => {
1234
+ client.getEngine().triggerExternal(nodeId, event, options);
1235
+ })
1236
+ .catch(() => {
1237
+ // Silently fail if connection not available
1238
+ });
1239
+ }
1152
1240
  }
1153
1241
  async coerce(from, to, value) {
1154
1242
  const client = await this.ensureClient();
@@ -1206,18 +1294,19 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1206
1294
  }
1207
1295
  }
1208
1296
  }
1209
- setEnvironment(env, opts) {
1297
+ async setEnvironment(env, opts) {
1210
1298
  // Use client if available, otherwise ensure client and then set environment
1211
1299
  if (this.client) {
1212
- this.client.setEnvironment(env, opts).catch(() => { });
1300
+ await this.client.setEnvironment(env, opts);
1213
1301
  }
1214
1302
  else {
1215
- // If client not ready yet, ensure it and then set environment
1216
- this.ensureClient()
1217
- .then((client) => {
1218
- client.setEnvironment(env, opts).catch(() => { });
1219
- })
1220
- .catch(() => { });
1303
+ try {
1304
+ const client = await this.ensureClient();
1305
+ await client.setEnvironment(env, opts);
1306
+ }
1307
+ catch {
1308
+ // Silently fail if connection not available
1309
+ }
1221
1310
  }
1222
1311
  }
1223
1312
  getEnvironment() {
@@ -1278,7 +1367,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1278
1367
  }
1279
1368
  return out;
1280
1369
  }
1281
- dispose() {
1370
+ async dispose() {
1282
1371
  // Idempotent: allow multiple calls safely
1283
1372
  if (this.disposed)
1284
1373
  return;
@@ -1299,9 +1388,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1299
1388
  this.registryFetched = false; // Reset so registry is fetched again on reconnect
1300
1389
  this.registryFetching = false; // Reset fetching state
1301
1390
  if (clientToDispose) {
1302
- clientToDispose.dispose().catch((err) => {
1391
+ try {
1392
+ await clientToDispose.dispose();
1393
+ }
1394
+ catch (err) {
1303
1395
  console.warn("[RemoteGraphRunner] Error disposing client:", err);
1304
- });
1396
+ }
1305
1397
  }
1306
1398
  const disconnectedStatus = {
1307
1399
  state: "disconnected",
@@ -2624,6 +2716,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2624
2716
  }));
2625
2717
  }
2626
2718
  });
2719
+ const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
2720
+ if (!runner.isRunning())
2721
+ return;
2722
+ try {
2723
+ if (event.change?.type === "addNode") {
2724
+ const { nodeId, inputs, copyOutputsFrom } = event.change;
2725
+ if (event.dry) {
2726
+ await runner.update(event.def, { dry: true });
2727
+ if (inputs) {
2728
+ runner.setInputs(nodeId, inputs, { dry: true });
2729
+ }
2730
+ if (copyOutputsFrom) {
2731
+ runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
2732
+ }
2733
+ }
2734
+ else {
2735
+ await runner.update(event.def, { dry: !!inputs });
2736
+ if (inputs) {
2737
+ runner.setInputs(nodeId, inputs, { dry: false });
2738
+ }
2739
+ }
2740
+ }
2741
+ else {
2742
+ await runner.update(event.def, { dry: event.dry });
2743
+ }
2744
+ }
2745
+ catch (err) {
2746
+ console.error("[WorkbenchContext] Error updating graph:", err);
2747
+ }
2748
+ });
2627
2749
  const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
2628
2750
  const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
2629
2751
  setSelectedNodeId(sel.nodes?.[0]);
@@ -2631,13 +2753,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2631
2753
  });
2632
2754
  const offWbError = wb.on("error", add("workbench", "error"));
2633
2755
  // Registry updates: swap registry and refresh graph validation/UI
2634
- const offRunnerRegistry = runner.on("registry", (newReg) => {
2756
+ const offRunnerRegistry = runner.on("registry", async (newReg) => {
2635
2757
  try {
2636
2758
  setRegistry(newReg);
2637
2759
  wb.setRegistry(newReg);
2638
2760
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
2639
2761
  try {
2640
- runner.update(wb.export());
2762
+ await runner.update(wb.export());
2641
2763
  }
2642
2764
  catch {
2643
2765
  console.error("Failed to update graph definition after registry changed");
@@ -2668,6 +2790,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2668
2790
  offWbValidationChanged();
2669
2791
  offWbError();
2670
2792
  offWbAddNode();
2793
+ offWbGraphChangedForUpdate();
2671
2794
  offWbdSetValidation();
2672
2795
  offWbSelectionChanged();
2673
2796
  offRunnerRegistry();
@@ -2685,16 +2808,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2685
2808
  const stop = useCallback(() => runner.stop(), [runner]);
2686
2809
  const step = useCallback(() => runner.step(), [runner]);
2687
2810
  const flush = useCallback(() => runner.flush(), [runner]);
2688
- // Push incremental updates into running engine without full reload
2689
- const isGraphRunning = isRunning();
2690
- useEffect(() => {
2691
- if (isGraphRunning) {
2692
- try {
2693
- runner.update(def);
2694
- }
2695
- catch { }
2696
- }
2697
- }, [runner, isGraphRunning, def, graphTick]);
2698
2811
  const validationByNode = useMemo(() => {
2699
2812
  const inputs = {};
2700
2813
  const outputs = {};
@@ -3446,7 +3559,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3446
3559
  return (jsxs("div", { ref: ref, tabIndex: -1, className: "fixed z-[1000] bg-white border border-gray-300 rounded-lg shadow-lg p-1 min-w-[180px] text-sm text-gray-700", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3447
3560
  e.preventDefault();
3448
3561
  e.stopPropagation();
3449
- }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3562
+ }, children: [jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxs(Fragment, { children: [jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3450
3563
  }
3451
3564
 
3452
3565
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
@@ -3479,26 +3592,20 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3479
3592
  const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3480
3593
  const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3481
3594
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3482
- const newId = wb.addNode({
3595
+ wb.addNode({
3483
3596
  typeId: singleTarget.nodeTypeId,
3484
3597
  position: { x: pos.x + 180, y: pos.y },
3485
- });
3486
- runner.update(wb.export());
3487
- await runner.whenIdle();
3488
- runner.setInputs(newId, { [singleTarget.inputHandle]: coerced });
3598
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3489
3599
  return;
3490
3600
  }
3491
3601
  if (isArray && arrTarget) {
3492
3602
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3493
3603
  const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3494
3604
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3495
- const newId = wb.addNode({
3605
+ wb.addNode({
3496
3606
  typeId: arrTarget.nodeTypeId,
3497
3607
  position: { x: pos.x + 180, y: pos.y },
3498
- });
3499
- runner.update(wb.export());
3500
- await runner.whenIdle();
3501
- runner.setInputs(newId, { [arrTarget.inputHandle]: coerced });
3608
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3502
3609
  return;
3503
3610
  }
3504
3611
  if (isArray && elemTarget) {
@@ -3510,24 +3617,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3510
3617
  const COLS = 4;
3511
3618
  const DX = 180;
3512
3619
  const DY = 160;
3513
- const nodeIds = [];
3514
3620
  for (let idx = 0; idx < coercedItems.length; idx++) {
3515
3621
  const col = idx % COLS;
3516
3622
  const row = Math.floor(idx / COLS);
3517
- const newId = wb.addNode({
3623
+ wb.addNode({
3518
3624
  typeId: elemTarget.nodeTypeId,
3519
3625
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3520
- });
3521
- nodeIds.push(newId);
3522
- }
3523
- if (nodeIds.length > 0) {
3524
- runner.update(wb.export());
3525
- await runner.whenIdle();
3526
- for (let idx = 0; idx < coercedItems.length; idx++) {
3527
- runner.setInputs(nodeIds[idx], {
3528
- [elemTarget.inputHandle]: coercedItems[idx],
3529
- });
3530
- }
3626
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3531
3627
  }
3532
3628
  return;
3533
3629
  }
@@ -3545,14 +3641,52 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3545
3641
  if (!n)
3546
3642
  return onClose();
3547
3643
  const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3548
- const newId = wb.addNode({
3644
+ const inboundHandles = new Set(def.edges
3645
+ .filter((e) => e.target.nodeId === nodeId)
3646
+ .map((e) => e.target.handle));
3647
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3648
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3649
+ wb.addNode({
3650
+ typeId: n.typeId,
3651
+ params: n.params,
3652
+ position: { x: pos.x + 24, y: pos.y + 24 },
3653
+ resolvedHandles: n.resolvedHandles,
3654
+ }, {
3655
+ inputs: inputsWithoutBindings,
3656
+ copyOutputsFrom: nodeId,
3657
+ dry: true,
3658
+ });
3659
+ onClose();
3660
+ },
3661
+ onDuplicateWithEdges: async () => {
3662
+ const def = wb.export();
3663
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3664
+ if (!n)
3665
+ return onClose();
3666
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3667
+ // Get inputs without bindings (literal values only)
3668
+ const inputs = runner.getInputs(def)[nodeId] || {};
3669
+ // Add the duplicated node
3670
+ const newNodeId = wb.addNode({
3549
3671
  typeId: n.typeId,
3550
3672
  params: n.params,
3551
3673
  position: { x: pos.x + 24, y: pos.y + 24 },
3552
3674
  resolvedHandles: n.resolvedHandles,
3675
+ }, {
3676
+ inputs,
3677
+ copyOutputsFrom: nodeId,
3678
+ dry: true,
3553
3679
  });
3554
- await runner.whenIdle();
3555
- runner.setInputs(newId, { ...runner.getInputs(def)[nodeId] });
3680
+ // Find all incoming edges (edges where target is the original node)
3681
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3682
+ // Duplicate each incoming edge to point to the new node
3683
+ for (const edge of incomingEdges) {
3684
+ wb.connect({
3685
+ source: edge.source, // Keep the same source
3686
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3687
+ typeId: edge.typeId,
3688
+ }, { dry: true });
3689
+ }
3556
3690
  onClose();
3557
3691
  },
3558
3692
  onRunPull: async () => {
@@ -3871,17 +4005,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3871
4005
  setNodeMenuOpen(false);
3872
4006
  }
3873
4007
  };
3874
- const addNodeAt = useCallback(async (typeId, opts) => {
3875
- const nodeId = wb.addNode({
3876
- typeId,
3877
- position: opts.position,
3878
- });
3879
- if (opts.inputs) {
3880
- runner.update(wb.export());
3881
- await runner.whenIdle();
3882
- runner.setInputs(nodeId, opts.inputs);
3883
- }
3884
- }, [wb, runner]);
4008
+ const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
3885
4009
  const onCloseMenu = useCallback(() => {
3886
4010
  setMenuOpen(false);
3887
4011
  }, []);