@bian-womp/spark-workbench 0.2.61 → 0.2.63

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 (51) hide show
  1. package/lib/cjs/index.cjs +271 -131
  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/WorkbenchStudio.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +1 -1
  13. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  14. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  15. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +1 -0
  16. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  17. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  18. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +15 -5
  19. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  20. package/lib/cjs/src/runtime/IGraphRunner.d.ts +16 -5
  21. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  22. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts +13 -1
  23. package/lib/cjs/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  24. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +17 -5
  25. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  26. package/lib/esm/index.js +271 -131
  27. package/lib/esm/index.js.map +1 -1
  28. package/lib/esm/src/core/AbstractWorkbench.d.ts +3 -1
  29. package/lib/esm/src/core/AbstractWorkbench.d.ts.map +1 -1
  30. package/lib/esm/src/core/InMemoryWorkbench.d.ts +6 -0
  31. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  32. package/lib/esm/src/core/contracts.d.ts +7 -0
  33. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  34. package/lib/esm/src/misc/NodeContextMenu.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/ContextMenuHandlers.d.ts +1 -1
  38. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  39. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  40. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +1 -0
  41. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  42. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  43. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +15 -5
  44. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  45. package/lib/esm/src/runtime/IGraphRunner.d.ts +16 -5
  46. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  47. package/lib/esm/src/runtime/LocalGraphRunner.d.ts +13 -1
  48. package/lib/esm/src/runtime/LocalGraphRunner.d.ts.map +1 -1
  49. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +17 -5
  50. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  51. package/package.json +4 -4
package/lib/esm/index.js CHANGED
@@ -188,21 +188,26 @@ 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({
195
195
  nodeId: id,
196
196
  typeId: node.typeId,
197
197
  params: node.params,
198
- initialInputs: node.initialInputs,
199
198
  resolvedHandles: node.resolvedHandles,
200
199
  });
201
200
  if (node.position)
202
201
  this.positions[id] = node.position;
203
202
  this.emit("graphChanged", {
204
203
  def: this.def,
205
- 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,
206
211
  });
207
212
  this.refreshValidation();
208
213
  return id;
@@ -217,7 +222,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
217
222
  });
218
223
  this.refreshValidation();
219
224
  }
220
- connect(edge) {
225
+ connect(edge, options) {
221
226
  const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
222
227
  this.def.edges.push({
223
228
  id,
@@ -228,6 +233,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
228
233
  this.emit("graphChanged", {
229
234
  def: this.def,
230
235
  change: { type: "connect", edgeId: id },
236
+ dry: options?.dry,
231
237
  });
232
238
  this.refreshValidation();
233
239
  return id;
@@ -425,35 +431,6 @@ class AbstractGraphRunner {
425
431
  this.stop();
426
432
  }
427
433
  }
428
- triggerExternal(nodeId, event) {
429
- this.engine?.triggerExternal(nodeId, event);
430
- }
431
- // Batch update multiple inputs on a node and trigger a single run
432
- setInputs(nodeId, inputs) {
433
- if (!inputs)
434
- return;
435
- if (!this.stagedInputs[nodeId])
436
- this.stagedInputs[nodeId] = {};
437
- for (const [handle, value] of Object.entries(inputs)) {
438
- if (value === undefined) {
439
- delete this.stagedInputs[nodeId][handle];
440
- }
441
- else {
442
- this.stagedInputs[nodeId][handle] = value;
443
- }
444
- }
445
- if (this.engine) {
446
- // Running: set all inputs
447
- this.engine.setInputs(nodeId, inputs);
448
- }
449
- else {
450
- // Not running: emit a single synthetic value event per handle; UI will coalesce
451
- console.warn("Engine does not exists");
452
- for (const [handle, value] of Object.entries(inputs)) {
453
- this.emit("value", { nodeId, handle, value, io: "input" });
454
- }
455
- }
456
- }
457
434
  async whenIdle() {
458
435
  await this.engine?.whenIdle();
459
436
  }
@@ -494,10 +471,8 @@ class AbstractGraphRunner {
494
471
  const out = {};
495
472
  for (const n of def.nodes) {
496
473
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
497
- const graphDefaults = n.initialInputs ?? {};
498
- const merged = { ...dynDefaults, ...graphDefaults };
499
- if (Object.keys(merged).length > 0) {
500
- out[n.nodeId] = merged;
474
+ if (Object.keys(dynDefaults).length > 0) {
475
+ out[n.nodeId] = dynDefaults;
501
476
  }
502
477
  }
503
478
  return out;
@@ -541,13 +516,22 @@ class LocalGraphRunner extends AbstractGraphRunner {
541
516
  this.setEnvironment = (env, opts) => {
542
517
  if (!this.runtime)
543
518
  return;
544
- if (opts?.merge) {
545
- const current = this.runtime.getEnvironment();
546
- const next = { ...(current || {}), ...(env || {}) };
547
- this.runtime.setEnvironment(next);
519
+ const wasPaused = this.runtime.isPaused();
520
+ if (opts?.dry && !wasPaused)
521
+ this.runtime.pause();
522
+ try {
523
+ if (opts?.merge) {
524
+ const current = this.runtime.getEnvironment();
525
+ const next = { ...(current || {}), ...(env || {}) };
526
+ this.runtime.setEnvironment(next);
527
+ }
528
+ else {
529
+ this.runtime.setEnvironment(env);
530
+ }
548
531
  }
549
- else {
550
- this.runtime.setEnvironment(env);
532
+ finally {
533
+ if (opts?.dry && !wasPaused)
534
+ this.runtime.resume();
551
535
  }
552
536
  };
553
537
  this.getEnvironment = () => {
@@ -565,14 +549,24 @@ class LocalGraphRunner extends AbstractGraphRunner {
565
549
  // Signal UI that freshly built graph should be considered invalidated
566
550
  this.emit("invalidate", { reason: "graph-built" });
567
551
  }
568
- update(def) {
552
+ update(def, options) {
569
553
  if (!this.runtime)
570
554
  return;
571
- // Prevent mid-run churn while wiring changes are applied
572
- this.runtime.pause();
573
- this.runtime.update(def, this.registry);
574
- this.runtime.resume();
575
- this.emit("invalidate", { reason: "graph-updated" });
555
+ const wasPaused = this.runtime.isPaused();
556
+ // Pause runtime if dry option is set (to prevent execution) or if not paused already
557
+ if (options?.dry && !wasPaused) {
558
+ this.runtime.pause();
559
+ }
560
+ try {
561
+ this.runtime.update(def, this.registry);
562
+ this.emit("invalidate", { reason: "graph-updated" });
563
+ }
564
+ finally {
565
+ // Resume only if we paused it due to dry option
566
+ if (options?.dry && !wasPaused) {
567
+ this.runtime.resume();
568
+ }
569
+ }
576
570
  }
577
571
  launch(def, opts) {
578
572
  super.launch(def, opts);
@@ -663,6 +657,70 @@ class LocalGraphRunner extends AbstractGraphRunner {
663
657
  }
664
658
  return out;
665
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
+ }
666
724
  async snapshotFull() {
667
725
  const def = undefined; // UI will supply def/positions on download for local
668
726
  const inputs = this.getInputs(this.runtime
@@ -687,7 +745,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
687
745
  return { def, environment, inputs, outputs };
688
746
  }
689
747
  async applySnapshotFull(payload, options) {
690
- if (payload.def && options?.skipBuild !== true) {
748
+ if (payload.def && !options?.skipBuild) {
691
749
  this.build(payload.def);
692
750
  }
693
751
  this.setEnvironment?.(payload.environment || {}, { merge: false });
@@ -985,7 +1043,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
985
1043
  // Trigger update so validation/UI refreshes using last known graph
986
1044
  try {
987
1045
  if (this.lastDef)
988
- this.update(this.lastDef);
1046
+ await this.update(this.lastDef);
989
1047
  }
990
1048
  catch {
991
1049
  console.error("Failed to update graph definition after registry changed");
@@ -1000,16 +1058,18 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1000
1058
  });
1001
1059
  }
1002
1060
  build(def) { }
1003
- update(def) {
1004
- // Remote: forward update; ignore errors (fire-and-forget)
1005
- this.ensureClient().then(async (client) => {
1006
- try {
1007
- await client.update(def);
1008
- this.emit("invalidate", { reason: "graph-updated" });
1009
- this.lastDef = def;
1010
- }
1011
- catch { }
1012
- });
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
+ }
1013
1073
  }
1014
1074
  launch(def, opts) {
1015
1075
  super.launch(def, opts);
@@ -1130,13 +1190,53 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1130
1190
  const client = await this.ensureClient();
1131
1191
  await client.flush();
1132
1192
  }
1133
- triggerExternal(nodeId, event) {
1134
- this.ensureClient().then(async (client) => {
1135
- try {
1136
- 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];
1137
1200
  }
1138
- catch { }
1139
- });
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
+ }
1140
1240
  }
1141
1241
  async coerce(from, to, value) {
1142
1242
  const client = await this.ensureClient();
@@ -1194,18 +1294,19 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1194
1294
  }
1195
1295
  }
1196
1296
  }
1197
- setEnvironment(env, opts) {
1297
+ async setEnvironment(env, opts) {
1198
1298
  // Use client if available, otherwise ensure client and then set environment
1199
1299
  if (this.client) {
1200
- this.client.setEnvironment(env, opts).catch(() => { });
1300
+ await this.client.setEnvironment(env, opts);
1201
1301
  }
1202
1302
  else {
1203
- // If client not ready yet, ensure it and then set environment
1204
- this.ensureClient()
1205
- .then((client) => {
1206
- client.setEnvironment(env, opts).catch(() => { });
1207
- })
1208
- .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
+ }
1209
1310
  }
1210
1311
  }
1211
1312
  getEnvironment() {
@@ -1266,7 +1367,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1266
1367
  }
1267
1368
  return out;
1268
1369
  }
1269
- dispose() {
1370
+ async dispose() {
1270
1371
  // Idempotent: allow multiple calls safely
1271
1372
  if (this.disposed)
1272
1373
  return;
@@ -1287,9 +1388,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1287
1388
  this.registryFetched = false; // Reset so registry is fetched again on reconnect
1288
1389
  this.registryFetching = false; // Reset fetching state
1289
1390
  if (clientToDispose) {
1290
- clientToDispose.dispose().catch((err) => {
1391
+ try {
1392
+ await clientToDispose.dispose();
1393
+ }
1394
+ catch (err) {
1291
1395
  console.warn("[RemoteGraphRunner] Error disposing client:", err);
1292
- });
1396
+ }
1293
1397
  }
1294
1398
  const disconnectedStatus = {
1295
1399
  state: "disconnected",
@@ -2612,6 +2716,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2612
2716
  }));
2613
2717
  }
2614
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
+ });
2615
2749
  const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
2616
2750
  const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
2617
2751
  setSelectedNodeId(sel.nodes?.[0]);
@@ -2619,13 +2753,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2619
2753
  });
2620
2754
  const offWbError = wb.on("error", add("workbench", "error"));
2621
2755
  // Registry updates: swap registry and refresh graph validation/UI
2622
- const offRunnerRegistry = runner.on("registry", (newReg) => {
2756
+ const offRunnerRegistry = runner.on("registry", async (newReg) => {
2623
2757
  try {
2624
2758
  setRegistry(newReg);
2625
2759
  wb.setRegistry(newReg);
2626
2760
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
2627
2761
  try {
2628
- runner.update(wb.export());
2762
+ await runner.update(wb.export());
2629
2763
  }
2630
2764
  catch {
2631
2765
  console.error("Failed to update graph definition after registry changed");
@@ -2656,6 +2790,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2656
2790
  offWbValidationChanged();
2657
2791
  offWbError();
2658
2792
  offWbAddNode();
2793
+ offWbGraphChangedForUpdate();
2659
2794
  offWbdSetValidation();
2660
2795
  offWbSelectionChanged();
2661
2796
  offRunnerRegistry();
@@ -2673,16 +2808,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2673
2808
  const stop = useCallback(() => runner.stop(), [runner]);
2674
2809
  const step = useCallback(() => runner.step(), [runner]);
2675
2810
  const flush = useCallback(() => runner.flush(), [runner]);
2676
- // Push incremental updates into running engine without full reload
2677
- const isGraphRunning = isRunning();
2678
- useEffect(() => {
2679
- if (isGraphRunning) {
2680
- try {
2681
- runner.update(def);
2682
- }
2683
- catch { }
2684
- }
2685
- }, [runner, isGraphRunning, def, graphTick]);
2686
2811
  const validationByNode = useMemo(() => {
2687
2812
  const inputs = {};
2688
2813
  const outputs = {};
@@ -3434,7 +3559,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3434
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) => {
3435
3560
  e.preventDefault();
3436
3561
  e.stopPropagation();
3437
- }, 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" })] }));
3438
3563
  }
3439
3564
 
3440
3565
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
@@ -3467,26 +3592,20 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3467
3592
  const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3468
3593
  const inType = getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3469
3594
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3470
- const newId = wb.addNode({
3595
+ wb.addNode({
3471
3596
  typeId: singleTarget.nodeTypeId,
3472
3597
  position: { x: pos.x + 180, y: pos.y },
3473
- });
3474
- runner.update(wb.export());
3475
- await runner.whenIdle();
3476
- runner.setInputs(newId, { [singleTarget.inputHandle]: coerced });
3598
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3477
3599
  return;
3478
3600
  }
3479
3601
  if (isArray && arrTarget) {
3480
3602
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3481
3603
  const inType = getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3482
3604
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3483
- const newId = wb.addNode({
3605
+ wb.addNode({
3484
3606
  typeId: arrTarget.nodeTypeId,
3485
3607
  position: { x: pos.x + 180, y: pos.y },
3486
- });
3487
- runner.update(wb.export());
3488
- await runner.whenIdle();
3489
- runner.setInputs(newId, { [arrTarget.inputHandle]: coerced });
3608
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3490
3609
  return;
3491
3610
  }
3492
3611
  if (isArray && elemTarget) {
@@ -3498,24 +3617,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3498
3617
  const COLS = 4;
3499
3618
  const DX = 180;
3500
3619
  const DY = 160;
3501
- const nodeIds = [];
3502
3620
  for (let idx = 0; idx < coercedItems.length; idx++) {
3503
3621
  const col = idx % COLS;
3504
3622
  const row = Math.floor(idx / COLS);
3505
- const newId = wb.addNode({
3623
+ wb.addNode({
3506
3624
  typeId: elemTarget.nodeTypeId,
3507
3625
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3508
- });
3509
- nodeIds.push(newId);
3510
- }
3511
- if (nodeIds.length > 0) {
3512
- runner.update(wb.export());
3513
- await runner.whenIdle();
3514
- for (let idx = 0; idx < coercedItems.length; idx++) {
3515
- runner.setInputs(nodeIds[idx], {
3516
- [elemTarget.inputHandle]: coercedItems[idx],
3517
- });
3518
- }
3626
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3519
3627
  }
3520
3628
  return;
3521
3629
  }
@@ -3533,15 +3641,52 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3533
3641
  if (!n)
3534
3642
  return onClose();
3535
3643
  const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3536
- 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({
3537
3671
  typeId: n.typeId,
3538
3672
  params: n.params,
3539
3673
  position: { x: pos.x + 24, y: pos.y + 24 },
3540
- initialInputs: n.initialInputs,
3541
3674
  resolvedHandles: n.resolvedHandles,
3675
+ }, {
3676
+ inputs,
3677
+ copyOutputsFrom: nodeId,
3678
+ dry: true,
3542
3679
  });
3543
- await runner.whenIdle();
3544
- 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
+ }
3545
3690
  onClose();
3546
3691
  },
3547
3692
  onRunPull: async () => {
@@ -3860,18 +4005,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3860
4005
  setNodeMenuOpen(false);
3861
4006
  }
3862
4007
  };
3863
- const addNodeAt = useCallback(async (typeId, opts) => {
3864
- const nodeId = wb.addNode({
3865
- typeId,
3866
- initialInputs: opts.initialInputs,
3867
- position: opts.position,
3868
- });
3869
- if (opts.inputs) {
3870
- runner.update(wb.export());
3871
- await runner.whenIdle();
3872
- runner.setInputs(nodeId, opts.inputs);
3873
- }
3874
- }, [wb, runner]);
4008
+ const addNodeAt = useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
3875
4009
  const onCloseMenu = useCallback(() => {
3876
4010
  setMenuOpen(false);
3877
4011
  }, []);
@@ -4115,7 +4249,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4115
4249
  const ex = examples.find((e) => e.id === key) ?? examples[0];
4116
4250
  if (!ex)
4117
4251
  return;
4118
- const { registry: r, def } = await ex.load();
4252
+ const { registry: r, def, inputs } = await ex.load();
4119
4253
  // Keep registry consistent with backend:
4120
4254
  // - For local backend, allow example to provide its own registry
4121
4255
  // - For remote backend, registry is automatically managed by RemoteGraphRunner
@@ -4128,6 +4262,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4128
4262
  await wb.load(def);
4129
4263
  // Build a local runtime so seeded defaults are visible pre-run
4130
4264
  runner.build(wb.export());
4265
+ // Set initial inputs if provided
4266
+ if (inputs) {
4267
+ for (const [nodeId, map] of Object.entries(inputs)) {
4268
+ runner.setInputs(nodeId, map);
4269
+ }
4270
+ }
4131
4271
  runAutoLayout();
4132
4272
  setExampleState(key);
4133
4273
  onExampleChange?.(key);