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