@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/cjs/index.cjs CHANGED
@@ -190,21 +190,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
190
190
  }
191
191
  return { ok: issues.every((i) => i.level !== "error"), issues };
192
192
  }
193
- addNode(node) {
193
+ addNode(node, options) {
194
194
  const id = node.nodeId ??
195
195
  this.genId("n", new Set(this.def.nodes.map((n) => n.nodeId)));
196
196
  this.def.nodes.push({
197
197
  nodeId: id,
198
198
  typeId: node.typeId,
199
199
  params: node.params,
200
- initialInputs: node.initialInputs,
201
200
  resolvedHandles: node.resolvedHandles,
202
201
  });
203
202
  if (node.position)
204
203
  this.positions[id] = node.position;
205
204
  this.emit("graphChanged", {
206
205
  def: this.def,
207
- change: { type: "addNode", nodeId: id },
206
+ change: {
207
+ type: "addNode",
208
+ nodeId: id,
209
+ inputs: options?.inputs,
210
+ copyOutputsFrom: options?.copyOutputsFrom,
211
+ },
212
+ dry: options?.dry,
208
213
  });
209
214
  this.refreshValidation();
210
215
  return id;
@@ -219,7 +224,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
219
224
  });
220
225
  this.refreshValidation();
221
226
  }
222
- connect(edge) {
227
+ connect(edge, options) {
223
228
  const id = edge.id ?? this.genId("e", new Set(this.def.edges.map((e) => e.id)));
224
229
  this.def.edges.push({
225
230
  id,
@@ -230,6 +235,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
230
235
  this.emit("graphChanged", {
231
236
  def: this.def,
232
237
  change: { type: "connect", edgeId: id },
238
+ dry: options?.dry,
233
239
  });
234
240
  this.refreshValidation();
235
241
  return id;
@@ -427,35 +433,6 @@ class AbstractGraphRunner {
427
433
  this.stop();
428
434
  }
429
435
  }
430
- triggerExternal(nodeId, event) {
431
- this.engine?.triggerExternal(nodeId, event);
432
- }
433
- // Batch update multiple inputs on a node and trigger a single run
434
- setInputs(nodeId, inputs) {
435
- if (!inputs)
436
- return;
437
- if (!this.stagedInputs[nodeId])
438
- this.stagedInputs[nodeId] = {};
439
- for (const [handle, value] of Object.entries(inputs)) {
440
- if (value === undefined) {
441
- delete this.stagedInputs[nodeId][handle];
442
- }
443
- else {
444
- this.stagedInputs[nodeId][handle] = value;
445
- }
446
- }
447
- if (this.engine) {
448
- // Running: set all inputs
449
- this.engine.setInputs(nodeId, inputs);
450
- }
451
- else {
452
- // Not running: emit a single synthetic value event per handle; UI will coalesce
453
- console.warn("Engine does not exists");
454
- for (const [handle, value] of Object.entries(inputs)) {
455
- this.emit("value", { nodeId, handle, value, io: "input" });
456
- }
457
- }
458
- }
459
436
  async whenIdle() {
460
437
  await this.engine?.whenIdle();
461
438
  }
@@ -496,10 +473,8 @@ class AbstractGraphRunner {
496
473
  const out = {};
497
474
  for (const n of def.nodes) {
498
475
  const dynDefaults = n.resolvedHandles?.inputDefaults ?? {};
499
- const graphDefaults = n.initialInputs ?? {};
500
- const merged = { ...dynDefaults, ...graphDefaults };
501
- if (Object.keys(merged).length > 0) {
502
- out[n.nodeId] = merged;
476
+ if (Object.keys(dynDefaults).length > 0) {
477
+ out[n.nodeId] = dynDefaults;
503
478
  }
504
479
  }
505
480
  return out;
@@ -543,13 +518,22 @@ class LocalGraphRunner extends AbstractGraphRunner {
543
518
  this.setEnvironment = (env, opts) => {
544
519
  if (!this.runtime)
545
520
  return;
546
- if (opts?.merge) {
547
- const current = this.runtime.getEnvironment();
548
- const next = { ...(current || {}), ...(env || {}) };
549
- this.runtime.setEnvironment(next);
521
+ const wasPaused = this.runtime.isPaused();
522
+ if (opts?.dry && !wasPaused)
523
+ this.runtime.pause();
524
+ try {
525
+ if (opts?.merge) {
526
+ const current = this.runtime.getEnvironment();
527
+ const next = { ...(current || {}), ...(env || {}) };
528
+ this.runtime.setEnvironment(next);
529
+ }
530
+ else {
531
+ this.runtime.setEnvironment(env);
532
+ }
550
533
  }
551
- else {
552
- this.runtime.setEnvironment(env);
534
+ finally {
535
+ if (opts?.dry && !wasPaused)
536
+ this.runtime.resume();
553
537
  }
554
538
  };
555
539
  this.getEnvironment = () => {
@@ -567,14 +551,24 @@ class LocalGraphRunner extends AbstractGraphRunner {
567
551
  // Signal UI that freshly built graph should be considered invalidated
568
552
  this.emit("invalidate", { reason: "graph-built" });
569
553
  }
570
- update(def) {
554
+ update(def, options) {
571
555
  if (!this.runtime)
572
556
  return;
573
- // Prevent mid-run churn while wiring changes are applied
574
- this.runtime.pause();
575
- this.runtime.update(def, this.registry);
576
- this.runtime.resume();
577
- this.emit("invalidate", { reason: "graph-updated" });
557
+ const wasPaused = this.runtime.isPaused();
558
+ // Pause runtime if dry option is set (to prevent execution) or if not paused already
559
+ if (options?.dry && !wasPaused) {
560
+ this.runtime.pause();
561
+ }
562
+ try {
563
+ this.runtime.update(def, this.registry);
564
+ this.emit("invalidate", { reason: "graph-updated" });
565
+ }
566
+ finally {
567
+ // Resume only if we paused it due to dry option
568
+ if (options?.dry && !wasPaused) {
569
+ this.runtime.resume();
570
+ }
571
+ }
578
572
  }
579
573
  launch(def, opts) {
580
574
  super.launch(def, opts);
@@ -665,6 +659,70 @@ class LocalGraphRunner extends AbstractGraphRunner {
665
659
  }
666
660
  return out;
667
661
  }
662
+ triggerExternal(nodeId, event, options) {
663
+ // Handle dry option: pause runtime before triggering, resume after
664
+ const wasPaused = this.runtime?.isPaused() ?? false;
665
+ if (options?.dry && !wasPaused && this.runtime) {
666
+ this.runtime.pause();
667
+ }
668
+ try {
669
+ this.engine?.triggerExternal(nodeId, event);
670
+ }
671
+ finally {
672
+ if (options?.dry && !wasPaused && this.runtime) {
673
+ this.runtime.resume();
674
+ }
675
+ }
676
+ }
677
+ // Batch update multiple inputs on a node and trigger a single run
678
+ setInputs(nodeId, inputs, options) {
679
+ if (!inputs)
680
+ return;
681
+ if (!this.stagedInputs[nodeId])
682
+ this.stagedInputs[nodeId] = {};
683
+ for (const [handle, value] of Object.entries(inputs)) {
684
+ if (value === undefined) {
685
+ delete this.stagedInputs[nodeId][handle];
686
+ }
687
+ else {
688
+ this.stagedInputs[nodeId][handle] = value;
689
+ }
690
+ }
691
+ // Handle dry option: pause runtime before setting inputs, resume after
692
+ const wasPaused = this.runtime?.isPaused() ?? false;
693
+ if (options?.dry && !wasPaused && this.runtime) {
694
+ this.runtime.pause();
695
+ }
696
+ try {
697
+ if (this.engine) {
698
+ this.engine.setInputs(nodeId, inputs);
699
+ }
700
+ else {
701
+ // Not running: emit a single synthetic value event per handle; UI will coalesce
702
+ console.warn("Engine does not exists");
703
+ for (const [handle, value] of Object.entries(inputs)) {
704
+ this.emit("value", { nodeId, handle, value, io: "input" });
705
+ }
706
+ }
707
+ }
708
+ finally {
709
+ if (options?.dry && !wasPaused && this.runtime) {
710
+ this.runtime.resume();
711
+ }
712
+ }
713
+ }
714
+ copyOutputs(fromNodeId, toNodeId, options) {
715
+ if (!this.runtime)
716
+ return;
717
+ // Get outputs from source node
718
+ const fromNode = this.runtime.getNodeData(fromNodeId);
719
+ if (!fromNode?.outputs)
720
+ return;
721
+ // Copy outputs to target node using hydrate
722
+ // hydrate already pauses internally, so we don't need to handle dry option here
723
+ // reemit: !options?.dry means don't propagate downstream if dry mode
724
+ this.runtime.hydrate({ outputs: { [toNodeId]: { ...fromNode.outputs } } }, { reemit: !options?.dry });
725
+ }
668
726
  async snapshotFull() {
669
727
  const def = undefined; // UI will supply def/positions on download for local
670
728
  const inputs = this.getInputs(this.runtime
@@ -689,7 +747,7 @@ class LocalGraphRunner extends AbstractGraphRunner {
689
747
  return { def, environment, inputs, outputs };
690
748
  }
691
749
  async applySnapshotFull(payload, options) {
692
- if (payload.def && options?.skipBuild !== true) {
750
+ if (payload.def && !options?.skipBuild) {
693
751
  this.build(payload.def);
694
752
  }
695
753
  this.setEnvironment?.(payload.environment || {}, { merge: false });
@@ -987,7 +1045,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
987
1045
  // Trigger update so validation/UI refreshes using last known graph
988
1046
  try {
989
1047
  if (this.lastDef)
990
- this.update(this.lastDef);
1048
+ await this.update(this.lastDef);
991
1049
  }
992
1050
  catch {
993
1051
  console.error("Failed to update graph definition after registry changed");
@@ -1002,16 +1060,18 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1002
1060
  });
1003
1061
  }
1004
1062
  build(def) { }
1005
- update(def) {
1006
- // Remote: forward update; ignore errors (fire-and-forget)
1007
- this.ensureClient().then(async (client) => {
1008
- try {
1009
- await client.update(def);
1010
- this.emit("invalidate", { reason: "graph-updated" });
1011
- this.lastDef = def;
1012
- }
1013
- catch { }
1014
- });
1063
+ async update(def, options) {
1064
+ // Remote: forward update and await completion
1065
+ const client = await this.ensureClient();
1066
+ try {
1067
+ await client.update(def, options);
1068
+ this.emit("invalidate", { reason: "graph-updated" });
1069
+ this.lastDef = def;
1070
+ }
1071
+ catch (err) {
1072
+ // Log error but don't throw to maintain compatibility
1073
+ console.error("[RemoteGraphRunner] Error updating graph:", err);
1074
+ }
1015
1075
  }
1016
1076
  launch(def, opts) {
1017
1077
  super.launch(def, opts);
@@ -1132,13 +1192,53 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1132
1192
  const client = await this.ensureClient();
1133
1193
  await client.flush();
1134
1194
  }
1135
- triggerExternal(nodeId, event) {
1136
- this.ensureClient().then(async (client) => {
1137
- try {
1138
- await client.getEngine().triggerExternal(nodeId, event);
1195
+ setInputs(nodeId, inputs, options) {
1196
+ // Update staged inputs (for getInputs to work correctly)
1197
+ if (!this.stagedInputs[nodeId])
1198
+ this.stagedInputs[nodeId] = {};
1199
+ for (const [handle, value] of Object.entries(inputs)) {
1200
+ if (value === undefined) {
1201
+ delete this.stagedInputs[nodeId][handle];
1139
1202
  }
1140
- catch { }
1141
- });
1203
+ else {
1204
+ this.stagedInputs[nodeId][handle] = value;
1205
+ }
1206
+ }
1207
+ // If engine exists, call directly; otherwise ensure client (fire-and-forget)
1208
+ if (this.engine) {
1209
+ this.engine.setInputs(nodeId, inputs, options);
1210
+ }
1211
+ else {
1212
+ this.ensureClient()
1213
+ .then((client) => {
1214
+ client.getEngine().setInputs(nodeId, inputs, options);
1215
+ })
1216
+ .catch(() => {
1217
+ // Emit synthetic events if connection fails
1218
+ for (const [handle, value] of Object.entries(inputs)) {
1219
+ this.emit("value", { nodeId, handle, value, io: "input" });
1220
+ }
1221
+ });
1222
+ }
1223
+ }
1224
+ async copyOutputs(fromNodeId, toNodeId, options) {
1225
+ const client = await this.ensureClient();
1226
+ await client.copyOutputs(fromNodeId, toNodeId, options);
1227
+ }
1228
+ triggerExternal(nodeId, event, options) {
1229
+ // If engine exists, call directly; otherwise ensure client (fire-and-forget)
1230
+ if (this.engine) {
1231
+ this.engine.triggerExternal(nodeId, event, options);
1232
+ }
1233
+ else {
1234
+ this.ensureClient()
1235
+ .then((client) => {
1236
+ client.getEngine().triggerExternal(nodeId, event, options);
1237
+ })
1238
+ .catch(() => {
1239
+ // Silently fail if connection not available
1240
+ });
1241
+ }
1142
1242
  }
1143
1243
  async coerce(from, to, value) {
1144
1244
  const client = await this.ensureClient();
@@ -1196,18 +1296,19 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1196
1296
  }
1197
1297
  }
1198
1298
  }
1199
- setEnvironment(env, opts) {
1299
+ async setEnvironment(env, opts) {
1200
1300
  // Use client if available, otherwise ensure client and then set environment
1201
1301
  if (this.client) {
1202
- this.client.setEnvironment(env, opts).catch(() => { });
1302
+ await this.client.setEnvironment(env, opts);
1203
1303
  }
1204
1304
  else {
1205
- // If client not ready yet, ensure it and then set environment
1206
- this.ensureClient()
1207
- .then((client) => {
1208
- client.setEnvironment(env, opts).catch(() => { });
1209
- })
1210
- .catch(() => { });
1305
+ try {
1306
+ const client = await this.ensureClient();
1307
+ await client.setEnvironment(env, opts);
1308
+ }
1309
+ catch {
1310
+ // Silently fail if connection not available
1311
+ }
1211
1312
  }
1212
1313
  }
1213
1314
  getEnvironment() {
@@ -1268,7 +1369,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1268
1369
  }
1269
1370
  return out;
1270
1371
  }
1271
- dispose() {
1372
+ async dispose() {
1272
1373
  // Idempotent: allow multiple calls safely
1273
1374
  if (this.disposed)
1274
1375
  return;
@@ -1289,9 +1390,12 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1289
1390
  this.registryFetched = false; // Reset so registry is fetched again on reconnect
1290
1391
  this.registryFetching = false; // Reset fetching state
1291
1392
  if (clientToDispose) {
1292
- clientToDispose.dispose().catch((err) => {
1393
+ try {
1394
+ await clientToDispose.dispose();
1395
+ }
1396
+ catch (err) {
1293
1397
  console.warn("[RemoteGraphRunner] Error disposing client:", err);
1294
- });
1398
+ }
1295
1399
  }
1296
1400
  const disconnectedStatus = {
1297
1401
  state: "disconnected",
@@ -2614,6 +2718,36 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2614
2718
  }));
2615
2719
  }
2616
2720
  });
2721
+ const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
2722
+ if (!runner.isRunning())
2723
+ return;
2724
+ try {
2725
+ if (event.change?.type === "addNode") {
2726
+ const { nodeId, inputs, copyOutputsFrom } = event.change;
2727
+ if (event.dry) {
2728
+ await runner.update(event.def, { dry: true });
2729
+ if (inputs) {
2730
+ runner.setInputs(nodeId, inputs, { dry: true });
2731
+ }
2732
+ if (copyOutputsFrom) {
2733
+ runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
2734
+ }
2735
+ }
2736
+ else {
2737
+ await runner.update(event.def, { dry: !!inputs });
2738
+ if (inputs) {
2739
+ runner.setInputs(nodeId, inputs, { dry: false });
2740
+ }
2741
+ }
2742
+ }
2743
+ else {
2744
+ await runner.update(event.def, { dry: event.dry });
2745
+ }
2746
+ }
2747
+ catch (err) {
2748
+ console.error("[WorkbenchContext] Error updating graph:", err);
2749
+ }
2750
+ });
2617
2751
  const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
2618
2752
  const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
2619
2753
  setSelectedNodeId(sel.nodes?.[0]);
@@ -2621,13 +2755,13 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2621
2755
  });
2622
2756
  const offWbError = wb.on("error", add("workbench", "error"));
2623
2757
  // Registry updates: swap registry and refresh graph validation/UI
2624
- const offRunnerRegistry = runner.on("registry", (newReg) => {
2758
+ const offRunnerRegistry = runner.on("registry", async (newReg) => {
2625
2759
  try {
2626
2760
  setRegistry(newReg);
2627
2761
  wb.setRegistry(newReg);
2628
2762
  // Trigger a graph update so the UI revalidates with new types/enums/nodes
2629
2763
  try {
2630
- runner.update(wb.export());
2764
+ await runner.update(wb.export());
2631
2765
  }
2632
2766
  catch {
2633
2767
  console.error("Failed to update graph definition after registry changed");
@@ -2658,6 +2792,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2658
2792
  offWbValidationChanged();
2659
2793
  offWbError();
2660
2794
  offWbAddNode();
2795
+ offWbGraphChangedForUpdate();
2661
2796
  offWbdSetValidation();
2662
2797
  offWbSelectionChanged();
2663
2798
  offRunnerRegistry();
@@ -2675,16 +2810,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2675
2810
  const stop = React.useCallback(() => runner.stop(), [runner]);
2676
2811
  const step = React.useCallback(() => runner.step(), [runner]);
2677
2812
  const flush = React.useCallback(() => runner.flush(), [runner]);
2678
- // Push incremental updates into running engine without full reload
2679
- const isGraphRunning = isRunning();
2680
- React.useEffect(() => {
2681
- if (isGraphRunning) {
2682
- try {
2683
- runner.update(def);
2684
- }
2685
- catch { }
2686
- }
2687
- }, [runner, isGraphRunning, def, graphTick]);
2688
2813
  const validationByNode = React.useMemo(() => {
2689
2814
  const inputs = {};
2690
2815
  const outputs = {};
@@ -3436,7 +3561,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3436
3561
  return (jsxRuntime.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) => {
3437
3562
  e.preventDefault();
3438
3563
  e.stopPropagation();
3439
- }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3564
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicate, children: "Duplicate" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDuplicateWithEdges, children: "Duplicate with edges" }), canRunPull && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onRunPull, children: "Run (pull)" })), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Bake" }), bakeableOutputs.map((h) => (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: () => handlers.onBake(h), children: ["Bake: ", h] }, h))), jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })] })), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" })] }));
3440
3565
  }
3441
3566
 
3442
3567
  function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap, outputTypesMap, onClose) {
@@ -3469,26 +3594,20 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3469
3594
  const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3470
3595
  const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3471
3596
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3472
- const newId = wb.addNode({
3597
+ wb.addNode({
3473
3598
  typeId: singleTarget.nodeTypeId,
3474
3599
  position: { x: pos.x + 180, y: pos.y },
3475
- });
3476
- runner.update(wb.export());
3477
- await runner.whenIdle();
3478
- runner.setInputs(newId, { [singleTarget.inputHandle]: coerced });
3600
+ }, { inputs: { [singleTarget.inputHandle]: coerced } });
3479
3601
  return;
3480
3602
  }
3481
3603
  if (isArray && arrTarget) {
3482
3604
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3483
3605
  const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3484
3606
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3485
- const newId = wb.addNode({
3607
+ wb.addNode({
3486
3608
  typeId: arrTarget.nodeTypeId,
3487
3609
  position: { x: pos.x + 180, y: pos.y },
3488
- });
3489
- runner.update(wb.export());
3490
- await runner.whenIdle();
3491
- runner.setInputs(newId, { [arrTarget.inputHandle]: coerced });
3610
+ }, { inputs: { [arrTarget.inputHandle]: coerced } });
3492
3611
  return;
3493
3612
  }
3494
3613
  if (isArray && elemTarget) {
@@ -3500,24 +3619,13 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3500
3619
  const COLS = 4;
3501
3620
  const DX = 180;
3502
3621
  const DY = 160;
3503
- const nodeIds = [];
3504
3622
  for (let idx = 0; idx < coercedItems.length; idx++) {
3505
3623
  const col = idx % COLS;
3506
3624
  const row = Math.floor(idx / COLS);
3507
- const newId = wb.addNode({
3625
+ wb.addNode({
3508
3626
  typeId: elemTarget.nodeTypeId,
3509
3627
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3510
- });
3511
- nodeIds.push(newId);
3512
- }
3513
- if (nodeIds.length > 0) {
3514
- runner.update(wb.export());
3515
- await runner.whenIdle();
3516
- for (let idx = 0; idx < coercedItems.length; idx++) {
3517
- runner.setInputs(nodeIds[idx], {
3518
- [elemTarget.inputHandle]: coercedItems[idx],
3519
- });
3520
- }
3628
+ }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3521
3629
  }
3522
3630
  return;
3523
3631
  }
@@ -3535,15 +3643,52 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3535
3643
  if (!n)
3536
3644
  return onClose();
3537
3645
  const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3538
- const newId = wb.addNode({
3646
+ const inboundHandles = new Set(def.edges
3647
+ .filter((e) => e.target.nodeId === nodeId)
3648
+ .map((e) => e.target.handle));
3649
+ const allInputs = runner.getInputs(def)[nodeId] || {};
3650
+ const inputsWithoutBindings = Object.fromEntries(Object.entries(allInputs).filter(([handle]) => !inboundHandles.has(handle)));
3651
+ wb.addNode({
3652
+ typeId: n.typeId,
3653
+ params: n.params,
3654
+ position: { x: pos.x + 24, y: pos.y + 24 },
3655
+ resolvedHandles: n.resolvedHandles,
3656
+ }, {
3657
+ inputs: inputsWithoutBindings,
3658
+ copyOutputsFrom: nodeId,
3659
+ dry: true,
3660
+ });
3661
+ onClose();
3662
+ },
3663
+ onDuplicateWithEdges: async () => {
3664
+ const def = wb.export();
3665
+ const n = def.nodes.find((n) => n.nodeId === nodeId);
3666
+ if (!n)
3667
+ return onClose();
3668
+ const pos = wb.getPositions?.()[nodeId] || { x: 0, y: 0 };
3669
+ // Get inputs without bindings (literal values only)
3670
+ const inputs = runner.getInputs(def)[nodeId] || {};
3671
+ // Add the duplicated node
3672
+ const newNodeId = wb.addNode({
3539
3673
  typeId: n.typeId,
3540
3674
  params: n.params,
3541
3675
  position: { x: pos.x + 24, y: pos.y + 24 },
3542
- initialInputs: n.initialInputs,
3543
3676
  resolvedHandles: n.resolvedHandles,
3677
+ }, {
3678
+ inputs,
3679
+ copyOutputsFrom: nodeId,
3680
+ dry: true,
3544
3681
  });
3545
- await runner.whenIdle();
3546
- runner.setInputs(newId, { ...runner.getInputs(def)[nodeId] });
3682
+ // Find all incoming edges (edges where target is the original node)
3683
+ const incomingEdges = def.edges.filter((e) => e.target.nodeId === nodeId);
3684
+ // Duplicate each incoming edge to point to the new node
3685
+ for (const edge of incomingEdges) {
3686
+ wb.connect({
3687
+ source: edge.source, // Keep the same source
3688
+ target: { nodeId: newNodeId, handle: edge.target.handle }, // Point to new node
3689
+ typeId: edge.typeId,
3690
+ }, { dry: true });
3691
+ }
3547
3692
  onClose();
3548
3693
  },
3549
3694
  onRunPull: async () => {
@@ -3862,18 +4007,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
3862
4007
  setNodeMenuOpen(false);
3863
4008
  }
3864
4009
  };
3865
- const addNodeAt = React.useCallback(async (typeId, opts) => {
3866
- const nodeId = wb.addNode({
3867
- typeId,
3868
- initialInputs: opts.initialInputs,
3869
- position: opts.position,
3870
- });
3871
- if (opts.inputs) {
3872
- runner.update(wb.export());
3873
- await runner.whenIdle();
3874
- runner.setInputs(nodeId, opts.inputs);
3875
- }
3876
- }, [wb, runner]);
4010
+ const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
3877
4011
  const onCloseMenu = React.useCallback(() => {
3878
4012
  setMenuOpen(false);
3879
4013
  }, []);
@@ -4117,7 +4251,7 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4117
4251
  const ex = examples.find((e) => e.id === key) ?? examples[0];
4118
4252
  if (!ex)
4119
4253
  return;
4120
- const { registry: r, def } = await ex.load();
4254
+ const { registry: r, def, inputs } = await ex.load();
4121
4255
  // Keep registry consistent with backend:
4122
4256
  // - For local backend, allow example to provide its own registry
4123
4257
  // - For remote backend, registry is automatically managed by RemoteGraphRunner
@@ -4130,6 +4264,12 @@ function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, ex
4130
4264
  await wb.load(def);
4131
4265
  // Build a local runtime so seeded defaults are visible pre-run
4132
4266
  runner.build(wb.export());
4267
+ // Set initial inputs if provided
4268
+ if (inputs) {
4269
+ for (const [nodeId, map] of Object.entries(inputs)) {
4270
+ runner.setInputs(nodeId, map);
4271
+ }
4272
+ }
4133
4273
  runAutoLayout();
4134
4274
  setExampleState(key);
4135
4275
  onExampleChange?.(key);