@bian-womp/spark-workbench 0.2.67 → 0.2.69

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 (41) hide show
  1. package/lib/cjs/index.cjs +284 -38
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/misc/DefaultContextMenu.d.ts +1 -1
  4. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +1 -1
  5. package/lib/cjs/src/misc/SelectionContextMenu.d.ts +1 -1
  6. package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +1 -1
  7. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  8. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts +16 -0
  9. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts +1 -1
  11. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +10 -22
  13. package/lib/cjs/src/misc/context/WorkbenchContext.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 +6 -1
  16. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  17. package/lib/cjs/src/runtime/IGraphRunner.d.ts +6 -1
  18. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  19. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +6 -1
  20. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  21. package/lib/esm/index.js +284 -38
  22. package/lib/esm/index.js.map +1 -1
  23. package/lib/esm/src/misc/DefaultContextMenu.d.ts +1 -1
  24. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +1 -1
  25. package/lib/esm/src/misc/SelectionContextMenu.d.ts +1 -1
  26. package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +1 -1
  27. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  28. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts +16 -0
  29. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +1 -1
  30. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts +1 -1
  31. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +1 -1
  32. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +10 -22
  33. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  34. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  35. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +6 -1
  36. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  37. package/lib/esm/src/runtime/IGraphRunner.d.ts +6 -1
  38. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  39. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +6 -1
  40. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  41. package/package.json +4 -4
package/lib/cjs/index.cjs CHANGED
@@ -627,11 +627,10 @@ class AbstractGraphRunner {
627
627
  this.runtime.resume();
628
628
  // Create and launch new engine (to be implemented by subclasses)
629
629
  await this.createAndLaunchEngine(opts);
630
- // Re-apply staged inputs to new engine
630
+ // Re-apply staged inputs to new engine using runner's setInputs method
631
+ // This ensures consistency and proper handling of staged inputs
631
632
  for (const [nodeId, map] of Object.entries(currentInputs)) {
632
- if (this.engine) {
633
- this.engine.setInputs(nodeId, map);
634
- }
633
+ await this.setInputs(nodeId, map);
635
634
  }
636
635
  }
637
636
  getInputDefaults(def) {
@@ -673,6 +672,21 @@ class AbstractGraphRunner {
673
672
  getRunningEngine() {
674
673
  return this.runningKind;
675
674
  }
675
+ // Optional undo/redo support
676
+ async undo() {
677
+ return false;
678
+ }
679
+ async redo() {
680
+ return false;
681
+ }
682
+ async canUndo() {
683
+ return false;
684
+ }
685
+ async canRedo() {
686
+ return false;
687
+ }
688
+ // Optional commit support
689
+ async commit() { }
676
690
  }
677
691
 
678
692
  // Counter for generating readable runner IDs
@@ -1297,8 +1311,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1297
1311
  this.engine = eng;
1298
1312
  this.runningKind = opts?.engine ?? "push";
1299
1313
  this.emit("status", { running: true, engine: this.runningKind });
1314
+ // Re-apply staged inputs using client.setInputs for consistency
1300
1315
  for (const [nodeId, map] of Object.entries(this.stagedInputs)) {
1301
- this.engine.setInputs(nodeId, map);
1316
+ await client.setInputs(nodeId, map).catch(() => {
1317
+ // Ignore errors during launch - inputs will be set when user calls setInputs
1318
+ });
1302
1319
  }
1303
1320
  }
1304
1321
  /**
@@ -1340,9 +1357,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1340
1357
  this.engine = eng;
1341
1358
  this.runningKind = opts?.engine ?? "push";
1342
1359
  this.emit("status", { running: true, engine: this.runningKind });
1343
- // Re-apply staged inputs to new engine
1360
+ // Re-apply staged inputs using client.setInputs for consistency
1344
1361
  for (const [nodeId, map] of Object.entries(currentInputs)) {
1345
- this.engine.setInputs(nodeId, map);
1362
+ await client.setInputs(nodeId, map).catch(() => {
1363
+ // Ignore errors during engine switch - inputs will be set when user calls setInputs
1364
+ });
1346
1365
  }
1347
1366
  }
1348
1367
  async step() {
@@ -1357,7 +1376,7 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1357
1376
  const client = await this.ensureClient();
1358
1377
  await client.flush();
1359
1378
  }
1360
- setInputs(nodeId, inputs, options) {
1379
+ async setInputs(nodeId, inputs, options) {
1361
1380
  // Update staged inputs (for getInputs to work correctly)
1362
1381
  if (!this.stagedInputs[nodeId])
1363
1382
  this.stagedInputs[nodeId] = {};
@@ -1369,21 +1388,17 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1369
1388
  this.stagedInputs[nodeId][handle] = value;
1370
1389
  }
1371
1390
  }
1372
- // If engine exists, call directly; otherwise ensure client (fire-and-forget)
1373
- if (this.engine) {
1374
- this.engine.setInputs(nodeId, inputs, options);
1391
+ // Use transport.request instead of transport.send for consistency
1392
+ const client = await this.ensureClient();
1393
+ try {
1394
+ await client.setInputs(nodeId, inputs, options);
1375
1395
  }
1376
- else {
1377
- this.ensureClient()
1378
- .then((client) => {
1379
- client.getEngine().setInputs(nodeId, inputs, options);
1380
- })
1381
- .catch(() => {
1382
- // Emit synthetic events if connection fails
1383
- for (const [handle, value] of Object.entries(inputs)) {
1384
- this.emit("value", { nodeId, handle, value, io: "input" });
1385
- }
1386
- });
1396
+ catch (err) {
1397
+ // Emit synthetic events if connection fails
1398
+ for (const [handle, value] of Object.entries(inputs)) {
1399
+ this.emit("value", { nodeId, handle, value, io: "input" });
1400
+ }
1401
+ throw err;
1387
1402
  }
1388
1403
  }
1389
1404
  async copyOutputs(fromNodeId, toNodeId, options) {
@@ -1418,6 +1433,52 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1418
1433
  const client = await this.ensureClient();
1419
1434
  await client.setExtData(data);
1420
1435
  }
1436
+ async commit() {
1437
+ const client = await this.ensureClient();
1438
+ try {
1439
+ await client.commit();
1440
+ }
1441
+ catch (err) {
1442
+ console.error("[RemoteGraphRunner] Error committing:", err);
1443
+ throw err;
1444
+ }
1445
+ }
1446
+ async undo() {
1447
+ const client = await this.ensureClient();
1448
+ try {
1449
+ return await client.undo();
1450
+ }
1451
+ catch {
1452
+ return false;
1453
+ }
1454
+ }
1455
+ async redo() {
1456
+ const client = await this.ensureClient();
1457
+ try {
1458
+ return await client.redo();
1459
+ }
1460
+ catch {
1461
+ return false;
1462
+ }
1463
+ }
1464
+ async canUndo() {
1465
+ const client = await this.ensureClient();
1466
+ try {
1467
+ return await client.canUndo();
1468
+ }
1469
+ catch {
1470
+ return false;
1471
+ }
1472
+ }
1473
+ async canRedo() {
1474
+ const client = await this.ensureClient();
1475
+ try {
1476
+ return await client.canRedo();
1477
+ }
1478
+ catch {
1479
+ return false;
1480
+ }
1481
+ }
1421
1482
  async snapshotFull() {
1422
1483
  const client = await this.ensureClient();
1423
1484
  try {
@@ -2870,7 +2931,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2870
2931
  }
2871
2932
  return add("workbench", "graphChanged")(event);
2872
2933
  });
2873
- const offWbGraphUiChanged = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
2934
+ const offWbGraphUiChangedForLog = wb.on("graphUiChanged", add("workbench", "graphUiChanged"));
2874
2935
  const offWbValidationChanged = wb.on("validationChanged", add("workbench", "validationChanged"));
2875
2936
  // Ensure newly added nodes start as invalidated until first evaluation
2876
2937
  const offWbAddNode = wb.on("graphChanged", (e) => {
@@ -2884,39 +2945,60 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2884
2945
  }
2885
2946
  });
2886
2947
  const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
2887
- if (!runner.isRunning())
2948
+ if (!runner.isRunning()) {
2949
+ // If runner not running, commit immediately (no update needed)
2950
+ await runner.commit().catch((err) => {
2951
+ console.error("[WorkbenchContext] Error committing:", err);
2952
+ });
2888
2953
  return;
2954
+ }
2889
2955
  try {
2890
2956
  if (event.change?.type === "addNode") {
2891
2957
  const { nodeId, inputs, copyOutputsFrom } = event.change;
2892
2958
  if (event.dry) {
2893
2959
  await runner.update(event.def, { dry: true });
2894
2960
  if (inputs) {
2895
- runner.setInputs(nodeId, inputs, { dry: true });
2961
+ await runner.setInputs(nodeId, inputs, { dry: true });
2896
2962
  }
2897
2963
  if (copyOutputsFrom) {
2898
- runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
2964
+ await runner.copyOutputs(copyOutputsFrom, nodeId, { dry: true });
2899
2965
  }
2900
2966
  }
2901
2967
  else {
2902
2968
  await runner.update(event.def, { dry: !!inputs });
2903
2969
  if (inputs) {
2904
- runner.setInputs(nodeId, inputs, { dry: false });
2970
+ await runner.setInputs(nodeId, inputs, { dry: false });
2905
2971
  }
2906
2972
  }
2907
2973
  }
2908
2974
  else {
2909
2975
  await runner.update(event.def, { dry: event.dry });
2910
2976
  }
2977
+ // Wait for update to complete, then commit
2978
+ await runner.commit().catch((err) => {
2979
+ console.error("[WorkbenchContext] Error committing after update:", err);
2980
+ });
2911
2981
  }
2912
2982
  catch (err) {
2913
2983
  console.error("[WorkbenchContext] Error updating graph:", err);
2914
2984
  }
2915
2985
  });
2916
2986
  const offWbdSetValidation = wb.on("validationChanged", (r) => setValidation(r));
2917
- const offWbSelectionChanged = wb.on("selectionChanged", (sel) => {
2987
+ const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
2918
2988
  setSelectedNodeId(sel.nodes?.[0]);
2919
2989
  setSelectedEdgeId(sel.edges?.[0]);
2990
+ // Commit on selection change
2991
+ await runner.commit().catch((err) => {
2992
+ console.error("[WorkbenchContext] Error committing selection change:", err);
2993
+ });
2994
+ });
2995
+ const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
2996
+ // Only commit if commit flag is true (e.g., drag end, not during dragging)
2997
+ if (event.commit) {
2998
+ await runner.commit().catch((err) => {
2999
+ console.error("[WorkbenchContext] Error committing UI changes:", err);
3000
+ });
3001
+ }
2920
3002
  });
2921
3003
  const offWbError = wb.on("error", add("workbench", "error"));
2922
3004
  // Registry updates: swap registry and refresh graph validation/UI
@@ -2953,6 +3035,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2953
3035
  offRunnerInvalidate();
2954
3036
  offRunnerStats();
2955
3037
  offWbGraphChanged();
3038
+ offWbGraphUiChangedForLog();
2956
3039
  offWbGraphUiChanged();
2957
3040
  offWbValidationChanged();
2958
3041
  offWbError();
@@ -3340,10 +3423,24 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
3340
3423
  /**
3341
3424
  * Creates base default context menu handlers.
3342
3425
  */
3343
- function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste) {
3426
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
3427
+ // Wrap paste handler to clear storage after paste
3428
+ const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
3429
+ ? (position) => {
3430
+ onPaste(position);
3431
+ clearCopiedData();
3432
+ }
3433
+ : onPaste;
3434
+ // Function to check if paste data exists (called dynamically when menu opens)
3435
+ const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
3344
3436
  return {
3345
3437
  onAddNode,
3346
- onPaste,
3438
+ onPaste: wrappedOnPaste,
3439
+ hasPasteData,
3440
+ onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
3441
+ onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
3442
+ canUndo: runner ? () => runner.canUndo().catch(() => false) : undefined,
3443
+ canRedo: runner ? () => runner.canRedo().catch(() => false) : undefined,
3347
3444
  onClose,
3348
3445
  };
3349
3446
  }
@@ -3867,13 +3964,48 @@ function DefaultNodeContent({ data, isConnectable, }) {
3867
3964
  } })] }));
3868
3965
  }
3869
3966
 
3870
- function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3967
+ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
3968
+ undo: "⌘/Ctrl + Z",
3969
+ redo: "⌘/Ctrl + Shift + Z",
3970
+ paste: "⌘/Ctrl + V",
3971
+ }, }) {
3871
3972
  const rf = react.useReactFlow();
3872
3973
  const [query, setQuery] = React.useState("");
3974
+ const [canUndo, setCanUndo] = React.useState(false);
3975
+ const [canRedo, setCanRedo] = React.useState(false);
3976
+ const [hasPasteData, setHasPasteData] = React.useState(false);
3873
3977
  const q = query.trim().toLowerCase();
3874
3978
  const filteredIds = q
3875
3979
  ? nodeIds.filter((id) => id.toLowerCase().includes(q))
3876
3980
  : nodeIds;
3981
+ // Check undo/redo availability and paste data when menu opens
3982
+ React.useEffect(() => {
3983
+ if (!open)
3984
+ return;
3985
+ let cancelled = false;
3986
+ const checkAvailability = async () => {
3987
+ if (handlers.canUndo) {
3988
+ const result = await handlers.canUndo();
3989
+ if (!cancelled)
3990
+ setCanUndo(result);
3991
+ }
3992
+ if (handlers.canRedo) {
3993
+ const result = await handlers.canRedo();
3994
+ if (!cancelled)
3995
+ setCanRedo(result);
3996
+ }
3997
+ // Check paste data dynamically
3998
+ if (handlers.hasPasteData) {
3999
+ const result = handlers.hasPasteData();
4000
+ if (!cancelled)
4001
+ setHasPasteData(result);
4002
+ }
4003
+ };
4004
+ checkAvailability();
4005
+ return () => {
4006
+ cancelled = true;
4007
+ };
4008
+ }, [open, handlers.canUndo, handlers.canRedo, handlers.hasPasteData]);
3877
4009
  const root = { __children: {} };
3878
4010
  for (const id of filteredIds) {
3879
4011
  const parts = id.split(".");
@@ -3938,6 +4070,12 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3938
4070
  handlers.onPaste(p);
3939
4071
  handlers.onClose();
3940
4072
  };
4073
+ // Helper to format shortcut for current platform
4074
+ const formatShortcut = (shortcut) => {
4075
+ const isMac = typeof navigator !== "undefined" &&
4076
+ navigator.userAgent.toLowerCase().includes("mac");
4077
+ return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
4078
+ };
3941
4079
  const renderTree = (tree, path = []) => {
3942
4080
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
3943
4081
  return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
@@ -3955,7 +4093,10 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, }) {
3955
4093
  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 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
3956
4094
  e.preventDefault();
3957
4095
  e.stopPropagation();
3958
- }, children: [handlers.onPaste && (jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed", onClick: handlePaste, children: "Paste" })), handlers.onPaste && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
4096
+ }, children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlePaste, children: [jsxRuntime.jsx("span", { children: "Paste" }), enableKeyboardShortcuts && keyboardShortcuts.paste && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.paste) }))] })), (handlers.onUndo || handlers.onRedo) && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" })), handlers.onUndo && (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlers.onUndo, disabled: !canUndo, children: [jsxRuntime.jsx("span", { children: "Undo" }), enableKeyboardShortcuts && keyboardShortcuts.undo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.undo) }))] })), handlers.onRedo && (jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between", onClick: handlers.onRedo, disabled: !canRedo, children: [jsxRuntime.jsx("span", { children: "Redo" }), enableKeyboardShortcuts && keyboardShortcuts.redo && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.redo) }))] }))] })), hasPasteData &&
4097
+ handlers.onPaste &&
4098
+ !handlers.onUndo &&
4099
+ !handlers.onRedo && jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Add Node", " ", jsxRuntime.jsxs("span", { className: "text-gray-500 font-normal", children: ["(", totalCount, ")"] })] }), jsxRuntime.jsx("div", { className: "px-2 pb-1", children: jsxRuntime.jsx("input", { ref: inputRef, type: "text", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Filter nodes...", className: "w-full border border-gray-300 px-2 py-1 text-sm outline-none focus:border-gray-400 select-text", onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation() }) }), jsxRuntime.jsx("div", { className: "max-h-60 overflow-auto", children: totalCount > 0 ? (renderTree(root)) : (jsxRuntime.jsx("div", { className: "px-3 py-2 text-gray-400", children: "No matches" })) })] }));
3959
4100
  }
3960
4101
 
3961
4102
  function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
@@ -3999,7 +4140,10 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
3999
4140
  }, 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" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopyId, children: "Copy Node ID" }), bakeableOutputs.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "h-px bg-gray-200 my-1" }), 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)))] }))] }));
4000
4141
  }
4001
4142
 
4002
- function SelectionContextMenu({ open, clientPos, handlers, }) {
4143
+ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
4144
+ copy: "⌘/Ctrl + C",
4145
+ delete: "Delete",
4146
+ }, }) {
4003
4147
  const ref = React.useRef(null);
4004
4148
  // Close on outside click and on ESC
4005
4149
  React.useEffect(() => {
@@ -4026,6 +4170,12 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
4026
4170
  if (open)
4027
4171
  ref.current?.focus();
4028
4172
  }, [open]);
4173
+ // Helper to format shortcut for current platform
4174
+ const formatShortcut = (shortcut) => {
4175
+ const isMac = typeof navigator !== "undefined" &&
4176
+ navigator.userAgent.toLowerCase().includes("mac");
4177
+ return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
4178
+ };
4029
4179
  if (!open || !clientPos)
4030
4180
  return null;
4031
4181
  // Clamp menu position to viewport
@@ -4037,7 +4187,7 @@ function SelectionContextMenu({ open, clientPos, handlers, }) {
4037
4187
  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 select-none", style: { left: x, top: y }, onClick: (e) => e.stopPropagation(), onMouseDown: (e) => e.stopPropagation(), onWheel: (e) => e.stopPropagation(), onContextMenu: (e) => {
4038
4188
  e.preventDefault();
4039
4189
  e.stopPropagation();
4040
- }, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onCopy, children: "Copy" }), jsxRuntime.jsx("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100", onClick: handlers.onDelete, children: "Delete" })] }));
4190
+ }, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onCopy, children: [jsxRuntime.jsx("span", { children: "Copy" }), enableKeyboardShortcuts && keyboardShortcuts.copy && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.copy) }))] }), jsxRuntime.jsxs("button", { className: "block w-full text-left px-2 py-1 hover:bg-gray-100 flex items-center justify-between", onClick: handlers.onDelete, children: [jsxRuntime.jsx("span", { children: "Delete" }), enableKeyboardShortcuts && keyboardShortcuts.delete && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(keyboardShortcuts.delete) }))] })] }));
4041
4191
  }
4042
4192
 
4043
4193
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
@@ -4409,12 +4559,12 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4409
4559
  return;
4410
4560
  wb.pasteCopiedData(data, position);
4411
4561
  onCloseMenu();
4412
- });
4562
+ }, runner, () => storage.get(), () => storage.set(null));
4413
4563
  if (overrides?.getDefaultContextMenuHandlers) {
4414
4564
  return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4415
4565
  }
4416
4566
  return baseHandlers;
4417
- }, [addNodeAt, onCloseMenu, overrides, wb]);
4567
+ }, [addNodeAt, onCloseMenu, overrides, wb, runner]);
4418
4568
  const selectionContextMenuHandlers = React.useMemo(() => {
4419
4569
  // Get storage from override or use workbench's internal storage
4420
4570
  const storage = overrides?.getCopiedDataStorage
@@ -4469,6 +4619,100 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4469
4619
  return [];
4470
4620
  return getBakeableOutputs(nodeAtMenu, wb, registry, outputTypesMap);
4471
4621
  }, [nodeAtMenu, wb, registry, outputTypesMap]);
4622
+ // Keyboard shortcuts configuration
4623
+ const enableKeyboardShortcuts = overrides?.enableKeyboardShortcuts !== false; // Default to true
4624
+ const keyboardShortcuts = overrides?.keyboardShortcuts || {
4625
+ undo: "⌘/Ctrl + Z",
4626
+ redo: "⌘/Ctrl + Shift + Z",
4627
+ copy: "⌘/Ctrl + C",
4628
+ paste: "⌘/Ctrl + V",
4629
+ delete: "Delete",
4630
+ };
4631
+ // Keyboard shortcut handler
4632
+ React.useEffect(() => {
4633
+ if (!enableKeyboardShortcuts)
4634
+ return;
4635
+ const handleKeyDown = async (e) => {
4636
+ // Ignore if typing in input/textarea
4637
+ const target = e.target;
4638
+ if (target.tagName === "INPUT" ||
4639
+ target.tagName === "TEXTAREA" ||
4640
+ target.isContentEditable) {
4641
+ return;
4642
+ }
4643
+ // Detect Mac platform using userAgent (navigator.platform is deprecated)
4644
+ const isMac = typeof navigator !== "undefined" &&
4645
+ navigator.userAgent.toLowerCase().includes("mac");
4646
+ const modKey = isMac ? e.metaKey : e.ctrlKey;
4647
+ const key = e.key.toLowerCase();
4648
+ // Undo: Cmd/Ctrl + Z
4649
+ if (modKey && key === "z" && !e.shiftKey && !e.altKey) {
4650
+ e.preventDefault();
4651
+ if (runner &&
4652
+ "onUndo" in defaultContextMenuHandlers &&
4653
+ defaultContextMenuHandlers.onUndo) {
4654
+ const canUndo = await runner.canUndo().catch(() => false);
4655
+ if (canUndo) {
4656
+ defaultContextMenuHandlers.onUndo();
4657
+ }
4658
+ }
4659
+ return;
4660
+ }
4661
+ // Redo: Cmd/Ctrl + Shift + Z
4662
+ if (modKey && e.shiftKey && key === "z" && !e.altKey) {
4663
+ e.preventDefault();
4664
+ if (runner &&
4665
+ "onRedo" in defaultContextMenuHandlers &&
4666
+ defaultContextMenuHandlers.onRedo) {
4667
+ const canRedo = await runner.canRedo().catch(() => false);
4668
+ if (canRedo) {
4669
+ defaultContextMenuHandlers.onRedo();
4670
+ }
4671
+ }
4672
+ return;
4673
+ }
4674
+ // Copy: Cmd/Ctrl + C
4675
+ if (modKey && key === "c" && !e.shiftKey && !e.altKey) {
4676
+ const selection = wb.getSelection();
4677
+ if (selection.nodes.length > 0 || selection.edges.length > 0) {
4678
+ e.preventDefault();
4679
+ if (selectionContextMenuHandlers.onCopy) {
4680
+ selectionContextMenuHandlers.onCopy();
4681
+ }
4682
+ }
4683
+ return;
4684
+ }
4685
+ // Paste: Cmd/Ctrl + V
4686
+ if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
4687
+ e.preventDefault();
4688
+ if ("hasPasteData" in defaultContextMenuHandlers &&
4689
+ defaultContextMenuHandlers.hasPasteData &&
4690
+ defaultContextMenuHandlers.hasPasteData() &&
4691
+ "onPaste" in defaultContextMenuHandlers &&
4692
+ defaultContextMenuHandlers.onPaste) {
4693
+ const center = rfInstanceRef.current?.screenToFlowPosition({
4694
+ x: window.innerWidth / 2,
4695
+ y: window.innerHeight / 2,
4696
+ }) || { x: 0, y: 0 };
4697
+ defaultContextMenuHandlers.onPaste(center);
4698
+ }
4699
+ return;
4700
+ }
4701
+ // Note: Delete/Backspace is handled by ReactFlow's deleteKeyCode prop
4702
+ // which triggers onNodesDelete/onEdgesDelete, so we don't need to handle it here
4703
+ };
4704
+ window.addEventListener("keydown", handleKeyDown);
4705
+ return () => {
4706
+ window.removeEventListener("keydown", handleKeyDown);
4707
+ };
4708
+ }, [
4709
+ enableKeyboardShortcuts,
4710
+ wb,
4711
+ runner,
4712
+ defaultContextMenuHandlers,
4713
+ selectionContextMenuHandlers,
4714
+ rfInstanceRef,
4715
+ ]);
4472
4716
  // Get custom renderers from UI extension registry (reactive to uiVersion changes)
4473
4717
  const { BackgroundRenderer, MinimapRenderer, ControlsRenderer, DefaultContextMenuRenderer, NodeContextMenuRenderer, connectionLineRenderer, } = React.useMemo(() => {
4474
4718
  return {
@@ -4515,9 +4759,11 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4515
4759
  zoom: savedViewport.zoom,
4516
4760
  });
4517
4761
  }
4518
- }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds })), !!nodeAtMenu &&
4762
+ }, onConnect: onConnect, onEdgesChange: onEdgesChange, onEdgesDelete: onEdgesDelete, onNodesDelete: onNodesDelete, onNodesChange: onNodesChange, onMoveEnd: onMoveEnd, deleteKeyCode: ["Backspace", "Delete"], proOptions: { hideAttribution: true }, noDragClassName: "wb-nodrag", noWheelClassName: "wb-nowheel", noPanClassName: "wb-nopan", fitView: true, children: [BackgroundRenderer ? (jsxRuntime.jsx(BackgroundRenderer, {})) : (jsxRuntime.jsx(react.Background, { id: "workbench-canvas-background", variant: react.BackgroundVariant.Dots, gap: 12, size: 1 })), MinimapRenderer ? jsxRuntime.jsx(MinimapRenderer, {}) : jsxRuntime.jsx(react.MiniMap, {}), ControlsRenderer ? jsxRuntime.jsx(ControlsRenderer, {}) : jsxRuntime.jsx(react.Controls, {}), DefaultContextMenuRenderer ? (jsxRuntime.jsx(DefaultContextMenuRenderer, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, ...(enableKeyboardShortcuts !== false
4763
+ ? { enableKeyboardShortcuts, keyboardShortcuts }
4764
+ : {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
4519
4765
  nodeContextMenuHandlers &&
4520
- (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers }))] }) }) }));
4766
+ (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
4521
4767
  });
4522
4768
 
4523
4769
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {