@evolve.labs/devflow 0.8.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.
Files changed (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. package/web/tsconfig.json +41 -0
@@ -0,0 +1,550 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+ import { toast } from 'sonner';
4
+ import type { FileNode, OpenFile } from '@/lib/types';
5
+ import { getExtension, getFileName, getLanguageFromExtension } from '@/lib/utils';
6
+
7
+ const MAX_RECENT_FILES = 20;
8
+ const MAX_HISTORY_SIZE = 50;
9
+
10
+ interface FileState {
11
+ // State
12
+ tree: FileNode | null;
13
+ openFiles: OpenFile[];
14
+ activeFile: string | null;
15
+ expandedFolders: Set<string>;
16
+ isLoading: boolean;
17
+ isSaving: boolean;
18
+ savingFile: string | null;
19
+ scrollToLine: number | null;
20
+
21
+ // Navigation state (US-019)
22
+ pinnedFiles: string[];
23
+ tabHistory: string[];
24
+ historyIndex: number;
25
+ recentFiles: string[];
26
+ closedTabs: string[];
27
+
28
+ // Actions
29
+ loadTree: (projectPath: string) => Promise<void>;
30
+ openFile: (path: string) => Promise<void>;
31
+ closeFile: (path: string) => void;
32
+ setActiveFile: (path: string | null) => void;
33
+ updateFileContent: (path: string, content: string) => void;
34
+ saveFile: (path: string) => Promise<void>;
35
+ createFile: (path: string, type: 'file' | 'directory', content?: string) => Promise<void>;
36
+ deleteFile: (path: string) => Promise<void>;
37
+ renameFile: (oldPath: string, newPath: string) => Promise<void>;
38
+ toggleFolder: (path: string) => void;
39
+ setExpandedFolders: (paths: Set<string>) => void;
40
+ setScrollToLine: (line: number | null) => void;
41
+
42
+ // Navigation actions (US-019)
43
+ navigateBack: () => void;
44
+ navigateForward: () => void;
45
+ canGoBack: () => boolean;
46
+ canGoForward: () => boolean;
47
+ togglePinned: (path: string) => void;
48
+ isPinned: (path: string) => boolean;
49
+ getRecentFiles: () => string[];
50
+ closeOtherTabs: (exceptPath: string) => void;
51
+ closeTabsToRight: (path: string) => void;
52
+ closeAllTabs: () => void;
53
+ reopenClosedTab: () => void;
54
+ copyPath: (path: string) => void;
55
+ }
56
+
57
+ export const useFileStore = create<FileState>()(
58
+ persist(
59
+ (set, get) => ({
60
+ tree: null,
61
+ openFiles: [],
62
+ activeFile: null,
63
+ expandedFolders: new Set(),
64
+ isLoading: false,
65
+ isSaving: false,
66
+ savingFile: null,
67
+ scrollToLine: null,
68
+
69
+ // Navigation state (US-019)
70
+ pinnedFiles: [],
71
+ tabHistory: [],
72
+ historyIndex: -1,
73
+ recentFiles: [],
74
+ closedTabs: [],
75
+
76
+ loadTree: async (projectPath: string) => {
77
+ set({ isLoading: true });
78
+
79
+ try {
80
+ const response = await fetch(
81
+ `/api/files/tree?path=${encodeURIComponent(projectPath)}`
82
+ );
83
+ const data = await response.json();
84
+
85
+ if (!response.ok) {
86
+ throw new Error(data.error || 'Failed to load file tree');
87
+ }
88
+
89
+ set({ tree: data.root, isLoading: false });
90
+ } catch (error) {
91
+ console.error('Failed to load tree:', error);
92
+ set({ isLoading: false });
93
+ }
94
+ },
95
+
96
+ openFile: async (path: string) => {
97
+ const { openFiles, pinnedFiles, tabHistory, historyIndex, recentFiles } = get();
98
+
99
+ // Helper to add to history and recent
100
+ const addToNavigation = () => {
101
+ // Add to tab history (truncate if navigated back)
102
+ const newHistory = tabHistory.slice(0, historyIndex + 1);
103
+ newHistory.push(path);
104
+ if (newHistory.length > MAX_HISTORY_SIZE) {
105
+ newHistory.shift();
106
+ }
107
+
108
+ // Add to recent files (no duplicates, max 20)
109
+ const newRecent = [path, ...recentFiles.filter((f) => f !== path)].slice(0, MAX_RECENT_FILES);
110
+
111
+ return {
112
+ tabHistory: newHistory,
113
+ historyIndex: newHistory.length - 1,
114
+ recentFiles: newRecent,
115
+ };
116
+ };
117
+
118
+ // Check if already open
119
+ const existing = openFiles.find((f) => f.path === path);
120
+ if (existing) {
121
+ set((state) => ({
122
+ activeFile: path,
123
+ ...addToNavigation(),
124
+ }));
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const response = await fetch(
130
+ `/api/files?path=${encodeURIComponent(path)}`
131
+ );
132
+ const data = await response.json();
133
+
134
+ if (!response.ok) {
135
+ throw new Error(data.error || 'Failed to read file');
136
+ }
137
+
138
+ const ext = getExtension(path);
139
+ const newFile: OpenFile = {
140
+ path,
141
+ name: getFileName(path),
142
+ content: data.content,
143
+ originalContent: data.content,
144
+ isDirty: false,
145
+ language: getLanguageFromExtension(ext),
146
+ };
147
+
148
+ // Sort files: pinned first, then by open order
149
+ const isPinned = pinnedFiles.includes(path);
150
+
151
+ set((state) => {
152
+ const newOpenFiles = [...state.openFiles, newFile];
153
+ // Sort: pinned first
154
+ newOpenFiles.sort((a, b) => {
155
+ const aPinned = state.pinnedFiles.includes(a.path);
156
+ const bPinned = state.pinnedFiles.includes(b.path);
157
+ if (aPinned && !bPinned) return -1;
158
+ if (!aPinned && bPinned) return 1;
159
+ return 0;
160
+ });
161
+
162
+ return {
163
+ openFiles: newOpenFiles,
164
+ activeFile: path,
165
+ ...addToNavigation(),
166
+ };
167
+ });
168
+ } catch (error) {
169
+ console.error('Failed to open file:', error);
170
+ }
171
+ },
172
+
173
+ closeFile: (path: string) => {
174
+ set((state) => {
175
+ const newOpenFiles = state.openFiles.filter((f) => f.path !== path);
176
+ let newActiveFile = state.activeFile;
177
+
178
+ // If closing active file, select another
179
+ if (state.activeFile === path) {
180
+ const index = state.openFiles.findIndex((f) => f.path === path);
181
+ if (newOpenFiles.length > 0) {
182
+ newActiveFile = newOpenFiles[Math.min(index, newOpenFiles.length - 1)].path;
183
+ } else {
184
+ newActiveFile = null;
185
+ }
186
+ }
187
+
188
+ // Add to closed tabs for reopen (max 10)
189
+ const newClosedTabs = [path, ...state.closedTabs.filter((f) => f !== path)].slice(0, 10);
190
+
191
+ // Remove from pinned if closing
192
+ const newPinnedFiles = state.pinnedFiles.filter((f) => f !== path);
193
+
194
+ return {
195
+ openFiles: newOpenFiles,
196
+ activeFile: newActiveFile,
197
+ closedTabs: newClosedTabs,
198
+ pinnedFiles: newPinnedFiles,
199
+ };
200
+ });
201
+ },
202
+
203
+ setActiveFile: (path: string | null) => {
204
+ set({ activeFile: path });
205
+ },
206
+
207
+ updateFileContent: (path: string, content: string) => {
208
+ set((state) => ({
209
+ openFiles: state.openFiles.map((f) =>
210
+ f.path === path
211
+ ? {
212
+ ...f,
213
+ content,
214
+ isDirty: content !== f.originalContent,
215
+ }
216
+ : f
217
+ ),
218
+ }));
219
+ },
220
+
221
+ saveFile: async (path: string) => {
222
+ const { openFiles } = get();
223
+ const file = openFiles.find((f) => f.path === path);
224
+
225
+ if (!file) return;
226
+
227
+ set({ isSaving: true, savingFile: path });
228
+
229
+ try {
230
+ const response = await fetch('/api/files', {
231
+ method: 'PUT',
232
+ headers: { 'Content-Type': 'application/json' },
233
+ body: JSON.stringify({ path, content: file.content }),
234
+ });
235
+
236
+ if (!response.ok) {
237
+ const data = await response.json();
238
+ throw new Error(data.error || 'Failed to save file');
239
+ }
240
+
241
+ set((state) => ({
242
+ openFiles: state.openFiles.map((f) =>
243
+ f.path === path
244
+ ? {
245
+ ...f,
246
+ originalContent: f.content,
247
+ isDirty: false,
248
+ }
249
+ : f
250
+ ),
251
+ isSaving: false,
252
+ savingFile: null,
253
+ }));
254
+
255
+ toast.success('File saved', {
256
+ description: getFileName(path),
257
+ });
258
+ } catch (error) {
259
+ set({ isSaving: false, savingFile: null });
260
+ const message = error instanceof Error ? error.message : 'Failed to save file';
261
+ toast.error('Save failed', {
262
+ description: message,
263
+ });
264
+ console.error('Failed to save file:', error);
265
+ }
266
+ },
267
+
268
+ createFile: async (path: string, type: 'file' | 'directory', content?: string) => {
269
+ try {
270
+ const response = await fetch('/api/files', {
271
+ method: 'POST',
272
+ headers: { 'Content-Type': 'application/json' },
273
+ body: JSON.stringify({ path, type, content }),
274
+ });
275
+
276
+ if (!response.ok) {
277
+ const data = await response.json();
278
+ throw new Error(data.error || 'Failed to create file');
279
+ }
280
+
281
+ toast.success(type === 'directory' ? 'Folder created' : 'File created', {
282
+ description: getFileName(path),
283
+ });
284
+ } catch (error) {
285
+ const message = error instanceof Error ? error.message : 'Failed to create file';
286
+ toast.error('Create failed', {
287
+ description: message,
288
+ });
289
+ console.error('Failed to create file:', error);
290
+ }
291
+ },
292
+
293
+ deleteFile: async (path: string) => {
294
+ try {
295
+ const response = await fetch('/api/files', {
296
+ method: 'DELETE',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify({ path }),
299
+ });
300
+
301
+ if (!response.ok) {
302
+ const data = await response.json();
303
+ throw new Error(data.error || 'Failed to delete file');
304
+ }
305
+
306
+ // Close if open
307
+ get().closeFile(path);
308
+
309
+ toast.success('Deleted', {
310
+ description: getFileName(path),
311
+ });
312
+ } catch (error) {
313
+ const message = error instanceof Error ? error.message : 'Failed to delete file';
314
+ toast.error('Delete failed', {
315
+ description: message,
316
+ });
317
+ console.error('Failed to delete file:', error);
318
+ }
319
+ },
320
+
321
+ renameFile: async (oldPath: string, newPath: string) => {
322
+ try {
323
+ const response = await fetch('/api/files', {
324
+ method: 'PATCH',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify({ oldPath, newPath }),
327
+ });
328
+
329
+ if (!response.ok) {
330
+ const data = await response.json();
331
+ throw new Error(data.error || 'Failed to rename file');
332
+ }
333
+
334
+ // Update open files if the renamed file was open
335
+ const { openFiles, activeFile } = get();
336
+ const renamedFile = openFiles.find((f) => f.path === oldPath);
337
+
338
+ if (renamedFile) {
339
+ const ext = getExtension(newPath);
340
+ set({
341
+ openFiles: openFiles.map((f) =>
342
+ f.path === oldPath
343
+ ? {
344
+ ...f,
345
+ path: newPath,
346
+ name: getFileName(newPath),
347
+ language: getLanguageFromExtension(ext),
348
+ }
349
+ : f
350
+ ),
351
+ activeFile: activeFile === oldPath ? newPath : activeFile,
352
+ });
353
+ }
354
+
355
+ toast.success('Renamed', {
356
+ description: `${getFileName(oldPath)} → ${getFileName(newPath)}`,
357
+ });
358
+ } catch (error) {
359
+ const message = error instanceof Error ? error.message : 'Failed to rename file';
360
+ toast.error('Rename failed', {
361
+ description: message,
362
+ });
363
+ console.error('Failed to rename file:', error);
364
+ }
365
+ },
366
+
367
+ toggleFolder: (path: string) => {
368
+ set((state) => {
369
+ const newExpanded = new Set(state.expandedFolders);
370
+ if (newExpanded.has(path)) {
371
+ newExpanded.delete(path);
372
+ } else {
373
+ newExpanded.add(path);
374
+ }
375
+ return { expandedFolders: newExpanded };
376
+ });
377
+ },
378
+
379
+ setExpandedFolders: (paths: Set<string>) => {
380
+ set({ expandedFolders: paths });
381
+ },
382
+
383
+ setScrollToLine: (line: number | null) => {
384
+ set({ scrollToLine: line });
385
+ },
386
+
387
+ // Navigation actions (US-019)
388
+ navigateBack: () => {
389
+ const { tabHistory, historyIndex, openFile } = get();
390
+ if (historyIndex > 0) {
391
+ const newIndex = historyIndex - 1;
392
+ const path = tabHistory[newIndex];
393
+ set({ historyIndex: newIndex, activeFile: path });
394
+ }
395
+ },
396
+
397
+ navigateForward: () => {
398
+ const { tabHistory, historyIndex } = get();
399
+ if (historyIndex < tabHistory.length - 1) {
400
+ const newIndex = historyIndex + 1;
401
+ const path = tabHistory[newIndex];
402
+ set({ historyIndex: newIndex, activeFile: path });
403
+ }
404
+ },
405
+
406
+ canGoBack: () => {
407
+ const { historyIndex } = get();
408
+ return historyIndex > 0;
409
+ },
410
+
411
+ canGoForward: () => {
412
+ const { tabHistory, historyIndex } = get();
413
+ return historyIndex < tabHistory.length - 1;
414
+ },
415
+
416
+ togglePinned: (path: string) => {
417
+ set((state) => {
418
+ const isPinned = state.pinnedFiles.includes(path);
419
+ const newPinnedFiles = isPinned
420
+ ? state.pinnedFiles.filter((f) => f !== path)
421
+ : [...state.pinnedFiles, path];
422
+
423
+ // Re-sort open files: pinned first
424
+ const newOpenFiles = [...state.openFiles].sort((a, b) => {
425
+ const aPinned = newPinnedFiles.includes(a.path);
426
+ const bPinned = newPinnedFiles.includes(b.path);
427
+ if (aPinned && !bPinned) return -1;
428
+ if (!aPinned && bPinned) return 1;
429
+ return 0;
430
+ });
431
+
432
+ toast.success(isPinned ? 'Tab unpinned' : 'Tab pinned', {
433
+ description: getFileName(path),
434
+ });
435
+
436
+ return {
437
+ pinnedFiles: newPinnedFiles,
438
+ openFiles: newOpenFiles,
439
+ };
440
+ });
441
+ },
442
+
443
+ isPinned: (path: string) => {
444
+ return get().pinnedFiles.includes(path);
445
+ },
446
+
447
+ getRecentFiles: () => {
448
+ return get().recentFiles;
449
+ },
450
+
451
+ closeOtherTabs: (exceptPath: string) => {
452
+ const { openFiles, pinnedFiles, closeFile } = get();
453
+
454
+ // Close all non-pinned tabs except the specified one
455
+ const tabsToClose = openFiles
456
+ .filter((f) => f.path !== exceptPath && !pinnedFiles.includes(f.path))
457
+ .map((f) => f.path);
458
+
459
+ tabsToClose.forEach((path) => {
460
+ get().closeFile(path);
461
+ });
462
+
463
+ toast.success('Closed other tabs', {
464
+ description: `${tabsToClose.length} tabs closed`,
465
+ });
466
+ },
467
+
468
+ closeTabsToRight: (path: string) => {
469
+ const { openFiles, pinnedFiles } = get();
470
+ const index = openFiles.findIndex((f) => f.path === path);
471
+
472
+ if (index === -1) return;
473
+
474
+ // Close all tabs to the right that are not pinned
475
+ const tabsToClose = openFiles
476
+ .slice(index + 1)
477
+ .filter((f) => !pinnedFiles.includes(f.path))
478
+ .map((f) => f.path);
479
+
480
+ tabsToClose.forEach((tabPath) => {
481
+ get().closeFile(tabPath);
482
+ });
483
+
484
+ if (tabsToClose.length > 0) {
485
+ toast.success('Closed tabs to right', {
486
+ description: `${tabsToClose.length} tabs closed`,
487
+ });
488
+ }
489
+ },
490
+
491
+ closeAllTabs: () => {
492
+ const { openFiles, pinnedFiles } = get();
493
+
494
+ // Close all non-pinned tabs
495
+ const tabsToClose = openFiles
496
+ .filter((f) => !pinnedFiles.includes(f.path))
497
+ .map((f) => f.path);
498
+
499
+ tabsToClose.forEach((path) => {
500
+ get().closeFile(path);
501
+ });
502
+
503
+ toast.success('Closed all tabs', {
504
+ description: `${tabsToClose.length} tabs closed`,
505
+ });
506
+ },
507
+
508
+ reopenClosedTab: () => {
509
+ const { closedTabs, openFile } = get();
510
+
511
+ if (closedTabs.length === 0) {
512
+ toast.info('No closed tabs to reopen');
513
+ return;
514
+ }
515
+
516
+ const pathToReopen = closedTabs[0];
517
+
518
+ // Remove from closed tabs
519
+ set((state) => ({
520
+ closedTabs: state.closedTabs.slice(1),
521
+ }));
522
+
523
+ // Open the file
524
+ openFile(pathToReopen);
525
+
526
+ toast.success('Tab reopened', {
527
+ description: getFileName(pathToReopen),
528
+ });
529
+ },
530
+
531
+ copyPath: (path: string) => {
532
+ navigator.clipboard.writeText(path).then(() => {
533
+ toast.success('Path copied', {
534
+ description: path,
535
+ });
536
+ }).catch(() => {
537
+ toast.error('Failed to copy path');
538
+ });
539
+ },
540
+ }),
541
+ {
542
+ name: 'devflow-file-store',
543
+ partialize: (state) => ({
544
+ pinnedFiles: state.pinnedFiles,
545
+ recentFiles: state.recentFiles,
546
+ closedTabs: state.closedTabs,
547
+ }),
548
+ }
549
+ )
550
+ );