@particle-academy/fancy-flow 0.17.0 → 0.19.0

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.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, ComponentType } from 'react';
3
3
  import { ReactFlowProps, Edge, BackgroundVariant, NodeProps, NodeTypes } from '@xyflow/react';
4
+ import { ConnectionValidatorOptions } from './registry/index.cjs';
5
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, PortCompatibility, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, createConnectionValidator, defaultConfigFor, defaultPortCompatibility, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.cjs';
4
6
  import { b as FlowNode, F as FlowGraph, e as NodeRunStatus, E as ExecutorRegistry } from './types-sOmpCitB.cjs';
5
7
  export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, a as FlowEdge, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, R as RunEvent, S as SubgraphNodeData, T as TriggerNodeData } from './types-sOmpCitB.cjs';
6
8
  import { WorkflowSchema, WorkflowMetadata } from './schema/index.cjs';
@@ -8,8 +10,7 @@ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW
8
10
  import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-D_jOR3Z2.cjs';
9
11
  export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-D_jOR3Z2.cjs';
10
12
  import { FlowRunFeedEntry } from './runtime/index.cjs';
11
- export { UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, useFlowRun, useFlowState } from './runtime/index.cjs';
12
- export { BUILTIN_KINDS, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, defaultConfigFor, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.cjs';
13
+ export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.cjs';
13
14
  export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-VKm57Hcj.cjs';
14
15
  export { L as LEGACY_PAUSE_PREFIXES, a as PAUSE_PREFIX, P as PauseAwaiting, b as PauseSignal, d as decodePause, e as encodePause, i as isPause, p as pauseForHuman } from './pause-9iT4tCEV.cjs';
15
16
  import './capabilities-ChE3Xi8R.cjs';
@@ -25,6 +26,14 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
25
26
  showMinimap?: boolean;
26
27
  /** Pixel height; FlowCanvas expects a sized container. Default 600. */
27
28
  height?: number | string;
29
+ /**
30
+ * Enforce port-type compatibility on new connections. `true` (default) uses
31
+ * the built-in validator: a connection is refused only when both ports
32
+ * declare a concrete, differing `type` (so untyped graphs are unaffected),
33
+ * and self-loops are blocked. Pass options to tune the rule, or `false` to
34
+ * disable. An `isValidConnection` you pass yourself always takes precedence.
35
+ */
36
+ validateConnections?: boolean | ConnectionValidatorOptions;
28
37
  /** Optional toolbar / palette etc. rendered above the canvas. */
29
38
  toolbar?: ReactNode;
30
39
  className?: string;
@@ -39,7 +48,7 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
39
48
  * (xyflow's standard pattern). The surrounding `useFlowState` hook in
40
49
  * `runtime/use-flow-state.ts` is a convenience that wires those for you.
41
50
  */
42
- declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
51
+ declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, validateConnections, isValidConnection, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
43
52
 
44
53
  /**
45
54
  * Everything the editor knows and can do, handed to every extension point.
@@ -100,6 +109,16 @@ type FlowEditorApi = {
100
109
  /** Open a file picker and load a workflow. */
101
110
  importWorkflow: () => void;
102
111
  fitView: () => void;
112
+ /** Undo the last committing edit (add / delete / connect / config / drag /
113
+ * import). Transient interactions (a drag in progress, selection) are not
114
+ * their own steps. */
115
+ undo: () => void;
116
+ /** Redo the last undone edit. */
117
+ redo: () => void;
118
+ /** True when there is an edit to undo. */
119
+ canUndo: boolean;
120
+ /** True when there is an undone edit to redo. */
121
+ canRedo: boolean;
103
122
  };
104
123
  /**
105
124
  * A declarative toolbar button. JSON-friendly on purpose — a host (or an
@@ -125,6 +144,8 @@ type FlowEditorAction = {
125
144
  type FlowEditorBuiltins = {
126
145
  run?: boolean;
127
146
  delete?: boolean;
147
+ /** Undo/redo toolbar buttons. Default true. */
148
+ history?: boolean;
128
149
  /** Right-click a node for Delete / Duplicate. Default true. */
129
150
  contextMenu?: boolean;
130
151
  /** Right-click a connection for Label / Delete. Default true. */
@@ -199,6 +220,17 @@ type FlowEditorProps = {
199
220
  onDelete?: (ids: string[]) => void;
200
221
  /** Called after connections are broken, with the deleted edge ids. */
201
222
  onEdgeDelete?: (ids: string[]) => void;
223
+ /**
224
+ * Stage destructive edits for human confirmation. When set, every delete path
225
+ * — keyboard, panel, context menu, and `api.deleteNodes`/`deleteEdges` —
226
+ * calls this first; return false to veto. Default: delete immediately. This
227
+ * realizes the component contract's "agents propose, humans confirm" on the
228
+ * canvas.
229
+ */
230
+ confirmDelete?: (targets: {
231
+ nodes: FlowNode[];
232
+ edges: Edge[];
233
+ }) => boolean | Promise<boolean>;
202
234
  className?: string;
203
235
  style?: CSSProperties;
204
236
  };
@@ -444,4 +476,4 @@ type FlowRunFeedProps = {
444
476
  /** FlowRunFeed — scrolling log panel. Auto-scrolls to bottom on new entries. */
445
477
  declare function FlowRunFeed({ entries, className, style }: FlowRunFeedProps): react.JSX.Element;
446
478
 
447
- export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
479
+ export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, ComponentType } from 'react';
3
3
  import { ReactFlowProps, Edge, BackgroundVariant, NodeProps, NodeTypes } from '@xyflow/react';
4
+ import { ConnectionValidatorOptions } from './registry/index.js';
5
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, PortCompatibility, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, createConnectionValidator, defaultConfigFor, defaultPortCompatibility, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.js';
4
6
  import { b as FlowNode, F as FlowGraph, e as NodeRunStatus, E as ExecutorRegistry } from './types-sOmpCitB.js';
5
7
  export { A as ActionNodeData, B as BaseNodeData, D as DecisionNodeData, a as FlowEdge, c as FlowNodeData, d as FlowNodeKind, N as NodeExecutor, f as NoteNodeData, O as OutputNodeData, P as PortDescriptor, R as RunEvent, S as SubgraphNodeData, T as TriggerNodeData } from './types-sOmpCitB.js';
6
8
  import { WorkflowSchema, WorkflowMetadata } from './schema/index.js';
@@ -8,8 +10,7 @@ export { ImportIssue, ImportOptions, ImportResult, WORKFLOW_SCHEMA_URL, WORKFLOW
8
10
  import { N as NodeCategory, a as NodeKindDefinition, C as ConfigField } from './types-NerkPtHS.js';
9
11
  export { D as DocumentConfigField, K as KeyValueConfigField, P as PortSpec, R as RenderBodyContext, d as RepeaterConfigField, e as RepeaterRowField } from './types-NerkPtHS.js';
10
12
  import { FlowRunFeedEntry } from './runtime/index.js';
11
- export { UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, useFlowRun, useFlowState } from './runtime/index.js';
12
- export { BUILTIN_KINDS, RegistryNode, RichInputAdapter, RichInputPreview, buildNodeTypes, categoryAccent, defaultConfigFor, getNodeKind, getRichInputAdapter, isRichInputEnabled, listNodeKinds, onNodeKindsChanged, onRichInputAdapterChanged, registerBuiltinKinds, registerNodeKind, registerRichInputAdapter, resolveNodePorts, resolvePortSpec, validateConfig } from './registry/index.js';
13
+ export { HistoryController, UseFlowHistoryReturn, UseFlowRunOptions, UseFlowRunReturn, UseFlowStateReturn, applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './runtime/index.js';
13
14
  export { R as RunOptions, a as RunResult, r as runFlow } from './run-flow-ChBi6q-W.js';
14
15
  export { L as LEGACY_PAUSE_PREFIXES, a as PAUSE_PREFIX, P as PauseAwaiting, b as PauseSignal, d as decodePause, e as encodePause, i as isPause, p as pauseForHuman } from './pause-9iT4tCEV.js';
15
16
  import './capabilities-DUhAc-EJ.js';
@@ -25,6 +26,14 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
25
26
  showMinimap?: boolean;
26
27
  /** Pixel height; FlowCanvas expects a sized container. Default 600. */
27
28
  height?: number | string;
29
+ /**
30
+ * Enforce port-type compatibility on new connections. `true` (default) uses
31
+ * the built-in validator: a connection is refused only when both ports
32
+ * declare a concrete, differing `type` (so untyped graphs are unaffected),
33
+ * and self-loops are blocked. Pass options to tune the rule, or `false` to
34
+ * disable. An `isValidConnection` you pass yourself always takes precedence.
35
+ */
36
+ validateConnections?: boolean | ConnectionValidatorOptions;
28
37
  /** Optional toolbar / palette etc. rendered above the canvas. */
29
38
  toolbar?: ReactNode;
30
39
  className?: string;
@@ -39,7 +48,7 @@ type FlowCanvasProps = Omit<ReactFlowProps<FlowNode, Edge>, "nodes" | "edges" |
39
48
  * (xyflow's standard pattern). The surrounding `useFlowState` hook in
40
49
  * `runtime/use-flow-state.ts` is a convenience that wires those for you.
41
50
  */
42
- declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
51
+ declare function FlowCanvas({ nodes, edges, background, showControls, showMinimap, height, validateConnections, isValidConnection, toolbar, nodeTypes, edgeTypes, className, style, ...rest }: FlowCanvasProps): react.JSX.Element;
43
52
 
44
53
  /**
45
54
  * Everything the editor knows and can do, handed to every extension point.
@@ -100,6 +109,16 @@ type FlowEditorApi = {
100
109
  /** Open a file picker and load a workflow. */
101
110
  importWorkflow: () => void;
102
111
  fitView: () => void;
112
+ /** Undo the last committing edit (add / delete / connect / config / drag /
113
+ * import). Transient interactions (a drag in progress, selection) are not
114
+ * their own steps. */
115
+ undo: () => void;
116
+ /** Redo the last undone edit. */
117
+ redo: () => void;
118
+ /** True when there is an edit to undo. */
119
+ canUndo: boolean;
120
+ /** True when there is an undone edit to redo. */
121
+ canRedo: boolean;
103
122
  };
104
123
  /**
105
124
  * A declarative toolbar button. JSON-friendly on purpose — a host (or an
@@ -125,6 +144,8 @@ type FlowEditorAction = {
125
144
  type FlowEditorBuiltins = {
126
145
  run?: boolean;
127
146
  delete?: boolean;
147
+ /** Undo/redo toolbar buttons. Default true. */
148
+ history?: boolean;
128
149
  /** Right-click a node for Delete / Duplicate. Default true. */
129
150
  contextMenu?: boolean;
130
151
  /** Right-click a connection for Label / Delete. Default true. */
@@ -199,6 +220,17 @@ type FlowEditorProps = {
199
220
  onDelete?: (ids: string[]) => void;
200
221
  /** Called after connections are broken, with the deleted edge ids. */
201
222
  onEdgeDelete?: (ids: string[]) => void;
223
+ /**
224
+ * Stage destructive edits for human confirmation. When set, every delete path
225
+ * — keyboard, panel, context menu, and `api.deleteNodes`/`deleteEdges` —
226
+ * calls this first; return false to veto. Default: delete immediately. This
227
+ * realizes the component contract's "agents propose, humans confirm" on the
228
+ * canvas.
229
+ */
230
+ confirmDelete?: (targets: {
231
+ nodes: FlowNode[];
232
+ edges: Edge[];
233
+ }) => boolean | Promise<boolean>;
202
234
  className?: string;
203
235
  style?: CSSProperties;
204
236
  };
@@ -444,4 +476,4 @@ type FlowRunFeedProps = {
444
476
  /** FlowRunFeed — scrolling log panel. Auto-scrolls to bottom on new entries. */
445
477
  declare function FlowRunFeed({ entries, className, style }: FlowRunFeedProps): react.JSX.Element;
446
478
 
447
- export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
479
+ export { ActionNode, ConfigField, ConfigFieldRenderer, type ConfigFieldRendererProps, ConnectionValidatorOptions, DecisionNode, ExecutorRegistry, FlowCanvas, type FlowCanvasProps, FlowEditor, type FlowEditorAction, type FlowEditorApi, type FlowEditorBuiltins, type FlowEditorProps, type FlowEditorSlots, FlowGraph, FlowNode, type FlowNodeRenderProps, FlowRunControls, type FlowRunControlsProps, FlowRunFeed, FlowRunFeedEntry, type FlowRunFeedProps, NodeCategory, NodeConfigPanel, type NodeConfigPanelProps, NodeKindDefinition, NodePalette, type NodePaletteProps, NodePort, type NodePortProps, type NodePortSide, type NodePortType, NodeRunStatus, NodeShell, type NodeShellProps, NoteNode, OutputNode, SubgraphNode, TriggerNode, WorkflowMetadata, WorkflowSchema, defaultNodeTypes, defineNode, paletteDropHandlers, useFlowEditor, useFlowEditorOptional };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { useFlowState, useFlowRun, applyStatusesToNodes } from './chunk-MFMRTRPO.js';
2
- export { applyStatusesToNodes, useFlowRun, useFlowState } from './chunk-MFMRTRPO.js';
3
- import { buildNodeTypes, registerBuiltinKinds } from './chunk-532A5QA3.js';
4
- export { BUILTIN_KINDS, RegistryNode, buildNodeTypes, registerBuiltinKinds } from './chunk-532A5QA3.js';
1
+ import { useFlowState, useFlowRun, useFlowHistory, applyStatusesToNodes } from './chunk-PHX4SBOD.js';
2
+ export { applyStatusesToNodes, createHistory, useFlowHistory, useFlowRun, useFlowState } from './chunk-PHX4SBOD.js';
3
+ import { buildNodeTypes, registerBuiltinKinds, createConnectionValidator } from './chunk-FSC64BPD.js';
4
+ export { ANY_PORT_TYPE, BUILTIN_KINDS, RegistryNode, buildNodeTypes, createConnectionValidator, defaultPortCompatibility, registerBuiltinKinds } from './chunk-FSC64BPD.js';
5
5
  import { ReactFlowProvider, useReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, Position, Handle, index, BackgroundVariant, Background, Controls, MiniMap } from './chunk-NF6NPY5N.js';
6
6
  export { LEGACY_PAUSE_PREFIXES, PAUSE_PREFIX, decodePause, encodePause, isPause, pauseForHuman } from './chunk-UEOE6B52.js';
7
7
  export { runFlow } from './chunk-L4AX73Q6.js';
@@ -151,6 +151,8 @@ function FlowCanvas({
151
151
  showControls = true,
152
152
  showMinimap = false,
153
153
  height = 600,
154
+ validateConnections = true,
155
+ isValidConnection,
154
156
  toolbar,
155
157
  nodeTypes,
156
158
  edgeTypes,
@@ -162,6 +164,16 @@ function FlowCanvas({
162
164
  () => ({ ...defaultNodeTypes, ...nodeTypes ?? {} }),
163
165
  [nodeTypes]
164
166
  );
167
+ const nodesRef = useRef(nodes);
168
+ nodesRef.current = nodes;
169
+ const builtinValidator = useMemo(
170
+ () => validateConnections === false ? void 0 : createConnectionValidator(
171
+ () => nodesRef.current,
172
+ validateConnections === true ? void 0 : validateConnections
173
+ ),
174
+ [validateConnections]
175
+ );
176
+ const resolvedIsValidConnection = isValidConnection ?? builtinValidator;
165
177
  const mergedEdgeTypes = useMemo(
166
178
  () => edgeTypes ? { ...edgeTypes } : void 0,
167
179
  [edgeTypes]
@@ -181,6 +193,7 @@ function FlowCanvas({
181
193
  proOptions: { hideAttribution: true },
182
194
  zoomActivationKeyCode: "Shift",
183
195
  preventScrolling: false,
196
+ isValidConnection: resolvedIsValidConnection,
184
197
  ...rest,
185
198
  children: [
186
199
  background !== "none" && /* @__PURE__ */ jsx(Background, { variant: background, gap: 20, size: 1, color: "rgba(0,0,0,0.18)" }),
@@ -865,13 +878,16 @@ function FlowEditorInner({
865
878
  onSelectionChange,
866
879
  onDelete,
867
880
  onEdgeDelete,
881
+ confirmDelete,
868
882
  apiRef
869
883
  }) {
870
884
  const internal = useFlowState(initial);
871
885
  const runner = useFlowRun();
872
886
  const rf = useReactFlow();
873
887
  const controlled = value !== void 0;
874
- const flow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
888
+ const baseFlow = controlled ? makeControlledFlowAdapter(value, onChange) : internal;
889
+ const hist = useFlowHistory(baseFlow);
890
+ const flow = hist.flow;
875
891
  const [, force] = useState(0);
876
892
  useEffect(() => onNodeKindsChanged(() => force((n) => n + 1)), []);
877
893
  const nodeTypes = useMemo(() => buildNodeTypes(), [listNodeKinds().length]);
@@ -924,6 +940,23 @@ function FlowEditorInner({
924
940
  window.removeEventListener("keydown", onKey);
925
941
  };
926
942
  }, [menu, labelEdit]);
943
+ useEffect(() => {
944
+ const onKey = (e) => {
945
+ const t = e.target;
946
+ if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
947
+ if (!(e.ctrlKey || e.metaKey)) return;
948
+ const key = e.key.toLowerCase();
949
+ if (key === "z" && !e.shiftKey) {
950
+ e.preventDefault();
951
+ hist.undo();
952
+ } else if (key === "z" && e.shiftKey || key === "y") {
953
+ e.preventDefault();
954
+ hist.redo();
955
+ }
956
+ };
957
+ window.addEventListener("keydown", onKey);
958
+ return () => window.removeEventListener("keydown", onKey);
959
+ }, [hist]);
927
960
  useEffect(() => {
928
961
  if (!controlled) onChange?.({ nodes: flow.nodes, edges: flow.edges });
929
962
  }, [flow.nodes, flow.edges, onChange, controlled]);
@@ -948,15 +981,32 @@ function FlowEditorInner({
948
981
  [flow]
949
982
  );
950
983
  const deleteNodes = useCallback(
951
- (ids) => {
984
+ async (ids) => {
952
985
  if (ids.length === 0) return;
986
+ const targets = flow.nodes.filter((n) => ids.includes(n.id));
987
+ if (targets.length === 0) return;
988
+ if (confirmDelete) {
989
+ const attached = flow.edges.filter((e) => ids.includes(e.source) || ids.includes(e.target));
990
+ if (!await confirmDelete({ nodes: targets, edges: attached })) return;
991
+ }
953
992
  const doomed = new Set(ids);
954
- flow.setNodes((all) => removeNodes({ nodes: all, edges: [] }, ids).nodes);
955
- flow.setEdges((all) => removeNodes({ nodes: [], edges: all }, ids).edges);
993
+ flow.setGraph(removeNodes({ nodes: flow.nodes, edges: flow.edges }, ids));
956
994
  setSelectedId((cur) => cur !== null && doomed.has(cur) ? null : cur);
957
995
  onDelete?.(ids);
958
996
  },
959
- [flow, onDelete]
997
+ [flow, onDelete, confirmDelete]
998
+ );
999
+ const deleteEdges = useCallback(
1000
+ async (ids) => {
1001
+ if (ids.length === 0) return;
1002
+ const targets = flow.edges.filter((e) => ids.includes(e.id));
1003
+ if (targets.length === 0) return;
1004
+ if (confirmDelete && !await confirmDelete({ nodes: [], edges: targets })) return;
1005
+ flow.setEdges((all) => removeEdges(all, ids));
1006
+ setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
1007
+ onEdgeDelete?.(ids);
1008
+ },
1009
+ [flow, onEdgeDelete, confirmDelete]
960
1010
  );
961
1011
  const api = useMemo(() => {
962
1012
  const toWorkflow = () => exportWorkflow({ nodes: flow.nodes, edges: flow.edges }, metadata);
@@ -976,18 +1026,8 @@ function FlowEditorInner({
976
1026
  updateNode: (next) => flow.setNodes((all) => all.map((x) => x.id === next.id ? next : x)),
977
1027
  deleteNodes,
978
1028
  deleteSelected: () => deleteNodes(selectedId ? [selectedId] : []),
979
- deleteEdges: (ids) => {
980
- if (ids.length === 0) return;
981
- flow.setEdges((all) => removeEdges(all, ids));
982
- setSelectedEdgeId((cur) => cur !== null && ids.includes(cur) ? null : cur);
983
- onEdgeDelete?.(ids);
984
- },
985
- deleteSelectedEdge: () => {
986
- if (!selectedEdgeId) return;
987
- flow.setEdges((all) => removeEdges(all, [selectedEdgeId]));
988
- setSelectedEdgeId(null);
989
- onEdgeDelete?.([selectedEdgeId]);
990
- },
1029
+ deleteEdges,
1030
+ deleteSelectedEdge: () => deleteEdges(selectedEdgeId ? [selectedEdgeId] : []),
991
1031
  setEdgeLabel: (id, label) => flow.setEdges((all) => setEdgeLabel(all, id, label)),
992
1032
  duplicateNode: (id) => {
993
1033
  const src = flow.nodes.find((n) => n.id === id);
@@ -997,22 +1037,20 @@ function FlowEditorInner({
997
1037
  setSelectedId(copy.id);
998
1038
  return copy.id;
999
1039
  },
1000
- setGraph: (graph) => {
1001
- flow.setNodes(graph.nodes);
1002
- flow.setEdges(graph.edges);
1003
- },
1040
+ setGraph: (graph) => flow.setGraph(graph),
1004
1041
  run: () => runner.run({ nodes: flow.nodes, edges: flow.edges }, executors),
1005
1042
  cancel: runner.cancel,
1006
1043
  reset: runner.reset,
1007
1044
  toWorkflow,
1008
1045
  exportWorkflow: () => downloadWorkflow(toWorkflow(), metadata),
1009
- importWorkflow: () => pickWorkflow((graph) => {
1010
- flow.setNodes(graph.nodes);
1011
- flow.setEdges(graph.edges);
1012
- }),
1013
- fitView: () => rf.fitView({ padding: 0.2 })
1046
+ importWorkflow: () => pickWorkflow((graph) => flow.setGraph(graph)),
1047
+ fitView: () => rf.fitView({ padding: 0.2 }),
1048
+ undo: hist.undo,
1049
+ redo: hist.redo,
1050
+ canUndo: hist.canUndo,
1051
+ canRedo: hist.canRedo
1014
1052
  };
1015
- }, [flow, selectedId, selected, runner, executors, metadata, addNode, deleteNodes, rf]);
1053
+ }, [flow, selectedId, selected, selectedEdgeId, selectedEdge, runner, executors, metadata, addNode, deleteNodes, deleteEdges, hist, rf]);
1016
1054
  useImperativeHandle(apiRef, () => api, [api]);
1017
1055
  const dropHandlers = paletteDropHandlers((kindName, evt) => {
1018
1056
  const point = rf.screenToFlowPosition({ x: evt.clientX, y: evt.clientY });
@@ -1023,6 +1061,31 @@ function FlowEditorInner({
1023
1061
  const toolbar = slots.toolbar ? slots.toolbar(api) : /* @__PURE__ */ jsxs(Fragment, { children: [
1024
1062
  startActions.map((a) => renderAction(a, api)),
1025
1063
  builtins.run !== false && /* @__PURE__ */ jsx(FlowRunControls, { running: api.running, onRun: api.run, onCancel: api.cancel, onReset: api.reset }),
1064
+ builtins.history !== false && /* @__PURE__ */ jsxs(Fragment, { children: [
1065
+ /* @__PURE__ */ jsx("span", { className: "ff-editor__sep" }),
1066
+ /* @__PURE__ */ jsx(
1067
+ "button",
1068
+ {
1069
+ className: "ff-editor__btn",
1070
+ "data-action": "undo",
1071
+ title: "Undo (Ctrl+Z)",
1072
+ disabled: !api.canUndo,
1073
+ onClick: api.undo,
1074
+ children: "\u21B6 Undo"
1075
+ }
1076
+ ),
1077
+ /* @__PURE__ */ jsx(
1078
+ "button",
1079
+ {
1080
+ className: "ff-editor__btn",
1081
+ "data-action": "redo",
1082
+ title: "Redo (Ctrl+Shift+Z)",
1083
+ disabled: !api.canRedo,
1084
+ onClick: api.redo,
1085
+ children: "\u21B7 Redo"
1086
+ }
1087
+ )
1088
+ ] }),
1026
1089
  (builtins.export !== false || builtins.import !== false) && /* @__PURE__ */ jsx("span", { className: "ff-editor__sep" }),
1027
1090
  builtins.export !== false && /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", "data-action": "export", onClick: api.exportWorkflow, children: "\u2193 Export" }),
1028
1091
  builtins.import !== false && /* @__PURE__ */ jsx("button", { className: "ff-editor__btn", "data-action": "import", onClick: api.importWorkflow, children: "\u2191 Import" }),
@@ -1047,12 +1110,14 @@ function FlowEditorInner({
1047
1110
  onNodesChange: flow.onNodesChange,
1048
1111
  onEdgesChange: flow.onEdgesChange,
1049
1112
  onConnect: flow.onConnect,
1113
+ onNodeDragStart: hist.onNodeDragStart,
1050
1114
  onNodeClick: handleNodeClick,
1051
1115
  onNodeContextMenu: builtins.contextMenu === false ? void 0 : handleNodeContextMenu,
1052
1116
  onEdgeClick: handleEdgeClick,
1053
1117
  onEdgeContextMenu: builtins.edgeContextMenu === false ? void 0 : handleEdgeContextMenu,
1054
1118
  onEdgesDelete: (deleted) => onEdgeDelete?.(deleted.map((e) => e.id)),
1055
1119
  onNodesDelete: (deleted) => onDelete?.(deleted.map((n) => n.id)),
1120
+ onBeforeDelete: confirmDelete ? async ({ nodes, edges }) => confirmDelete({ nodes, edges }) : void 0,
1056
1121
  deleteKeyCode: ["Delete", "Backspace"],
1057
1122
  height: "100%",
1058
1123
  toolbar,
@@ -1223,6 +1288,8 @@ function makeControlledFlowAdapter(value, onChange) {
1223
1288
  const nextEdges = typeof next === "function" ? next(value.edges) : next;
1224
1289
  apply({ nodes: value.nodes, edges: nextEdges });
1225
1290
  },
1291
+ // Atomic both-at-once commit — the reason this exists (see UseFlowStateReturn).
1292
+ setGraph: (graph) => apply(graph),
1226
1293
  onNodesChange: (changes) => {
1227
1294
  apply({ nodes: applyNodeChanges(changes, value.nodes), edges: value.edges });
1228
1295
  },