@marktoflow/gui 2.0.0-alpha.1

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 (165) hide show
  1. package/.turbo/turbo-build.log +26 -0
  2. package/.turbo/turbo-test.log +22 -0
  3. package/README.md +179 -0
  4. package/dist/client/assets/index-DwTI8opO.js +608 -0
  5. package/dist/client/assets/index-DwTI8opO.js.map +1 -0
  6. package/dist/client/assets/index-RoEdL6gO.css +1 -0
  7. package/dist/client/index.html +20 -0
  8. package/dist/client/vite.svg +9 -0
  9. package/dist/server/index.d.ts +3 -0
  10. package/dist/server/index.d.ts.map +1 -0
  11. package/dist/server/index.js +56 -0
  12. package/dist/server/index.js.map +1 -0
  13. package/dist/server/routes/ai.js +50 -0
  14. package/dist/server/routes/ai.js.map +1 -0
  15. package/dist/server/routes/execute.js +62 -0
  16. package/dist/server/routes/execute.js.map +1 -0
  17. package/dist/server/routes/workflows.js +99 -0
  18. package/dist/server/routes/workflows.js.map +1 -0
  19. package/dist/server/server/index.js +95 -0
  20. package/dist/server/server/index.js.map +1 -0
  21. package/dist/server/server/routes/ai.js +87 -0
  22. package/dist/server/server/routes/ai.js.map +1 -0
  23. package/dist/server/server/routes/execute.js +63 -0
  24. package/dist/server/server/routes/execute.js.map +1 -0
  25. package/dist/server/server/routes/tools.js +518 -0
  26. package/dist/server/server/routes/tools.js.map +1 -0
  27. package/dist/server/server/routes/workflows.js +99 -0
  28. package/dist/server/server/routes/workflows.js.map +1 -0
  29. package/dist/server/server/services/AIService.js +69 -0
  30. package/dist/server/server/services/AIService.js.map +1 -0
  31. package/dist/server/server/services/FileWatcher.js +60 -0
  32. package/dist/server/server/services/FileWatcher.js.map +1 -0
  33. package/dist/server/server/services/WorkflowService.js +363 -0
  34. package/dist/server/server/services/WorkflowService.js.map +1 -0
  35. package/dist/server/server/services/agents/claude-code-provider.js +250 -0
  36. package/dist/server/server/services/agents/claude-code-provider.js.map +1 -0
  37. package/dist/server/server/services/agents/claude-provider.js +204 -0
  38. package/dist/server/server/services/agents/claude-provider.js.map +1 -0
  39. package/dist/server/server/services/agents/copilot-provider.js +227 -0
  40. package/dist/server/server/services/agents/copilot-provider.js.map +1 -0
  41. package/dist/server/server/services/agents/demo-provider.js +167 -0
  42. package/dist/server/server/services/agents/demo-provider.js.map +1 -0
  43. package/dist/server/server/services/agents/index.js +31 -0
  44. package/dist/server/server/services/agents/index.js.map +1 -0
  45. package/dist/server/server/services/agents/ollama-provider.js +220 -0
  46. package/dist/server/server/services/agents/ollama-provider.js.map +1 -0
  47. package/dist/server/server/services/agents/prompts.js +436 -0
  48. package/dist/server/server/services/agents/prompts.js.map +1 -0
  49. package/dist/server/server/services/agents/registry.js +242 -0
  50. package/dist/server/server/services/agents/registry.js.map +1 -0
  51. package/dist/server/server/services/agents/types.js +6 -0
  52. package/dist/server/server/services/agents/types.js.map +1 -0
  53. package/dist/server/server/websocket/index.js +85 -0
  54. package/dist/server/server/websocket/index.js.map +1 -0
  55. package/dist/server/services/AIService.d.ts +30 -0
  56. package/dist/server/services/AIService.d.ts.map +1 -0
  57. package/dist/server/services/AIService.js +216 -0
  58. package/dist/server/services/AIService.js.map +1 -0
  59. package/dist/server/services/FileWatcher.d.ts +10 -0
  60. package/dist/server/services/FileWatcher.d.ts.map +1 -0
  61. package/dist/server/services/FileWatcher.js +62 -0
  62. package/dist/server/services/FileWatcher.js.map +1 -0
  63. package/dist/server/services/WorkflowService.d.ts +54 -0
  64. package/dist/server/services/WorkflowService.d.ts.map +1 -0
  65. package/dist/server/services/WorkflowService.js +323 -0
  66. package/dist/server/services/WorkflowService.js.map +1 -0
  67. package/dist/server/shared/constants.js +175 -0
  68. package/dist/server/shared/constants.js.map +1 -0
  69. package/dist/server/shared/types.js +3 -0
  70. package/dist/server/shared/types.js.map +1 -0
  71. package/dist/server/websocket/index.d.ts +10 -0
  72. package/dist/server/websocket/index.d.ts.map +1 -0
  73. package/dist/server/websocket/index.js +85 -0
  74. package/dist/server/websocket/index.js.map +1 -0
  75. package/index.html +19 -0
  76. package/package.json +96 -0
  77. package/playwright.config.ts +27 -0
  78. package/postcss.config.js +6 -0
  79. package/public/vite.svg +9 -0
  80. package/src/client/App.tsx +520 -0
  81. package/src/client/components/Canvas/Canvas.tsx +405 -0
  82. package/src/client/components/Canvas/ExecutionOverlay.tsx +847 -0
  83. package/src/client/components/Canvas/NodeContextMenu.tsx +188 -0
  84. package/src/client/components/Canvas/OutputNode.tsx +111 -0
  85. package/src/client/components/Canvas/StepNode.tsx +106 -0
  86. package/src/client/components/Canvas/SubWorkflowNode.tsx +141 -0
  87. package/src/client/components/Canvas/Toolbar.tsx +189 -0
  88. package/src/client/components/Canvas/TriggerNode.tsx +128 -0
  89. package/src/client/components/Editor/InputsEditor.tsx +458 -0
  90. package/src/client/components/Editor/NewStepWizard.tsx +344 -0
  91. package/src/client/components/Editor/StepEditor.tsx +532 -0
  92. package/src/client/components/Editor/YamlEditor.tsx +160 -0
  93. package/src/client/components/Panels/PropertiesPanel.tsx +589 -0
  94. package/src/client/components/Prompt/ChangePreview.tsx +281 -0
  95. package/src/client/components/Prompt/PromptHistoryPanel.tsx +209 -0
  96. package/src/client/components/Prompt/PromptInput.tsx +108 -0
  97. package/src/client/components/Sidebar/Sidebar.tsx +343 -0
  98. package/src/client/components/common/Breadcrumb.tsx +40 -0
  99. package/src/client/components/common/Button.tsx +68 -0
  100. package/src/client/components/common/ContextMenu.tsx +202 -0
  101. package/src/client/components/common/KeyboardShortcuts.tsx +143 -0
  102. package/src/client/components/common/Modal.tsx +93 -0
  103. package/src/client/components/common/Tabs.tsx +57 -0
  104. package/src/client/components/common/ThemeToggle.tsx +63 -0
  105. package/src/client/components/index.ts +32 -0
  106. package/src/client/hooks/index.ts +4 -0
  107. package/src/client/hooks/useAIPrompt.ts +108 -0
  108. package/src/client/hooks/useCanvas.ts +247 -0
  109. package/src/client/hooks/useWebSocket.ts +164 -0
  110. package/src/client/hooks/useWorkflow.ts +138 -0
  111. package/src/client/main.tsx +10 -0
  112. package/src/client/stores/canvasStore.ts +348 -0
  113. package/src/client/stores/editorStore.ts +133 -0
  114. package/src/client/stores/executionStore.ts +440 -0
  115. package/src/client/stores/index.ts +4 -0
  116. package/src/client/stores/layoutStore.ts +103 -0
  117. package/src/client/stores/navigationStore.ts +49 -0
  118. package/src/client/stores/promptStore.ts +113 -0
  119. package/src/client/stores/themeStore.ts +75 -0
  120. package/src/client/stores/workflowStore.ts +177 -0
  121. package/src/client/styles/globals.css +346 -0
  122. package/src/client/utils/cn.ts +9 -0
  123. package/src/client/utils/index.ts +4 -0
  124. package/src/client/utils/serviceIcons.tsx +64 -0
  125. package/src/client/utils/stepValidation.ts +155 -0
  126. package/src/client/utils/workflowToGraph.ts +299 -0
  127. package/src/server/index.ts +114 -0
  128. package/src/server/routes/ai.ts +91 -0
  129. package/src/server/routes/execute.ts +71 -0
  130. package/src/server/routes/tools.ts +564 -0
  131. package/src/server/routes/workflows.ts +106 -0
  132. package/src/server/services/AIService.ts +105 -0
  133. package/src/server/services/FileWatcher.ts +69 -0
  134. package/src/server/services/WorkflowService.ts +441 -0
  135. package/src/server/services/agents/claude-code-provider.ts +320 -0
  136. package/src/server/services/agents/claude-provider.ts +248 -0
  137. package/src/server/services/agents/copilot-provider.ts +311 -0
  138. package/src/server/services/agents/demo-provider.ts +184 -0
  139. package/src/server/services/agents/index.ts +31 -0
  140. package/src/server/services/agents/ollama-provider.ts +267 -0
  141. package/src/server/services/agents/prompts.ts +482 -0
  142. package/src/server/services/agents/registry.ts +289 -0
  143. package/src/server/services/agents/types.ts +146 -0
  144. package/src/server/websocket/index.ts +104 -0
  145. package/src/shared/constants.ts +180 -0
  146. package/src/shared/types.ts +179 -0
  147. package/tailwind.config.ts +73 -0
  148. package/tests/e2e/app.spec.ts +90 -0
  149. package/tests/e2e/canvas.spec.ts +128 -0
  150. package/tests/e2e/workflow.spec.ts +185 -0
  151. package/tests/integration/api.test.ts +250 -0
  152. package/tests/integration/testApp.ts +31 -0
  153. package/tests/setup.ts +37 -0
  154. package/tests/unit/canvasStore.test.ts +502 -0
  155. package/tests/unit/components.test.tsx +151 -0
  156. package/tests/unit/executionStore.test.ts +527 -0
  157. package/tests/unit/layoutStore.test.ts +194 -0
  158. package/tests/unit/navigationStore.test.ts +152 -0
  159. package/tests/unit/stepValidation.test.ts +226 -0
  160. package/tests/unit/themeStore.test.ts +141 -0
  161. package/tests/unit/workflowToGraph.test.ts +289 -0
  162. package/tsconfig.json +29 -0
  163. package/tsconfig.server.json +28 -0
  164. package/vite.config.ts +31 -0
  165. package/vitest.config.ts +26 -0
@@ -0,0 +1,348 @@
1
+ import { create } from 'zustand';
2
+ import {
3
+ type Node,
4
+ type Edge,
5
+ type OnNodesChange,
6
+ type OnEdgesChange,
7
+ type OnConnect,
8
+ applyNodeChanges,
9
+ applyEdgeChanges,
10
+ addEdge,
11
+ } from '@xyflow/react';
12
+
13
+ interface HistoryState {
14
+ nodes: Node[];
15
+ edges: Edge[];
16
+ }
17
+
18
+ interface ClipboardState {
19
+ nodes: Node[];
20
+ edges: Edge[];
21
+ }
22
+
23
+ interface CanvasState {
24
+ nodes: Node[];
25
+ edges: Edge[];
26
+ // History for undo/redo
27
+ past: HistoryState[];
28
+ future: HistoryState[];
29
+ maxHistorySize: number;
30
+ // Clipboard for copy/paste
31
+ clipboard: ClipboardState | null;
32
+ onNodesChange: OnNodesChange;
33
+ onEdgesChange: OnEdgesChange;
34
+ onConnect: OnConnect;
35
+ setNodes: (nodes: Node[]) => void;
36
+ setEdges: (edges: Edge[]) => void;
37
+ updateNodeData: (nodeId: string, data: Partial<any>) => void;
38
+ clearCanvas: () => void;
39
+ // Undo/redo methods
40
+ undo: () => void;
41
+ redo: () => void;
42
+ canUndo: () => boolean;
43
+ canRedo: () => boolean;
44
+ // Save checkpoint for history
45
+ saveCheckpoint: () => void;
46
+ // Copy/paste methods
47
+ copySelected: () => void;
48
+ paste: (offset?: { x: number; y: number }) => void;
49
+ canPaste: () => boolean;
50
+ }
51
+
52
+ // Demo data for initial view
53
+ const initialNodes: Node[] = [
54
+ {
55
+ id: 'trigger',
56
+ type: 'step',
57
+ position: { x: 250, y: 0 },
58
+ data: {
59
+ id: 'trigger',
60
+ name: 'PR Opened',
61
+ action: 'github.webhook',
62
+ status: 'completed',
63
+ },
64
+ },
65
+ {
66
+ id: 'fetch_pr',
67
+ type: 'step',
68
+ position: { x: 250, y: 120 },
69
+ data: {
70
+ id: 'fetch_pr',
71
+ name: 'Fetch PR Details',
72
+ action: 'github.pulls.get',
73
+ status: 'completed',
74
+ },
75
+ },
76
+ {
77
+ id: 'get_files',
78
+ type: 'step',
79
+ position: { x: 250, y: 240 },
80
+ data: {
81
+ id: 'get_files',
82
+ name: 'Get Changed Files',
83
+ action: 'github.pulls.listFiles',
84
+ status: 'running',
85
+ },
86
+ },
87
+ {
88
+ id: 'analyze',
89
+ type: 'step',
90
+ position: { x: 250, y: 360 },
91
+ data: {
92
+ id: 'analyze',
93
+ name: 'Analyze Changes',
94
+ action: 'claude.analyze',
95
+ status: 'pending',
96
+ },
97
+ },
98
+ {
99
+ id: 'post_review',
100
+ type: 'step',
101
+ position: { x: 250, y: 480 },
102
+ data: {
103
+ id: 'post_review',
104
+ name: 'Post Review',
105
+ action: 'github.pulls.createReview',
106
+ status: 'pending',
107
+ },
108
+ },
109
+ ];
110
+
111
+ const initialEdges: Edge[] = [
112
+ { id: 'e1', source: 'trigger', target: 'fetch_pr', animated: true },
113
+ { id: 'e2', source: 'fetch_pr', target: 'get_files', animated: true },
114
+ { id: 'e3', source: 'get_files', target: 'analyze' },
115
+ { id: 'e4', source: 'analyze', target: 'post_review' },
116
+ ];
117
+
118
+ export const useCanvasStore = create<CanvasState>((set, get) => ({
119
+ nodes: initialNodes,
120
+ edges: initialEdges,
121
+ past: [],
122
+ future: [],
123
+ maxHistorySize: 50,
124
+ clipboard: null,
125
+
126
+ onNodesChange: (changes) => {
127
+ // Filter out position changes for smoother dragging (don't create history for every pixel)
128
+ const isSignificantChange = changes.some(
129
+ (change) => change.type !== 'position' && change.type !== 'select' && change.type !== 'dimensions'
130
+ );
131
+
132
+ if (isSignificantChange) {
133
+ get().saveCheckpoint();
134
+ }
135
+
136
+ set({
137
+ nodes: applyNodeChanges(changes, get().nodes),
138
+ });
139
+ },
140
+
141
+ onEdgesChange: (changes) => {
142
+ // Check for significant changes (not just selection)
143
+ const isSignificantChange = changes.some(
144
+ (change) => change.type !== 'select'
145
+ );
146
+
147
+ if (isSignificantChange) {
148
+ get().saveCheckpoint();
149
+ }
150
+
151
+ set({
152
+ edges: applyEdgeChanges(changes, get().edges),
153
+ });
154
+ },
155
+
156
+ onConnect: (connection) => {
157
+ get().saveCheckpoint();
158
+ set({
159
+ edges: addEdge(
160
+ { ...connection, animated: true, style: { stroke: '#ff6d5a', strokeWidth: 2 } },
161
+ get().edges
162
+ ),
163
+ });
164
+ },
165
+
166
+ setNodes: (nodes) => {
167
+ get().saveCheckpoint();
168
+ set({ nodes, future: [] });
169
+ },
170
+
171
+ setEdges: (edges) => {
172
+ get().saveCheckpoint();
173
+ set({ edges, future: [] });
174
+ },
175
+
176
+ updateNodeData: (nodeId, data) => {
177
+ get().saveCheckpoint();
178
+ set({
179
+ nodes: get().nodes.map((node) =>
180
+ node.id === nodeId ? { ...node, data: { ...node.data, ...data } } : node
181
+ ),
182
+ future: [],
183
+ });
184
+ },
185
+
186
+ clearCanvas: () => {
187
+ get().saveCheckpoint();
188
+ set({ nodes: [], edges: [], future: [] });
189
+ },
190
+
191
+ saveCheckpoint: () => {
192
+ const { nodes, edges, past, maxHistorySize } = get();
193
+
194
+ // Don't save if there's no change from the last checkpoint
195
+ if (past.length > 0) {
196
+ const lastState = past[past.length - 1];
197
+ if (
198
+ JSON.stringify(lastState.nodes) === JSON.stringify(nodes) &&
199
+ JSON.stringify(lastState.edges) === JSON.stringify(edges)
200
+ ) {
201
+ return;
202
+ }
203
+ }
204
+
205
+ // Deep clone to avoid reference issues
206
+ const checkpoint: HistoryState = {
207
+ nodes: JSON.parse(JSON.stringify(nodes)),
208
+ edges: JSON.parse(JSON.stringify(edges)),
209
+ };
210
+
211
+ const newPast = [...past, checkpoint];
212
+
213
+ // Limit history size
214
+ if (newPast.length > maxHistorySize) {
215
+ newPast.shift();
216
+ }
217
+
218
+ set({ past: newPast });
219
+ },
220
+
221
+ undo: () => {
222
+ const { nodes, edges, past, future } = get();
223
+
224
+ if (past.length === 0) return;
225
+
226
+ // Save current state to future
227
+ const currentState: HistoryState = {
228
+ nodes: JSON.parse(JSON.stringify(nodes)),
229
+ edges: JSON.parse(JSON.stringify(edges)),
230
+ };
231
+
232
+ // Get the last state from past
233
+ const previousState = past[past.length - 1];
234
+ const newPast = past.slice(0, -1);
235
+
236
+ set({
237
+ nodes: previousState.nodes,
238
+ edges: previousState.edges,
239
+ past: newPast,
240
+ future: [currentState, ...future],
241
+ });
242
+ },
243
+
244
+ redo: () => {
245
+ const { nodes, edges, past, future } = get();
246
+
247
+ if (future.length === 0) return;
248
+
249
+ // Save current state to past
250
+ const currentState: HistoryState = {
251
+ nodes: JSON.parse(JSON.stringify(nodes)),
252
+ edges: JSON.parse(JSON.stringify(edges)),
253
+ };
254
+
255
+ // Get the first state from future
256
+ const nextState = future[0];
257
+ const newFuture = future.slice(1);
258
+
259
+ set({
260
+ nodes: nextState.nodes,
261
+ edges: nextState.edges,
262
+ past: [...past, currentState],
263
+ future: newFuture,
264
+ });
265
+ },
266
+
267
+ canUndo: () => get().past.length > 0,
268
+
269
+ canRedo: () => get().future.length > 0,
270
+
271
+ copySelected: () => {
272
+ const { nodes } = get();
273
+ const selectedNodes = nodes.filter((node) => node.selected);
274
+
275
+ if (selectedNodes.length === 0) return;
276
+
277
+ // Get the IDs of selected nodes
278
+ const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));
279
+
280
+ // Get edges that connect selected nodes
281
+ const { edges } = get();
282
+ const selectedEdges = edges.filter(
283
+ (edge) => selectedNodeIds.has(edge.source) && selectedNodeIds.has(edge.target)
284
+ );
285
+
286
+ // Deep clone to avoid reference issues
287
+ const clipboard: ClipboardState = {
288
+ nodes: JSON.parse(JSON.stringify(selectedNodes)),
289
+ edges: JSON.parse(JSON.stringify(selectedEdges)),
290
+ };
291
+
292
+ set({ clipboard });
293
+ },
294
+
295
+ paste: (offset = { x: 50, y: 50 }) => {
296
+ const { clipboard, nodes, edges } = get();
297
+
298
+ if (!clipboard || clipboard.nodes.length === 0) return;
299
+
300
+ get().saveCheckpoint();
301
+
302
+ // Create ID mapping for new nodes
303
+ const idMap = new Map<string, string>();
304
+ const timestamp = Date.now().toString(36);
305
+
306
+ // Create new nodes with unique IDs and offset positions
307
+ const newNodes: Node[] = clipboard.nodes.map((node, index) => {
308
+ const newId = `${node.id.split('-copy')[0]}-copy-${timestamp}-${index}`;
309
+ idMap.set(node.id, newId);
310
+
311
+ return {
312
+ ...node,
313
+ id: newId,
314
+ position: {
315
+ x: node.position.x + offset.x,
316
+ y: node.position.y + offset.y,
317
+ },
318
+ selected: true, // Select pasted nodes
319
+ data: {
320
+ ...node.data,
321
+ id: newId,
322
+ },
323
+ };
324
+ });
325
+
326
+ // Create new edges with updated source/target IDs
327
+ const newEdges: Edge[] = clipboard.edges.map((edge, index) => ({
328
+ ...edge,
329
+ id: `${edge.id.split('-copy')[0]}-copy-${timestamp}-${index}`,
330
+ source: idMap.get(edge.source) || edge.source,
331
+ target: idMap.get(edge.target) || edge.target,
332
+ }));
333
+
334
+ // Deselect existing nodes
335
+ const updatedNodes = nodes.map((node) => ({ ...node, selected: false }));
336
+
337
+ set({
338
+ nodes: [...updatedNodes, ...newNodes],
339
+ edges: [...edges, ...newEdges],
340
+ future: [],
341
+ });
342
+ },
343
+
344
+ canPaste: () => {
345
+ const { clipboard } = get();
346
+ return clipboard !== null && clipboard.nodes.length > 0;
347
+ },
348
+ }));
@@ -0,0 +1,133 @@
1
+ import { create } from 'zustand';
2
+ import type { WorkflowStep } from '@shared/types';
3
+
4
+ interface EditorState {
5
+ // Currently editing step
6
+ editingStep: WorkflowStep | null;
7
+ isEditorOpen: boolean;
8
+
9
+ // YAML viewer
10
+ yamlViewStep: WorkflowStep | null;
11
+ isYamlViewOpen: boolean;
12
+
13
+ // New step wizard
14
+ isNewStepOpen: boolean;
15
+ newStepPosition: { afterStepId?: string; beforeStepId?: string } | null;
16
+
17
+ // Clipboard
18
+ copiedNodes: WorkflowStep[];
19
+
20
+ // Undo/redo stacks
21
+ undoStack: WorkflowStep[][];
22
+ redoStack: WorkflowStep[][];
23
+
24
+ // Actions
25
+ openEditor: (step: WorkflowStep) => void;
26
+ closeEditor: () => void;
27
+ openYamlViewer: (step: WorkflowStep) => void;
28
+ closeYamlViewer: () => void;
29
+ openNewStepWizard: (position?: { afterStepId?: string; beforeStepId?: string }) => void;
30
+ closeNewStepWizard: () => void;
31
+ copyNodes: (steps: WorkflowStep[]) => void;
32
+ clearClipboard: () => void;
33
+ pushUndo: (steps: WorkflowStep[]) => void;
34
+ undo: () => WorkflowStep[] | null;
35
+ redo: () => WorkflowStep[] | null;
36
+ clearHistory: () => void;
37
+ }
38
+
39
+ export const useEditorStore = create<EditorState>((set, get) => ({
40
+ editingStep: null,
41
+ isEditorOpen: false,
42
+ yamlViewStep: null,
43
+ isYamlViewOpen: false,
44
+ isNewStepOpen: false,
45
+ newStepPosition: null,
46
+ copiedNodes: [],
47
+ undoStack: [],
48
+ redoStack: [],
49
+
50
+ openEditor: (step) =>
51
+ set({
52
+ editingStep: step,
53
+ isEditorOpen: true,
54
+ }),
55
+
56
+ closeEditor: () =>
57
+ set({
58
+ editingStep: null,
59
+ isEditorOpen: false,
60
+ }),
61
+
62
+ openYamlViewer: (step) =>
63
+ set({
64
+ yamlViewStep: step,
65
+ isYamlViewOpen: true,
66
+ }),
67
+
68
+ closeYamlViewer: () =>
69
+ set({
70
+ yamlViewStep: null,
71
+ isYamlViewOpen: false,
72
+ }),
73
+
74
+ openNewStepWizard: (position) =>
75
+ set({
76
+ isNewStepOpen: true,
77
+ newStepPosition: position ?? null,
78
+ }),
79
+
80
+ closeNewStepWizard: () =>
81
+ set({
82
+ isNewStepOpen: false,
83
+ newStepPosition: null,
84
+ }),
85
+
86
+ copyNodes: (steps) =>
87
+ set({
88
+ copiedNodes: steps.map((s) => ({ ...s })),
89
+ }),
90
+
91
+ clearClipboard: () =>
92
+ set({
93
+ copiedNodes: [],
94
+ }),
95
+
96
+ pushUndo: (steps) => {
97
+ const { undoStack } = get();
98
+ set({
99
+ undoStack: [...undoStack, steps],
100
+ redoStack: [], // Clear redo stack when new action is taken
101
+ });
102
+ },
103
+
104
+ undo: () => {
105
+ const { undoStack, redoStack } = get();
106
+ if (undoStack.length === 0) return null;
107
+
108
+ const lastState = undoStack[undoStack.length - 1];
109
+ set({
110
+ undoStack: undoStack.slice(0, -1),
111
+ redoStack: [...redoStack, lastState],
112
+ });
113
+ return lastState;
114
+ },
115
+
116
+ redo: () => {
117
+ const { undoStack, redoStack } = get();
118
+ if (redoStack.length === 0) return null;
119
+
120
+ const nextState = redoStack[redoStack.length - 1];
121
+ set({
122
+ redoStack: redoStack.slice(0, -1),
123
+ undoStack: [...undoStack, nextState],
124
+ });
125
+ return nextState;
126
+ },
127
+
128
+ clearHistory: () =>
129
+ set({
130
+ undoStack: [],
131
+ redoStack: [],
132
+ }),
133
+ }));