@bian-womp/spark-workbench 0.2.69 → 0.2.71

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 (77) hide show
  1. package/lib/cjs/index.cjs +377 -177
  2. package/lib/cjs/index.cjs.map +1 -1
  3. package/lib/cjs/src/core/InMemoryWorkbench.d.ts +35 -14
  4. package/lib/cjs/src/core/InMemoryWorkbench.d.ts.map +1 -1
  5. package/lib/cjs/src/core/contracts.d.ts +5 -0
  6. package/lib/cjs/src/core/contracts.d.ts.map +1 -1
  7. package/lib/cjs/src/index.d.ts +1 -1
  8. package/lib/cjs/src/index.d.ts.map +1 -1
  9. package/lib/cjs/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  10. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts +2 -1
  11. package/lib/cjs/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  12. package/lib/cjs/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  13. package/lib/cjs/src/misc/context-menu/ContextMenuButton.d.ts +9 -0
  14. package/lib/cjs/src/misc/context-menu/ContextMenuButton.d.ts.map +1 -0
  15. package/lib/cjs/src/misc/{context → context-menu}/ContextMenuHandlers.d.ts +8 -2
  16. package/lib/cjs/src/misc/context-menu/ContextMenuHandlers.d.ts.map +1 -0
  17. package/lib/cjs/src/misc/{context → context-menu}/ContextMenuHelpers.d.ts +2 -1
  18. package/lib/cjs/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -0
  19. package/lib/cjs/src/misc/{DefaultContextMenu.d.ts → context-menu/DefaultContextMenu.d.ts} +1 -1
  20. package/lib/cjs/src/misc/context-menu/DefaultContextMenu.d.ts.map +1 -0
  21. package/lib/cjs/src/misc/context-menu/NodeContextMenu.d.ts +3 -0
  22. package/lib/cjs/src/misc/context-menu/NodeContextMenu.d.ts.map +1 -0
  23. package/lib/cjs/src/misc/{SelectionContextMenu.d.ts → context-menu/SelectionContextMenu.d.ts} +1 -1
  24. package/lib/cjs/src/misc/context-menu/SelectionContextMenu.d.ts.map +1 -0
  25. package/lib/cjs/src/misc/hooks.d.ts.map +1 -1
  26. package/lib/cjs/src/misc/load.d.ts.map +1 -1
  27. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts +2 -4
  28. package/lib/cjs/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  29. package/lib/cjs/src/runtime/IGraphRunner.d.ts +3 -4
  30. package/lib/cjs/src/runtime/IGraphRunner.d.ts.map +1 -1
  31. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts +2 -4
  32. package/lib/cjs/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  33. package/lib/esm/index.js +377 -177
  34. package/lib/esm/index.js.map +1 -1
  35. package/lib/esm/src/core/InMemoryWorkbench.d.ts +35 -14
  36. package/lib/esm/src/core/InMemoryWorkbench.d.ts.map +1 -1
  37. package/lib/esm/src/core/contracts.d.ts +5 -0
  38. package/lib/esm/src/core/contracts.d.ts.map +1 -1
  39. package/lib/esm/src/index.d.ts +1 -1
  40. package/lib/esm/src/index.d.ts.map +1 -1
  41. package/lib/esm/src/misc/WorkbenchCanvas.d.ts.map +1 -1
  42. package/lib/esm/src/misc/context/WorkbenchContext.d.ts +2 -1
  43. package/lib/esm/src/misc/context/WorkbenchContext.d.ts.map +1 -1
  44. package/lib/esm/src/misc/context/WorkbenchContext.provider.d.ts.map +1 -1
  45. package/lib/esm/src/misc/context-menu/ContextMenuButton.d.ts +9 -0
  46. package/lib/esm/src/misc/context-menu/ContextMenuButton.d.ts.map +1 -0
  47. package/lib/esm/src/misc/{context → context-menu}/ContextMenuHandlers.d.ts +8 -2
  48. package/lib/esm/src/misc/context-menu/ContextMenuHandlers.d.ts.map +1 -0
  49. package/lib/esm/src/misc/{context → context-menu}/ContextMenuHelpers.d.ts +2 -1
  50. package/lib/esm/src/misc/context-menu/ContextMenuHelpers.d.ts.map +1 -0
  51. package/lib/esm/src/misc/{DefaultContextMenu.d.ts → context-menu/DefaultContextMenu.d.ts} +1 -1
  52. package/lib/esm/src/misc/context-menu/DefaultContextMenu.d.ts.map +1 -0
  53. package/lib/esm/src/misc/context-menu/NodeContextMenu.d.ts +3 -0
  54. package/lib/esm/src/misc/context-menu/NodeContextMenu.d.ts.map +1 -0
  55. package/lib/esm/src/misc/{SelectionContextMenu.d.ts → context-menu/SelectionContextMenu.d.ts} +1 -1
  56. package/lib/esm/src/misc/context-menu/SelectionContextMenu.d.ts.map +1 -0
  57. package/lib/esm/src/misc/hooks.d.ts.map +1 -1
  58. package/lib/esm/src/misc/load.d.ts.map +1 -1
  59. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts +2 -4
  60. package/lib/esm/src/runtime/AbstractGraphRunner.d.ts.map +1 -1
  61. package/lib/esm/src/runtime/IGraphRunner.d.ts +3 -4
  62. package/lib/esm/src/runtime/IGraphRunner.d.ts.map +1 -1
  63. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts +2 -4
  64. package/lib/esm/src/runtime/RemoteGraphRunner.d.ts.map +1 -1
  65. package/package.json +4 -4
  66. package/lib/cjs/src/misc/DefaultContextMenu.d.ts.map +0 -1
  67. package/lib/cjs/src/misc/NodeContextMenu.d.ts +0 -3
  68. package/lib/cjs/src/misc/NodeContextMenu.d.ts.map +0 -1
  69. package/lib/cjs/src/misc/SelectionContextMenu.d.ts.map +0 -1
  70. package/lib/cjs/src/misc/context/ContextMenuHandlers.d.ts.map +0 -1
  71. package/lib/cjs/src/misc/context/ContextMenuHelpers.d.ts.map +0 -1
  72. package/lib/esm/src/misc/DefaultContextMenu.d.ts.map +0 -1
  73. package/lib/esm/src/misc/NodeContextMenu.d.ts +0 -3
  74. package/lib/esm/src/misc/NodeContextMenu.d.ts.map +0 -1
  75. package/lib/esm/src/misc/SelectionContextMenu.d.ts.map +0 -1
  76. package/lib/esm/src/misc/context/ContextMenuHandlers.d.ts.map +0 -1
  77. package/lib/esm/src/misc/context/ContextMenuHelpers.d.ts.map +0 -1
package/lib/cjs/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var sparkGraph = require('@bian-womp/spark-graph');
4
+ var lod = require('lodash');
4
5
  var sparkRemote = require('@bian-womp/spark-remote');
5
6
  var react = require('@xyflow/react');
6
7
  var React = require('react');
@@ -124,13 +125,15 @@ class InMemoryWorkbench extends AbstractWorkbench {
124
125
  constructor() {
125
126
  super(...arguments);
126
127
  this.def = { nodes: [], edges: [] };
127
- this.positions = {};
128
128
  this.listeners = new Map();
129
+ this.positions = {};
129
130
  this.selection = {
130
131
  nodes: [],
131
132
  edges: [],
132
133
  };
133
134
  this.viewport = null;
135
+ this.runtimeState = null;
136
+ this.historyState = undefined;
134
137
  this.copiedData = null;
135
138
  }
136
139
  setRegistry(registry) {
@@ -210,18 +213,19 @@ class InMemoryWorkbench extends AbstractWorkbench {
210
213
  inputs: options?.inputs,
211
214
  copyOutputsFrom: options?.copyOutputsFrom,
212
215
  },
213
- dry: options?.dry,
216
+ ...lod.pick(options, ["dry", "commit", "reason"]),
214
217
  });
215
218
  this.refreshValidation();
216
219
  return id;
217
220
  }
218
- removeNode(nodeId) {
221
+ removeNode(nodeId, options) {
219
222
  this.def.nodes = this.def.nodes.filter((n) => n.nodeId !== nodeId);
220
223
  this.def.edges = this.def.edges.filter((e) => e.source.nodeId !== nodeId && e.target.nodeId !== nodeId);
221
224
  delete this.positions[nodeId];
222
225
  this.emit("graphChanged", {
223
226
  def: this.def,
224
227
  change: { type: "removeNode", nodeId },
228
+ ...options,
225
229
  });
226
230
  this.refreshValidation();
227
231
  }
@@ -236,16 +240,17 @@ class InMemoryWorkbench extends AbstractWorkbench {
236
240
  this.emit("graphChanged", {
237
241
  def: this.def,
238
242
  change: { type: "connect", edgeId: id },
239
- dry: options?.dry,
243
+ ...options,
240
244
  });
241
245
  this.refreshValidation();
242
246
  return id;
243
247
  }
244
- disconnect(edgeId) {
248
+ disconnect(edgeId, options) {
245
249
  this.def.edges = this.def.edges.filter((e) => e.id !== edgeId);
246
250
  this.emit("graphChanged", {
247
251
  def: this.def,
248
252
  change: { type: "disconnect", edgeId },
253
+ ...options,
249
254
  });
250
255
  this.emit("validationChanged", this.validate());
251
256
  }
@@ -274,32 +279,26 @@ class InMemoryWorkbench extends AbstractWorkbench {
274
279
  });
275
280
  }
276
281
  // Position and selection APIs for React Flow bridge
277
- setPosition(nodeId, pos, opts) {
278
- this.positions[nodeId] = pos;
279
- this.emit("graphUiChanged", {
280
- def: this.def,
281
- change: { type: "moveNode", nodeId, pos },
282
- commit: !!opts?.commit === true,
283
- });
284
- }
285
- setPositions(map, opts) {
282
+ setPositions(map, options) {
286
283
  this.positions = { ...map };
287
284
  this.emit("graphUiChanged", {
288
285
  def: this.def,
289
286
  change: { type: "moveNodes" },
290
- commit: opts?.commit,
287
+ ...options,
291
288
  });
292
289
  }
293
290
  getPositions() {
294
291
  return { ...this.positions };
295
292
  }
296
- setSelection(sel, opts) {
293
+ setSelection(sel, options) {
294
+ if (lod.isEqual(this.selection, sel))
295
+ return;
297
296
  this.selection = { nodes: [...sel.nodes], edges: [...sel.edges] };
298
297
  this.emit("selectionChanged", this.selection);
299
298
  this.emit("graphUiChanged", {
300
299
  def: this.def,
301
300
  change: { type: "selection" },
302
- commit: opts?.commit,
301
+ ...options,
303
302
  });
304
303
  }
305
304
  getSelection() {
@@ -311,7 +310,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
311
310
  /**
312
311
  * Delete all selected nodes and edges.
313
312
  */
314
- deleteSelection() {
313
+ deleteSelection(options) {
315
314
  const selection = this.getSelection();
316
315
  // Delete all selected nodes (this will also remove connected edges)
317
316
  for (const nodeId of selection.nodes) {
@@ -322,14 +321,14 @@ class InMemoryWorkbench extends AbstractWorkbench {
322
321
  this.disconnect(edgeId);
323
322
  }
324
323
  // Clear selection
325
- this.setSelection({ nodes: [], edges: [] });
324
+ this.setSelection({ nodes: [], edges: [] }, options);
326
325
  }
327
- setViewport(viewport, opts) {
326
+ setViewport(viewport, options) {
328
327
  this.viewport = { ...viewport };
329
328
  this.emit("graphUiChanged", {
330
329
  def: this.def,
331
330
  change: { type: "viewport" },
332
- commit: opts?.commit,
331
+ ...options,
333
332
  });
334
333
  }
335
334
  getViewport() {
@@ -371,6 +370,27 @@ class InMemoryWorkbench extends AbstractWorkbench {
371
370
  this.viewport = { ...ui.viewport };
372
371
  }
373
372
  }
373
+ getRuntimeState() {
374
+ return this.runtimeState ? { ...this.runtimeState } : null;
375
+ }
376
+ setRuntimeState(runtime) {
377
+ this.runtimeState = runtime ? { ...runtime } : null;
378
+ }
379
+ getHistory() {
380
+ return this.historyState;
381
+ }
382
+ setHistory(history) {
383
+ this.historyState = history;
384
+ }
385
+ getNodeRuntimeMetadata(nodeId) {
386
+ return this.runtimeState?.nodes[nodeId];
387
+ }
388
+ updateNodeRuntimeMetadata(nodeId, updater) {
389
+ const current = this.runtimeState ?? { nodes: {} };
390
+ const nodeMeta = current.nodes[nodeId] ?? {};
391
+ const updated = updater({ ...nodeMeta });
392
+ this.runtimeState = { nodes: { ...current.nodes, [nodeId]: updated } };
393
+ }
374
394
  on(event, handler) {
375
395
  if (!this.listeners.has(event))
376
396
  this.listeners.set(event, new Set());
@@ -474,7 +494,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
474
494
  * Returns the mapping from original node IDs to new node IDs.
475
495
  * Uses copyOutputsFrom to copy outputs from original nodes (like duplicate does).
476
496
  */
477
- pasteCopiedData(data, center) {
497
+ pasteCopiedData(data, center, options) {
478
498
  const nodeIdMap = new Map();
479
499
  const edgeIds = [];
480
500
  // Add nodes
@@ -514,10 +534,7 @@ class InMemoryWorkbench extends AbstractWorkbench {
514
534
  }
515
535
  }
516
536
  // Select the newly pasted nodes
517
- this.setSelection({
518
- nodes: Array.from(nodeIdMap.values()),
519
- edges: edgeIds,
520
- });
537
+ this.setSelection({ nodes: Array.from(nodeIdMap.values()), edges: edgeIds }, options);
521
538
  return { nodeIdMap, edgeIds };
522
539
  }
523
540
  /**
@@ -679,14 +696,10 @@ class AbstractGraphRunner {
679
696
  async redo() {
680
697
  return false;
681
698
  }
682
- async canUndo() {
683
- return false;
684
- }
685
- async canRedo() {
686
- return false;
687
- }
688
699
  // Optional commit support
689
- async commit() { }
700
+ async commit(_reason) {
701
+ return undefined;
702
+ }
690
703
  }
691
704
 
692
705
  // Counter for generating readable runner IDs
@@ -1433,10 +1446,11 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1433
1446
  const client = await this.ensureClient();
1434
1447
  await client.setExtData(data);
1435
1448
  }
1436
- async commit() {
1449
+ async commit(reason) {
1437
1450
  const client = await this.ensureClient();
1438
1451
  try {
1439
- await client.commit();
1452
+ const history = await client.commit(reason);
1453
+ return history;
1440
1454
  }
1441
1455
  catch (err) {
1442
1456
  console.error("[RemoteGraphRunner] Error committing:", err);
@@ -1461,24 +1475,6 @@ class RemoteGraphRunner extends AbstractGraphRunner {
1461
1475
  return false;
1462
1476
  }
1463
1477
  }
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
- }
1482
1478
  async snapshotFull() {
1483
1479
  const client = await this.ensureClient();
1484
1480
  try {
@@ -1916,13 +1912,17 @@ function useWorkbenchBridge(wb) {
1916
1912
  wb.connect({
1917
1913
  source: { nodeId: params.source, handle: params.sourceHandle },
1918
1914
  target: { nodeId: params.target, handle: params.targetHandle },
1919
- });
1915
+ }, { commit: true });
1920
1916
  }, [wb]);
1921
1917
  const onNodesChange = React.useCallback((changes) => {
1922
1918
  // Apply position updates continuously, but mark commit only on drag end
1919
+ const positions = {};
1920
+ let commit = false;
1923
1921
  changes.forEach((c) => {
1924
1922
  if (c.type === "position" && c.position) {
1925
- wb.setPosition(c.id, c.position, { commit: !c.dragging });
1923
+ positions[c.id] = c.position;
1924
+ if (!c.dragging)
1925
+ commit = true;
1926
1926
  }
1927
1927
  });
1928
1928
  // Derive next node selection from change set
@@ -1953,13 +1953,13 @@ function useWorkbenchBridge(wb) {
1953
1953
  }
1954
1954
  }
1955
1955
  if (selectionChanged) {
1956
- wb.setSelection({
1957
- nodes: Array.from(nextNodeIds),
1958
- edges: current.edges,
1959
- });
1956
+ wb.setSelection({ nodes: Array.from(nextNodeIds), edges: current.edges }, { commit: !(Object.keys(positions).length && commit) });
1957
+ }
1958
+ if (Object.keys(positions).length > 0) {
1959
+ wb.setPositions(positions, { commit });
1960
1960
  }
1961
1961
  }, [wb]);
1962
- const onEdgesDelete = React.useCallback((edges) => edges.forEach((e) => wb.disconnect(e.id)), [wb]);
1962
+ const onEdgesDelete = React.useCallback((edges) => edges.forEach((e, idx) => wb.disconnect(e.id, { commit: idx === edges.length - 1 })), [wb]);
1963
1963
  const onEdgesChange = React.useCallback((changes) => {
1964
1964
  const current = wb.getSelection();
1965
1965
  const nextEdgeIds = new Set(current.edges);
@@ -1988,15 +1988,11 @@ function useWorkbenchBridge(wb) {
1988
1988
  }
1989
1989
  }
1990
1990
  if (selectionChanged) {
1991
- wb.setSelection({
1992
- nodes: current.nodes,
1993
- edges: Array.from(nextEdgeIds),
1994
- });
1991
+ wb.setSelection({ nodes: current.nodes, edges: Array.from(nextEdgeIds) }, { commit: true });
1995
1992
  }
1996
1993
  }, [wb]);
1997
1994
  const onNodesDelete = React.useCallback((nodes) => {
1998
- for (const n of nodes)
1999
- wb.removeNode(n.id);
1995
+ nodes.forEach((n, idx) => wb.removeNode(n.id, { commit: idx === nodes.length - 1 }));
2000
1996
  }, [wb]);
2001
1997
  return {
2002
1998
  onConnect,
@@ -2440,6 +2436,7 @@ async function download(wb, runner) {
2440
2436
  try {
2441
2437
  const def = wb.export();
2442
2438
  const uiState = wb.getUIState();
2439
+ const runtimeState = wb.getRuntimeState();
2443
2440
  let snapshot;
2444
2441
  if (runner.isRunning()) {
2445
2442
  const fullSnapshot = await runner.snapshotFull();
@@ -2449,6 +2446,7 @@ async function download(wb, runner) {
2449
2446
  extData: {
2450
2447
  ...(fullSnapshot.extData || {}),
2451
2448
  ui: uiState,
2449
+ runtime: runtimeState || undefined,
2452
2450
  },
2453
2451
  };
2454
2452
  }
@@ -2459,7 +2457,7 @@ async function download(wb, runner) {
2459
2457
  inputs,
2460
2458
  outputs: {},
2461
2459
  environment: {},
2462
- extData: { ui: uiState },
2460
+ extData: { ui: uiState, runtime: runtimeState || undefined },
2463
2461
  };
2464
2462
  }
2465
2463
  downloadJSON(snapshot, `spark-snapshot-${generateTimestamp()}.json`);
@@ -2485,6 +2483,9 @@ async function upload(parsed, wb, runner) {
2485
2483
  if (extData.ui && typeof extData.ui === "object") {
2486
2484
  wb.setUIState(extData.ui);
2487
2485
  }
2486
+ if (extData.runtime && typeof extData.runtime === "object") {
2487
+ wb.setRuntimeState(extData.runtime);
2488
+ }
2488
2489
  if (runner.isRunning()) {
2489
2490
  await runner.applySnapshotFull({
2490
2491
  def,
@@ -2512,6 +2513,18 @@ function useWorkbenchContext() {
2512
2513
  return ctx;
2513
2514
  }
2514
2515
 
2516
+ // Helper to compute invalidated status from runtime metadata
2517
+ function computeInvalidatedFromMetadata(metadata) {
2518
+ if (!metadata)
2519
+ return true;
2520
+ const { lastSuccessAt, lastInputAt, lastRunAt } = metadata;
2521
+ if (!lastSuccessAt && !lastRunAt)
2522
+ return true;
2523
+ if (!lastInputAt || Object.keys(lastInputAt).length === 0)
2524
+ return false;
2525
+ const maxInputTime = Math.max(...Object.values(lastInputAt));
2526
+ return maxInputTime > (lastSuccessAt ?? lastRunAt ?? 0);
2527
+ }
2515
2528
  function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVersion, children, }) {
2516
2529
  const [nodeStatus, setNodeStatus] = React.useState({});
2517
2530
  const [edgeStatus, setEdgeStatus] = React.useState({});
@@ -2602,27 +2615,35 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2602
2615
  }
2603
2616
  return out;
2604
2617
  }, [def, outputsMap, registry]);
2605
- // Initialize nodes as invalidated by default until first successful run
2618
+ // Initialize nodes and derive invalidated status from persisted metadata
2606
2619
  React.useEffect(() => {
2620
+ const workbenchRuntimeState = wb.getRuntimeState() ?? { nodes: {} };
2607
2621
  setNodeStatus((prev) => {
2608
2622
  const next = { ...prev };
2623
+ const metadata = workbenchRuntimeState;
2609
2624
  for (const n of def.nodes) {
2610
2625
  const cur = next[n.nodeId] ?? (next[n.nodeId] = {});
2626
+ const nodeMeta = metadata.nodes[n.nodeId];
2611
2627
  const updates = {};
2612
2628
  if (cur.invalidated === undefined) {
2613
- updates.invalidated = true;
2629
+ updates.invalidated = computeInvalidatedFromMetadata(nodeMeta);
2614
2630
  }
2615
- // Ensure activeRunIds is always initialized as an array
2616
2631
  if (cur.activeRunIds === undefined) {
2617
2632
  updates.activeRunIds = [];
2618
2633
  }
2634
+ if (cur.activeRuns === undefined) {
2635
+ updates.activeRuns = 0;
2636
+ }
2637
+ if (nodeMeta?.lastErrorSummary && cur.lastError === undefined) {
2638
+ updates.lastError = nodeMeta.lastErrorSummary;
2639
+ }
2619
2640
  if (Object.keys(updates).length > 0) {
2620
2641
  next[n.nodeId] = { ...cur, ...updates };
2621
2642
  }
2622
2643
  }
2623
2644
  return next;
2624
2645
  });
2625
- }, [def]);
2646
+ }, [def, wb]);
2626
2647
  // Auto layout (simple layered layout)
2627
2648
  const runAutoLayout = React.useCallback(() => {
2628
2649
  const cur = wb.export();
@@ -2689,10 +2710,31 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2689
2710
  }
2690
2711
  curX += maxWidth + H_GAP;
2691
2712
  }
2692
- wb.setPositions(pos, { commit: true });
2713
+ wb.setPositions(pos, { commit: true, reason: "auto-layout" });
2693
2714
  }, [wb, registry, overrides?.getDefaultNodeSize]);
2694
2715
  const updateEdgeType = React.useCallback((edgeId, typeId) => wb.updateEdgeType(edgeId, typeId), [wb]);
2695
2716
  const triggerExternal = React.useCallback((nodeId, event) => runner.triggerExternal(nodeId, event), [runner]);
2717
+ // Helper to save runtime metadata to extData.runtime and workbench state
2718
+ const saveRuntimeMetadata = React.useCallback(async () => {
2719
+ try {
2720
+ const current = wb.getRuntimeState() ?? { nodes: {} };
2721
+ const metadata = { nodes: { ...current.nodes } };
2722
+ // Clean up metadata for nodes that no longer exist
2723
+ const nodeIds = new Set(def.nodes.map((n) => n.nodeId));
2724
+ for (const nodeId of Object.keys(metadata.nodes)) {
2725
+ if (!nodeIds.has(nodeId)) {
2726
+ delete metadata.nodes[nodeId];
2727
+ }
2728
+ }
2729
+ // Save cleaned metadata to workbench state
2730
+ wb.setRuntimeState(metadata);
2731
+ // Save to extData.runtime via runner (no snapshotFull)
2732
+ await runner.setExtData?.({ runtime: metadata });
2733
+ }
2734
+ catch (err) {
2735
+ console.warn("[WorkbenchContext] Failed to save runtime metadata:", err);
2736
+ }
2737
+ }, [wb, def, runner]);
2696
2738
  // Subscribe to runner/workbench events
2697
2739
  React.useEffect(() => {
2698
2740
  const add = (source, type) => (payload) => setEvents((prev) => {
@@ -2735,9 +2777,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2735
2777
  wb.refreshValidation();
2736
2778
  };
2737
2779
  const offRunnerValue = runner.on("value", (e) => {
2780
+ const now = Date.now();
2738
2781
  if (e?.io === "input") {
2739
- const nodeId = e?.nodeId;
2740
- const handle = e?.handle;
2782
+ const nodeId = e.nodeId;
2783
+ const handle = e.handle;
2784
+ // Track input timestamp in workbench runtime state
2785
+ wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
2786
+ ...nodeMeta,
2787
+ lastInputAt: {
2788
+ ...(nodeMeta.lastInputAt ?? {}),
2789
+ [handle]: now,
2790
+ },
2791
+ }));
2741
2792
  setNodeStatus((s) => ({
2742
2793
  ...s,
2743
2794
  [nodeId]: { ...s[nodeId], invalidated: true },
@@ -2745,6 +2796,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2745
2796
  // Clear validation errors for this input when a valid value is set
2746
2797
  setInputValidationErrors((prev) => prev.filter((err) => !(err.nodeId === nodeId && err.handle === handle)));
2747
2798
  }
2799
+ else if (e?.io === "output") {
2800
+ const nodeId = e.nodeId;
2801
+ const handle = e.handle;
2802
+ // Track output timestamp in workbench runtime state
2803
+ wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
2804
+ ...nodeMeta,
2805
+ lastOutputAt: {
2806
+ ...(nodeMeta.lastOutputAt ?? {}),
2807
+ [handle]: now,
2808
+ },
2809
+ }));
2810
+ }
2748
2811
  return add("runner", "value")(e);
2749
2812
  });
2750
2813
  const offRunnerError = runner.on("error", (e) => {
@@ -2763,6 +2826,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2763
2826
  else if (nodeError.kind === "node-run" && nodeError.nodeId) {
2764
2827
  const nodeId = nodeError.nodeId;
2765
2828
  const runId = nodeError.runId;
2829
+ const now = Date.now();
2830
+ // Track error timestamp and summary in workbench runtime state
2831
+ const err = nodeError.err;
2832
+ let errorSummary;
2833
+ if (err && typeof err === "object") {
2834
+ const message = err.message || String(err);
2835
+ const code = err.code || err.statusCode;
2836
+ errorSummary = {
2837
+ message: typeof message === "string" ? message : String(message),
2838
+ code: typeof code === "number" ? code : undefined,
2839
+ };
2840
+ }
2841
+ wb.updateNodeRuntimeMetadata(nodeId, (nodeMeta) => ({
2842
+ ...nodeMeta,
2843
+ lastErrorAt: now,
2844
+ lastRunAt: now,
2845
+ ...(errorSummary ? { lastErrorSummary: errorSummary } : {}),
2846
+ }));
2766
2847
  setNodeStatus((s) => ({
2767
2848
  ...s,
2768
2849
  [nodeId]: {
@@ -2817,6 +2898,30 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2817
2898
  // If resolvedHandles are included in the event, use them directly (more efficient)
2818
2899
  if (e?.resolvedHandles && Object.keys(e.resolvedHandles).length > 0) {
2819
2900
  applyResolvedHandles(e.resolvedHandles);
2901
+ // Mark nodes whose handles changed as invalid
2902
+ const affectedNodeIds = Object.keys(e.resolvedHandles);
2903
+ if (affectedNodeIds.length > 0) {
2904
+ setNodeStatus((prev) => {
2905
+ const next = { ...prev };
2906
+ for (const id of affectedNodeIds) {
2907
+ const cur = next[id] ?? (next[id] = { activeRuns: 0, activeRunIds: [] });
2908
+ next[id] = { ...cur, invalidated: true };
2909
+ }
2910
+ return next;
2911
+ });
2912
+ }
2913
+ }
2914
+ // For broader invalidations (e.g. registry-changed, graph-updated), mark all nodes invalid
2915
+ if (e?.reason === "registry-changed" || e?.reason === "graph-updated") {
2916
+ setNodeStatus((prev) => {
2917
+ const next = { ...prev };
2918
+ for (const n of def.nodes) {
2919
+ const cur = next[n.nodeId] ??
2920
+ (next[n.nodeId] = { activeRuns: 0, activeRunIds: [] });
2921
+ next[n.nodeId] = { ...cur, invalidated: true };
2922
+ }
2923
+ return next;
2924
+ });
2820
2925
  }
2821
2926
  return add("runner", "invalidate")(e);
2822
2927
  });
@@ -2826,6 +2931,12 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2826
2931
  if (s.kind === "node-start") {
2827
2932
  const id = s.nodeId;
2828
2933
  const runId = s.runId;
2934
+ const now = Date.now();
2935
+ // Track run timestamp in workbench runtime state
2936
+ wb.updateNodeRuntimeMetadata(id, (nodeMeta) => ({
2937
+ ...nodeMeta,
2938
+ lastRunAt: now,
2939
+ }));
2829
2940
  // Validate runId is a non-empty string
2830
2941
  const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
2831
2942
  if (!isValidRunId) {
@@ -2848,7 +2959,7 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2848
2959
  };
2849
2960
  });
2850
2961
  // Start fallback animation window
2851
- setFallbackStarts((prev) => ({ ...prev, [id]: Date.now() }));
2962
+ setFallbackStarts((prev) => ({ ...prev, [id]: now }));
2852
2963
  }
2853
2964
  else if (s.kind === "node-progress") {
2854
2965
  const id = s.nodeId;
@@ -2863,8 +2974,21 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2863
2974
  else if (s.kind === "node-done") {
2864
2975
  const id = s.nodeId;
2865
2976
  const runId = s.runId;
2977
+ const now = Date.now();
2866
2978
  // Validate runId is a non-empty string
2867
2979
  const isValidRunId = runId && typeof runId === "string" && runId.length > 0;
2980
+ const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
2981
+ // Track success timestamp if no error in workbench runtime state
2982
+ if (!hadError) {
2983
+ wb.updateNodeRuntimeMetadata(id, (nodeMeta) => {
2984
+ const updated = { ...nodeMeta, lastSuccessAt: now };
2985
+ // Clear error summary on success
2986
+ if (updated.lastErrorSummary) {
2987
+ delete updated.lastErrorSummary;
2988
+ }
2989
+ return updated;
2990
+ });
2991
+ }
2868
2992
  setNodeStatus((prev) => {
2869
2993
  const current = prev[id]?.activeRuns ?? 0;
2870
2994
  const currentRunIds = prev[id]?.activeRunIds ?? [];
@@ -2876,7 +3000,6 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2876
3000
  const nextRunIds = isValidRunId
2877
3001
  ? currentRunIds.filter((rid) => rid !== runId)
2878
3002
  : currentRunIds;
2879
- const hadError = !!(isValidRunId && errorRunsRef.current[id]?.[runId]);
2880
3003
  const keepProgress = hadError || nextActive > 0;
2881
3004
  // Clear error flag for this runId
2882
3005
  if (isValidRunId && errorRunsRef.current[id]?.[runId]) {
@@ -2945,11 +3068,40 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2945
3068
  }
2946
3069
  });
2947
3070
  const offWbGraphChangedForUpdate = wb.on("graphChanged", async (event) => {
3071
+ // Build detailed reason from change type
3072
+ let reason = "graph-changed";
3073
+ if (event.change) {
3074
+ const changeType = event.change.type;
3075
+ if (changeType === "addNode") {
3076
+ reason = "add-node";
3077
+ }
3078
+ else if (changeType === "removeNode") {
3079
+ reason = "remove-node";
3080
+ }
3081
+ else if (changeType === "connect") {
3082
+ reason = "connect-edge";
3083
+ }
3084
+ else if (changeType === "disconnect") {
3085
+ reason = "disconnect-edge";
3086
+ }
3087
+ else if (changeType === "updateParams") {
3088
+ reason = "update-node-params";
3089
+ }
3090
+ else if (changeType === "updateEdgeType") {
3091
+ reason = "update-edge-type";
3092
+ }
3093
+ }
2948
3094
  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
- });
3095
+ if (event.commit) {
3096
+ await saveRuntimeMetadata();
3097
+ const history = await runner.commit(reason).catch((err) => {
3098
+ console.error("[WorkbenchContext] Error committing:", err);
3099
+ return undefined;
3100
+ });
3101
+ if (history) {
3102
+ wb.setHistory(history);
3103
+ }
3104
+ }
2953
3105
  return;
2954
3106
  }
2955
3107
  try {
@@ -2974,10 +3126,18 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2974
3126
  else {
2975
3127
  await runner.update(event.def, { dry: event.dry });
2976
3128
  }
2977
- // Wait for update to complete, then commit
2978
- await runner.commit().catch((err) => {
2979
- console.error("[WorkbenchContext] Error committing after update:", err);
2980
- });
3129
+ if (event.commit) {
3130
+ await saveRuntimeMetadata();
3131
+ const history = await runner
3132
+ .commit(event.reason ?? reason)
3133
+ .catch((err) => {
3134
+ console.error("[WorkbenchContext] Error committing after update:", err);
3135
+ return undefined;
3136
+ });
3137
+ if (history) {
3138
+ wb.setHistory(history);
3139
+ }
3140
+ }
2981
3141
  }
2982
3142
  catch (err) {
2983
3143
  console.error("[WorkbenchContext] Error updating graph:", err);
@@ -2987,17 +3147,49 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
2987
3147
  const offWbSelectionChanged = wb.on("selectionChanged", async (sel) => {
2988
3148
  setSelectedNodeId(sel.nodes?.[0]);
2989
3149
  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
- });
3150
+ if (sel.commit) {
3151
+ await saveRuntimeMetadata();
3152
+ const history = await runner
3153
+ .commit(sel.reason ?? "selection")
3154
+ .catch((err) => {
3155
+ console.error("[WorkbenchContext] Error committing selection change:", err);
3156
+ return undefined;
3157
+ });
3158
+ if (history) {
3159
+ wb.setHistory(history);
3160
+ }
3161
+ }
2994
3162
  });
2995
3163
  const offWbGraphUiChanged = wb.on("graphUiChanged", async (event) => {
2996
3164
  // Only commit if commit flag is true (e.g., drag end, not during dragging)
2997
3165
  if (event.commit) {
2998
- await runner.commit().catch((err) => {
3166
+ // Build detailed reason from change type
3167
+ let reason = "ui-changed";
3168
+ if (event.change) {
3169
+ const changeType = event.change.type;
3170
+ if (changeType === "moveNode") {
3171
+ reason = "move-node";
3172
+ }
3173
+ else if (changeType === "moveNodes") {
3174
+ reason = "move-nodes";
3175
+ }
3176
+ else if (changeType === "selection") {
3177
+ reason = "selection";
3178
+ }
3179
+ else if (changeType === "viewport") {
3180
+ reason = "viewport";
3181
+ }
3182
+ }
3183
+ await saveRuntimeMetadata();
3184
+ const history = await runner
3185
+ .commit(event.reason ?? reason)
3186
+ .catch((err) => {
2999
3187
  console.error("[WorkbenchContext] Error committing UI changes:", err);
3188
+ return undefined;
3000
3189
  });
3190
+ if (history) {
3191
+ wb.setHistory(history);
3192
+ }
3001
3193
  }
3002
3194
  });
3003
3195
  const offWbError = wb.on("error", add("workbench", "error"));
@@ -3018,11 +3210,24 @@ function WorkbenchProvider({ wb, runner, registry, setRegistry, overrides, uiVer
3018
3210
  console.error("Failed to handle registry changed event");
3019
3211
  }
3020
3212
  });
3021
- // Handle transport disconnect: reset runtime status when connection is lost
3213
+ // Handle transport changes: reset runtime status when connection is lost
3022
3214
  const offRunnerTransport = runner.on("transport", (t) => {
3023
3215
  if (t.state === "disconnected") {
3024
3216
  console.info("[WorkbenchContext] Transport disconnected, resetting node status");
3025
- setNodeStatus({});
3217
+ // Reinitialize node status with invalidated=true for all nodes
3218
+ setNodeStatus(() => {
3219
+ const next = {};
3220
+ const metadata = wb.getRuntimeState() ?? { nodes: {} };
3221
+ for (const n of def.nodes) {
3222
+ const nodeMeta = metadata.nodes[n.nodeId];
3223
+ next[n.nodeId] = {
3224
+ activeRuns: 0,
3225
+ activeRunIds: [],
3226
+ invalidated: computeInvalidatedFromMetadata(nodeMeta),
3227
+ };
3228
+ }
3229
+ return next;
3230
+ });
3026
3231
  setEdgeStatus({});
3027
3232
  setFallbackStarts({});
3028
3233
  errorRunsRef.current = {};
@@ -3211,6 +3416,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3211
3416
  try {
3212
3417
  const typeId = outputTypesMap?.[nodeId]?.[handleId];
3213
3418
  const raw = outputsMap?.[nodeId]?.[handleId];
3419
+ let newNodeId;
3214
3420
  if (!typeId || raw === undefined)
3215
3421
  return;
3216
3422
  const unwrap = (v) => sparkGraph.isTypedOutput(v) ? sparkGraph.getTypedOutputValue(v) : v;
@@ -3236,23 +3442,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3236
3442
  const nodeDesc = registry.nodes.get(singleTarget.nodeTypeId);
3237
3443
  const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, singleTarget.inputHandle);
3238
3444
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3239
- wb.addNode({
3445
+ newNodeId = wb.addNode({
3240
3446
  typeId: singleTarget.nodeTypeId,
3241
3447
  position: { x: pos.x + 180, y: pos.y },
3242
3448
  }, { inputs: { [singleTarget.inputHandle]: coerced } });
3243
- return;
3244
3449
  }
3245
- if (isArray && arrTarget) {
3450
+ else if (isArray && arrTarget) {
3246
3451
  const nodeDesc = registry.nodes.get(arrTarget.nodeTypeId);
3247
3452
  const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, arrTarget.inputHandle);
3248
3453
  const coerced = await coerceIfNeeded(typeId, inType, unwrap(raw));
3249
- wb.addNode({
3454
+ newNodeId = wb.addNode({
3250
3455
  typeId: arrTarget.nodeTypeId,
3251
3456
  position: { x: pos.x + 180, y: pos.y },
3252
3457
  }, { inputs: { [arrTarget.inputHandle]: coerced } });
3253
- return;
3254
3458
  }
3255
- if (isArray && elemTarget) {
3459
+ else if (isArray && elemTarget) {
3256
3460
  const nodeDesc = registry.nodes.get(elemTarget.nodeTypeId);
3257
3461
  const inType = sparkGraph.getInputTypeId(nodeDesc?.inputs, elemTarget.inputHandle);
3258
3462
  const src = unwrap(raw);
@@ -3264,19 +3468,21 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3264
3468
  for (let idx = 0; idx < coercedItems.length; idx++) {
3265
3469
  const col = idx % COLS;
3266
3470
  const row = Math.floor(idx / COLS);
3267
- wb.addNode({
3471
+ newNodeId = wb.addNode({
3268
3472
  typeId: elemTarget.nodeTypeId,
3269
3473
  position: { x: pos.x + (col + 1) * DX, y: pos.y + row * DY },
3270
3474
  }, { inputs: { [elemTarget.inputHandle]: coercedItems[idx] } });
3271
3475
  }
3272
- return;
3476
+ }
3477
+ if (newNodeId) {
3478
+ wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "bake" });
3273
3479
  }
3274
3480
  }
3275
3481
  catch { }
3276
3482
  };
3277
3483
  return {
3278
3484
  onDelete: () => {
3279
- wb.removeNode(nodeId);
3485
+ wb.removeNode(nodeId, { commit: true });
3280
3486
  onClose();
3281
3487
  },
3282
3488
  onDuplicate: async () => {
@@ -3301,10 +3507,7 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3301
3507
  dry: true,
3302
3508
  });
3303
3509
  // Select the newly duplicated node
3304
- wb.setSelection({
3305
- nodes: [newNodeId],
3306
- edges: [],
3307
- });
3510
+ wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate" });
3308
3511
  onClose();
3309
3512
  },
3310
3513
  onDuplicateWithEdges: async () => {
@@ -3337,10 +3540,9 @@ function createNodeContextMenuHandlers(nodeId, wb, runner, registry, outputsMap,
3337
3540
  }, { dry: true });
3338
3541
  }
3339
3542
  // Select the newly duplicated node and edges
3340
- wb.setSelection({
3341
- nodes: [newNodeId],
3342
- edges: [],
3343
- });
3543
+ if (newNodeId) {
3544
+ wb.setSelection({ nodes: [newNodeId], edges: [] }, { commit: true, reason: "duplicate-with-edges" });
3545
+ }
3344
3546
  onClose();
3345
3547
  },
3346
3548
  onRunPull: async () => {
@@ -3414,7 +3616,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
3414
3616
  onClose();
3415
3617
  },
3416
3618
  onDelete: () => {
3417
- wb.deleteSelection();
3619
+ wb.deleteSelection({ commit: true, reason: "delete-selection" });
3418
3620
  onClose();
3419
3621
  },
3420
3622
  onClose,
@@ -3423,7 +3625,7 @@ function createSelectionContextMenuHandlers(wb, onClose, getDefaultNodeSize, onC
3423
3625
  /**
3424
3626
  * Creates base default context menu handlers.
3425
3627
  */
3426
- function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData) {
3628
+ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, getCopiedData, clearCopiedData, history) {
3427
3629
  // Wrap paste handler to clear storage after paste
3428
3630
  const wrappedOnPaste = onPaste && getCopiedData && clearCopiedData
3429
3631
  ? (position) => {
@@ -3431,16 +3633,17 @@ function createDefaultContextMenuHandlers(onAddNode, onClose, onPaste, runner, g
3431
3633
  clearCopiedData();
3432
3634
  }
3433
3635
  : onPaste;
3434
- // Function to check if paste data exists (called dynamically when menu opens)
3435
3636
  const hasPasteData = getCopiedData ? () => !!getCopiedData() : undefined;
3637
+ const canUndo = history ? history.undoCount > 0 : undefined;
3638
+ const canRedo = history ? history.redoCount > 0 : undefined;
3436
3639
  return {
3437
3640
  onAddNode,
3438
3641
  onPaste: wrappedOnPaste,
3439
3642
  hasPasteData,
3440
3643
  onUndo: runner ? () => runner.undo().then(() => onClose()) : undefined,
3441
3644
  onRedo: runner ? () => runner.redo().then(() => onClose()) : undefined,
3442
- canUndo: runner ? () => runner.canUndo().catch(() => false) : undefined,
3443
- canRedo: runner ? () => runner.canRedo().catch(() => false) : undefined,
3645
+ canUndo,
3646
+ canRedo,
3444
3647
  onClose,
3445
3648
  };
3446
3649
  }
@@ -3964,6 +4167,16 @@ function DefaultNodeContent({ data, isConnectable, }) {
3964
4167
  } })] }));
3965
4168
  }
3966
4169
 
4170
+ // Helper to format shortcut for current platform
4171
+ function formatShortcut(shortcut) {
4172
+ const isMac = typeof navigator !== "undefined" &&
4173
+ navigator.userAgent.toLowerCase().includes("mac");
4174
+ return shortcut.replace(/⌘\/Ctrl/g, isMac ? "⌘" : "Ctrl");
4175
+ }
4176
+ function ContextMenuButton({ label, onClick, disabled = false, shortcut, enableKeyboardShortcuts = true, }) {
4177
+ return (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: onClick, disabled: disabled, children: [jsxRuntime.jsx("span", { children: label }), enableKeyboardShortcuts && shortcut && (jsxRuntime.jsx("span", { className: "text-gray-400 text-xs ml-4", children: formatShortcut(shortcut) }))] }));
4178
+ }
4179
+
3967
4180
  function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enableKeyboardShortcuts = true, keyboardShortcuts = {
3968
4181
  undo: "⌘/Ctrl + Z",
3969
4182
  redo: "⌘/Ctrl + Shift + Z",
@@ -3971,41 +4184,24 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
3971
4184
  }, }) {
3972
4185
  const rf = react.useReactFlow();
3973
4186
  const [query, setQuery] = React.useState("");
3974
- const [canUndo, setCanUndo] = React.useState(false);
3975
- const [canRedo, setCanRedo] = React.useState(false);
3976
4187
  const [hasPasteData, setHasPasteData] = React.useState(false);
3977
4188
  const q = query.trim().toLowerCase();
3978
4189
  const filteredIds = q
3979
4190
  ? nodeIds.filter((id) => id.toLowerCase().includes(q))
3980
4191
  : nodeIds;
3981
- // Check undo/redo availability and paste data when menu opens
4192
+ const canUndo = handlers.canUndo ?? false;
4193
+ const canRedo = handlers.canRedo ?? false;
3982
4194
  React.useEffect(() => {
3983
4195
  if (!open)
3984
4196
  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]);
4197
+ if (handlers.hasPasteData) {
4198
+ const result = handlers.hasPasteData();
4199
+ setHasPasteData(result);
4200
+ }
4201
+ else {
4202
+ setHasPasteData(false);
4203
+ }
4204
+ }, [open, handlers.hasPasteData]);
4009
4205
  const root = { __children: {} };
4010
4206
  for (const id of filteredIds) {
4011
4207
  const parts = id.split(".");
@@ -4070,12 +4266,6 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
4070
4266
  handlers.onPaste(p);
4071
4267
  handlers.onClose();
4072
4268
  };
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
- };
4079
4269
  const renderTree = (tree, path = []) => {
4080
4270
  const entries = Object.entries(tree?.__children ?? {}).sort((a, b) => a[0].localeCompare(b[0]));
4081
4271
  return (jsxRuntime.jsx("div", { children: entries.map(([key, child]) => {
@@ -4093,13 +4283,17 @@ function DefaultContextMenu({ open, clientPos, handlers, registry, nodeIds, enab
4093
4283
  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) => {
4094
4284
  e.preventDefault();
4095
4285
  e.stopPropagation();
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 &&
4286
+ }, children: [hasPasteData && handlers.onPaste && (jsxRuntime.jsx(ContextMenuButton, { label: "Paste", onClick: handlePaste, shortcut: keyboardShortcuts.paste, enableKeyboardShortcuts: enableKeyboardShortcuts })), (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.jsx(ContextMenuButton, { label: "Undo", onClick: handlers.onUndo, disabled: !canUndo, shortcut: keyboardShortcuts.undo, enableKeyboardShortcuts: enableKeyboardShortcuts })), handlers.onRedo && (jsxRuntime.jsx(ContextMenuButton, { label: "Redo", onClick: handlers.onRedo, disabled: !canRedo, shortcut: keyboardShortcuts.redo, enableKeyboardShortcuts: enableKeyboardShortcuts }))] })), hasPasteData &&
4097
4287
  handlers.onPaste &&
4098
4288
  !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" })) })] }));
4289
+ !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 rounded 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" })) })] }));
4100
4290
  }
4101
4291
 
4102
- function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, }) {
4292
+ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeableOutputs, enableKeyboardShortcuts = true, keyboardShortcuts = {
4293
+ copy: "⌘/Ctrl + C",
4294
+ duplicate: "⌘/Ctrl + D",
4295
+ delete: "Delete",
4296
+ }, }) {
4103
4297
  const ref = React.useRef(null);
4104
4298
  // outside click + ESC
4105
4299
  React.useEffect(() => {
@@ -4137,7 +4331,7 @@ function NodeContextMenu({ open, clientPos, nodeId, handlers, canRunPull, bakeab
4137
4331
  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) => {
4138
4332
  e.preventDefault();
4139
4333
  e.stopPropagation();
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)))] }))] }));
4334
+ }, children: [jsxRuntime.jsxs("div", { className: "px-2 py-1 font-semibold text-gray-700", children: ["Node (", nodeId, ")"] }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Duplicate", onClick: handlers.onDuplicate, shortcut: keyboardShortcuts.duplicate, enableKeyboardShortcuts: enableKeyboardShortcuts }), 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(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), 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)))] }))] }));
4141
4335
  }
4142
4336
 
4143
4337
  function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcuts = true, keyboardShortcuts = {
@@ -4170,12 +4364,6 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
4170
4364
  if (open)
4171
4365
  ref.current?.focus();
4172
4366
  }, [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
- };
4179
4367
  if (!open || !clientPos)
4180
4368
  return null;
4181
4369
  // Clamp menu position to viewport
@@ -4187,7 +4375,7 @@ function SelectionContextMenu({ open, clientPos, handlers, enableKeyboardShortcu
4187
4375
  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) => {
4188
4376
  e.preventDefault();
4189
4377
  e.stopPropagation();
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) }))] })] }));
4378
+ }, children: [jsxRuntime.jsx("div", { className: "px-2 py-1 font-semibold text-gray-700", children: "Selection" }), jsxRuntime.jsx(ContextMenuButton, { label: "Copy", onClick: handlers.onCopy, shortcut: keyboardShortcuts.copy, enableKeyboardShortcuts: enableKeyboardShortcuts }), jsxRuntime.jsx(ContextMenuButton, { label: "Delete", onClick: handlers.onDelete, shortcut: keyboardShortcuts.delete, enableKeyboardShortcuts: enableKeyboardShortcuts })] }));
4191
4379
  }
4192
4380
 
4193
4381
  const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, getDefaultNodeSize }, ref) => {
@@ -4195,7 +4383,6 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4195
4383
  const nodeValidation = validationByNode;
4196
4384
  const edgeValidation = validationByEdge.errors;
4197
4385
  const [registryVersion, setRegistryVersion] = React.useState(0);
4198
- // Keep stable references for nodes/edges to avoid unnecessary updates
4199
4386
  const prevNodesRef = React.useRef([]);
4200
4387
  const prevEdgesRef = React.useRef([]);
4201
4388
  function retainStabilityById(prev, next, isSame) {
@@ -4525,7 +4712,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4525
4712
  setNodeMenuOpen(false);
4526
4713
  setSelectionMenuOpen(false);
4527
4714
  };
4528
- const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs }), [wb]);
4715
+ const addNodeAt = React.useCallback(async (typeId, opts) => wb.addNode({ typeId, position: opts.position }, { inputs: opts.inputs, commit: true }), [wb]);
4529
4716
  const onCloseMenu = React.useCallback(() => {
4530
4717
  setMenuOpen(false);
4531
4718
  }, []);
@@ -4550,16 +4737,13 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4550
4737
  get: () => wb.getCopiedData(),
4551
4738
  set: (data) => wb.setCopiedData(data),
4552
4739
  };
4553
- const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu,
4554
- // Paste handler - checks storage dynamically when called
4555
- // Only provide handler if storage has data or might have data (for dynamic checking)
4556
- (position) => {
4740
+ const baseHandlers = createDefaultContextMenuHandlers(addNodeAt, onCloseMenu, (position) => {
4557
4741
  const data = storage.get();
4558
4742
  if (!data)
4559
4743
  return;
4560
- wb.pasteCopiedData(data, position);
4744
+ wb.pasteCopiedData(data, position, { commit: true, reason: "paste" });
4561
4745
  onCloseMenu();
4562
- }, runner, () => storage.get(), () => storage.set(null));
4746
+ }, runner, () => storage.get(), () => storage.set(null), wb.getHistory());
4563
4747
  if (overrides?.getDefaultContextMenuHandlers) {
4564
4748
  return overrides.getDefaultContextMenuHandlers(wb, baseHandlers);
4565
4749
  }
@@ -4578,9 +4762,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4578
4762
  }, runner);
4579
4763
  if (overrides?.getSelectionContextMenuHandlers) {
4580
4764
  const selection = wb.getSelection();
4581
- return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, {
4582
- getDefaultNodeSize: overrides.getDefaultNodeSize,
4583
- });
4765
+ return overrides.getSelectionContextMenuHandlers(wb, selection, baseHandlers, { getDefaultNodeSize: overrides.getDefaultNodeSize });
4584
4766
  }
4585
4767
  return baseHandlers;
4586
4768
  }, [wb, runner, overrides, onCloseSelectionMenu]);
@@ -4626,6 +4808,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4626
4808
  redo: "⌘/Ctrl + Shift + Z",
4627
4809
  copy: "⌘/Ctrl + C",
4628
4810
  paste: "⌘/Ctrl + V",
4811
+ duplicate: "⌘/Ctrl + D",
4629
4812
  delete: "Delete",
4630
4813
  };
4631
4814
  // Keyboard shortcut handler
@@ -4650,9 +4833,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4650
4833
  e.preventDefault();
4651
4834
  if (runner &&
4652
4835
  "onUndo" in defaultContextMenuHandlers &&
4653
- defaultContextMenuHandlers.onUndo) {
4654
- const canUndo = await runner.canUndo().catch(() => false);
4655
- if (canUndo) {
4836
+ defaultContextMenuHandlers.onUndo &&
4837
+ defaultContextMenuHandlers.canUndo) {
4838
+ if (defaultContextMenuHandlers.canUndo) {
4656
4839
  defaultContextMenuHandlers.onUndo();
4657
4840
  }
4658
4841
  }
@@ -4663,9 +4846,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4663
4846
  e.preventDefault();
4664
4847
  if (runner &&
4665
4848
  "onRedo" in defaultContextMenuHandlers &&
4666
- defaultContextMenuHandlers.onRedo) {
4667
- const canRedo = await runner.canRedo().catch(() => false);
4668
- if (canRedo) {
4849
+ defaultContextMenuHandlers.onRedo &&
4850
+ defaultContextMenuHandlers.canRedo) {
4851
+ if (defaultContextMenuHandlers.canRedo) {
4669
4852
  defaultContextMenuHandlers.onRedo();
4670
4853
  }
4671
4854
  }
@@ -4676,12 +4859,26 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4676
4859
  const selection = wb.getSelection();
4677
4860
  if (selection.nodes.length > 0 || selection.edges.length > 0) {
4678
4861
  e.preventDefault();
4679
- if (selectionContextMenuHandlers.onCopy) {
4862
+ // If single node selected, use node context menu handler; otherwise use selection handler
4863
+ if (selection.nodes.length === 1 && nodeContextMenuHandlers?.onCopy) {
4864
+ nodeContextMenuHandlers.onCopy();
4865
+ }
4866
+ else if (selectionContextMenuHandlers.onCopy) {
4680
4867
  selectionContextMenuHandlers.onCopy();
4681
4868
  }
4682
4869
  }
4683
4870
  return;
4684
4871
  }
4872
+ // Duplicate: Cmd/Ctrl + D
4873
+ if (modKey && key === "d" && !e.shiftKey && !e.altKey) {
4874
+ const selection = wb.getSelection();
4875
+ if (selection.nodes.length === 1 &&
4876
+ nodeContextMenuHandlers?.onDuplicate) {
4877
+ e.preventDefault();
4878
+ nodeContextMenuHandlers.onDuplicate();
4879
+ }
4880
+ return;
4881
+ }
4685
4882
  // Paste: Cmd/Ctrl + V
4686
4883
  if (modKey && key === "v" && !e.shiftKey && !e.altKey) {
4687
4884
  e.preventDefault();
@@ -4711,6 +4908,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4711
4908
  runner,
4712
4909
  defaultContextMenuHandlers,
4713
4910
  selectionContextMenuHandlers,
4911
+ nodeContextMenuHandlers,
4714
4912
  rfInstanceRef,
4715
4913
  ]);
4716
4914
  // Get custom renderers from UI extension registry (reactive to uiVersion changes)
@@ -4727,7 +4925,7 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4727
4925
  const onMoveEnd = React.useCallback(() => {
4728
4926
  if (rfInstanceRef.current) {
4729
4927
  const viewport = rfInstanceRef.current.getViewport();
4730
- wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom }, { commit: true });
4928
+ wb.setViewport({ x: viewport.x, y: viewport.y, zoom: viewport.zoom });
4731
4929
  }
4732
4930
  }, [wb]);
4733
4931
  const viewportRef = React.useRef(null);
@@ -4763,7 +4961,9 @@ const WorkbenchCanvas = React.forwardRef(({ showValues, toString, toElement, get
4763
4961
  ? { enableKeyboardShortcuts, keyboardShortcuts }
4764
4962
  : {}) })) : (jsxRuntime.jsx(DefaultContextMenu, { open: menuOpen, clientPos: menuPos, handlers: defaultContextMenuHandlers, registry: registry, nodeIds: nodeIds, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts })), !!nodeAtMenu &&
4765
4963
  nodeContextMenuHandlers &&
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 }))] }) }) }));
4964
+ (NodeContextMenuRenderer ? (jsxRuntime.jsx(NodeContextMenuRenderer, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, ...(enableKeyboardShortcuts !== false
4965
+ ? { enableKeyboardShortcuts, keyboardShortcuts }
4966
+ : {}) })) : (jsxRuntime.jsx(NodeContextMenu, { open: nodeMenuOpen, clientPos: nodeMenuPos, nodeId: nodeAtMenu, handlers: nodeContextMenuHandlers, canRunPull: canRunPull, bakeableOutputs: bakeableOutputs, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))), selectionMenuOpen && selectionMenuPos && (jsxRuntime.jsx(SelectionContextMenu, { open: selectionMenuOpen, clientPos: selectionMenuPos, handlers: selectionContextMenuHandlers, enableKeyboardShortcuts: enableKeyboardShortcuts, keyboardShortcuts: keyboardShortcuts }))] }) }) }));
4767
4967
  });
4768
4968
 
4769
4969
  function WorkbenchStudioCanvas({ setRegistry, autoScroll, onAutoScrollChange, example, onExampleChange, engine, onEngineChange, backendKind, onBackendKindChange, httpBaseUrl, onHttpBaseUrlChange, wsUrl, onWsUrlChange, debug, onDebugChange, showValues, onShowValuesChange, hideWorkbench, onHideWorkbenchChange, overrides, onInit, onChange, }) {