@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,386 @@
1
+ import { create } from 'zustand';
2
+ import { toast } from 'sonner';
3
+ import type { GitStatus, GitCommit, GitBranch } from '@/lib/git';
4
+
5
+ interface GitState {
6
+ // State
7
+ status: GitStatus | null;
8
+ commits: GitCommit[];
9
+ branches: GitBranch[];
10
+ isLoading: boolean;
11
+ error: string | null;
12
+ selectedFiles: Set<string>;
13
+ diffContent: string | null;
14
+ diffFile: string | null;
15
+
16
+ // Actions
17
+ fetchStatus: (projectPath: string) => Promise<void>;
18
+ fetchLog: (projectPath: string, maxCount?: number) => Promise<void>;
19
+ fetchBranches: (projectPath: string) => Promise<void>;
20
+ fetchDiff: (projectPath: string, file?: string, staged?: boolean) => Promise<void>;
21
+ stageFiles: (projectPath: string, files: string[]) => Promise<boolean>;
22
+ unstageFiles: (projectPath: string, files: string[]) => Promise<boolean>;
23
+ stageAll: (projectPath: string) => Promise<boolean>;
24
+ unstageAll: (projectPath: string) => Promise<boolean>;
25
+ commit: (projectPath: string, message: string) => Promise<{ success: boolean; hash?: string; error?: string }>;
26
+ push: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
27
+ pull: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
28
+ checkout: (projectPath: string, branch: string) => Promise<{ success: boolean; error?: string }>;
29
+ createBranch: (projectPath: string, branch: string) => Promise<{ success: boolean; error?: string }>;
30
+ discardChanges: (projectPath: string, files: string[]) => Promise<boolean>;
31
+ initRepo: (projectPath: string) => Promise<{ success: boolean; error?: string }>;
32
+ toggleFileSelection: (file: string) => void;
33
+ selectAllFiles: (files: string[]) => void;
34
+ clearSelection: () => void;
35
+ clearDiff: () => void;
36
+ setError: (error: string | null) => void;
37
+ }
38
+
39
+ export const useGitStore = create<GitState>((set, get) => ({
40
+ // Initial state
41
+ status: null,
42
+ commits: [],
43
+ branches: [],
44
+ isLoading: false,
45
+ error: null,
46
+ selectedFiles: new Set(),
47
+ diffContent: null,
48
+ diffFile: null,
49
+
50
+ // Actions
51
+ fetchStatus: async (projectPath: string) => {
52
+ set({ isLoading: true, error: null });
53
+ try {
54
+ const response = await fetch(`/api/git?action=status&projectPath=${encodeURIComponent(projectPath)}`);
55
+ if (!response.ok) throw new Error('Failed to fetch status');
56
+ const status = await response.json();
57
+ set({ status, isLoading: false });
58
+ } catch (error) {
59
+ set({ error: error instanceof Error ? error.message : 'Failed to fetch status', isLoading: false });
60
+ }
61
+ },
62
+
63
+ fetchLog: async (projectPath: string, maxCount: number = 50) => {
64
+ try {
65
+ const response = await fetch(`/api/git?action=log&projectPath=${encodeURIComponent(projectPath)}&maxCount=${maxCount}`);
66
+ if (!response.ok) throw new Error('Failed to fetch log');
67
+ const { commits } = await response.json();
68
+ set({ commits });
69
+ } catch (error) {
70
+ set({ error: error instanceof Error ? error.message : 'Failed to fetch log' });
71
+ }
72
+ },
73
+
74
+ fetchBranches: async (projectPath: string) => {
75
+ try {
76
+ const response = await fetch(`/api/git?action=branches&projectPath=${encodeURIComponent(projectPath)}`);
77
+ if (!response.ok) throw new Error('Failed to fetch branches');
78
+ const { branches } = await response.json();
79
+ set({ branches });
80
+ } catch (error) {
81
+ set({ error: error instanceof Error ? error.message : 'Failed to fetch branches' });
82
+ }
83
+ },
84
+
85
+ fetchDiff: async (projectPath: string, file?: string, staged: boolean = false) => {
86
+ try {
87
+ let url = `/api/git?action=diff&projectPath=${encodeURIComponent(projectPath)}&staged=${staged}`;
88
+ if (file) {
89
+ url += `&file=${encodeURIComponent(file)}`;
90
+ }
91
+ const response = await fetch(url);
92
+ if (!response.ok) throw new Error('Failed to fetch diff');
93
+ const { diff } = await response.json();
94
+ set({ diffContent: diff, diffFile: file || null });
95
+ } catch (error) {
96
+ set({ error: error instanceof Error ? error.message : 'Failed to fetch diff' });
97
+ }
98
+ },
99
+
100
+ stageFiles: async (projectPath: string, files: string[]) => {
101
+ try {
102
+ const response = await fetch('/api/git', {
103
+ method: 'POST',
104
+ headers: { 'Content-Type': 'application/json' },
105
+ body: JSON.stringify({ action: 'stage', projectPath, files }),
106
+ });
107
+ if (!response.ok) throw new Error('Failed to stage files');
108
+ const { success } = await response.json();
109
+ if (success) {
110
+ await get().fetchStatus(projectPath);
111
+ }
112
+ return success;
113
+ } catch (error) {
114
+ set({ error: error instanceof Error ? error.message : 'Failed to stage files' });
115
+ return false;
116
+ }
117
+ },
118
+
119
+ unstageFiles: async (projectPath: string, files: string[]) => {
120
+ try {
121
+ const response = await fetch('/api/git', {
122
+ method: 'POST',
123
+ headers: { 'Content-Type': 'application/json' },
124
+ body: JSON.stringify({ action: 'unstage', projectPath, files }),
125
+ });
126
+ if (!response.ok) throw new Error('Failed to unstage files');
127
+ const { success } = await response.json();
128
+ if (success) {
129
+ await get().fetchStatus(projectPath);
130
+ }
131
+ return success;
132
+ } catch (error) {
133
+ set({ error: error instanceof Error ? error.message : 'Failed to unstage files' });
134
+ return false;
135
+ }
136
+ },
137
+
138
+ stageAll: async (projectPath: string) => {
139
+ try {
140
+ const response = await fetch('/api/git', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({ action: 'stageAll', projectPath }),
144
+ });
145
+ if (!response.ok) throw new Error('Failed to stage all');
146
+ const { success } = await response.json();
147
+ if (success) {
148
+ await get().fetchStatus(projectPath);
149
+ }
150
+ return success;
151
+ } catch (error) {
152
+ set({ error: error instanceof Error ? error.message : 'Failed to stage all' });
153
+ return false;
154
+ }
155
+ },
156
+
157
+ unstageAll: async (projectPath: string) => {
158
+ try {
159
+ const response = await fetch('/api/git', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body: JSON.stringify({ action: 'unstageAll', projectPath }),
163
+ });
164
+ if (!response.ok) throw new Error('Failed to unstage all');
165
+ const { success } = await response.json();
166
+ if (success) {
167
+ await get().fetchStatus(projectPath);
168
+ }
169
+ return success;
170
+ } catch (error) {
171
+ set({ error: error instanceof Error ? error.message : 'Failed to unstage all' });
172
+ return false;
173
+ }
174
+ },
175
+
176
+ commit: async (projectPath: string, message: string) => {
177
+ set({ isLoading: true });
178
+ try {
179
+ const response = await fetch('/api/git', {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({ action: 'commit', projectPath, message }),
183
+ });
184
+ if (!response.ok) throw new Error('Failed to commit');
185
+ const result = await response.json();
186
+ if (result.success) {
187
+ await get().fetchStatus(projectPath);
188
+ await get().fetchLog(projectPath);
189
+ toast.success('Changes committed', {
190
+ description: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
191
+ });
192
+ }
193
+ set({ isLoading: false });
194
+ return result;
195
+ } catch (error) {
196
+ const errorMsg = error instanceof Error ? error.message : 'Failed to commit';
197
+ set({ error: errorMsg, isLoading: false });
198
+ toast.error('Commit failed', { description: errorMsg });
199
+ return { success: false, error: errorMsg };
200
+ }
201
+ },
202
+
203
+ push: async (projectPath: string) => {
204
+ set({ isLoading: true });
205
+ try {
206
+ const response = await fetch('/api/git', {
207
+ method: 'POST',
208
+ headers: { 'Content-Type': 'application/json' },
209
+ body: JSON.stringify({ action: 'push', projectPath }),
210
+ });
211
+ if (!response.ok) throw new Error('Failed to push');
212
+ const result = await response.json();
213
+ if (result.success) {
214
+ await get().fetchStatus(projectPath);
215
+ toast.success('Pushed to remote', {
216
+ description: 'Changes uploaded successfully',
217
+ });
218
+ }
219
+ set({ isLoading: false });
220
+ return result;
221
+ } catch (error) {
222
+ const errorMsg = error instanceof Error ? error.message : 'Failed to push';
223
+ set({ error: errorMsg, isLoading: false });
224
+ toast.error('Push failed', { description: errorMsg });
225
+ return { success: false, error: errorMsg };
226
+ }
227
+ },
228
+
229
+ pull: async (projectPath: string) => {
230
+ set({ isLoading: true });
231
+ try {
232
+ const response = await fetch('/api/git', {
233
+ method: 'POST',
234
+ headers: { 'Content-Type': 'application/json' },
235
+ body: JSON.stringify({ action: 'pull', projectPath }),
236
+ });
237
+ if (!response.ok) throw new Error('Failed to pull');
238
+ const result = await response.json();
239
+ if (result.success) {
240
+ await get().fetchStatus(projectPath);
241
+ await get().fetchLog(projectPath);
242
+ toast.success('Pulled from remote', {
243
+ description: 'Changes downloaded successfully',
244
+ });
245
+ }
246
+ set({ isLoading: false });
247
+ return result;
248
+ } catch (error) {
249
+ const errorMsg = error instanceof Error ? error.message : 'Failed to pull';
250
+ set({ error: errorMsg, isLoading: false });
251
+ toast.error('Pull failed', { description: errorMsg });
252
+ return { success: false, error: errorMsg };
253
+ }
254
+ },
255
+
256
+ checkout: async (projectPath: string, branch: string) => {
257
+ set({ isLoading: true });
258
+ try {
259
+ const response = await fetch('/api/git', {
260
+ method: 'POST',
261
+ headers: { 'Content-Type': 'application/json' },
262
+ body: JSON.stringify({ action: 'checkout', projectPath, branch }),
263
+ });
264
+ if (!response.ok) throw new Error('Failed to checkout');
265
+ const result = await response.json();
266
+ if (result.success) {
267
+ await get().fetchStatus(projectPath);
268
+ await get().fetchBranches(projectPath);
269
+ toast.success('Branch switched', {
270
+ description: `Now on ${branch}`,
271
+ });
272
+ }
273
+ set({ isLoading: false });
274
+ return result;
275
+ } catch (error) {
276
+ const errorMsg = error instanceof Error ? error.message : 'Failed to checkout';
277
+ set({ error: errorMsg, isLoading: false });
278
+ toast.error('Checkout failed', { description: errorMsg });
279
+ return { success: false, error: errorMsg };
280
+ }
281
+ },
282
+
283
+ createBranch: async (projectPath: string, branch: string) => {
284
+ set({ isLoading: true });
285
+ try {
286
+ const response = await fetch('/api/git', {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({ action: 'createBranch', projectPath, branch }),
290
+ });
291
+ if (!response.ok) throw new Error('Failed to create branch');
292
+ const result = await response.json();
293
+ if (result.success) {
294
+ await get().fetchStatus(projectPath);
295
+ await get().fetchBranches(projectPath);
296
+ toast.success('Branch created', {
297
+ description: branch,
298
+ });
299
+ }
300
+ set({ isLoading: false });
301
+ return result;
302
+ } catch (error) {
303
+ const errorMsg = error instanceof Error ? error.message : 'Failed to create branch';
304
+ set({ error: errorMsg, isLoading: false });
305
+ toast.error('Create branch failed', { description: errorMsg });
306
+ return { success: false, error: errorMsg };
307
+ }
308
+ },
309
+
310
+ discardChanges: async (projectPath: string, files: string[]) => {
311
+ try {
312
+ const response = await fetch('/api/git', {
313
+ method: 'POST',
314
+ headers: { 'Content-Type': 'application/json' },
315
+ body: JSON.stringify({ action: 'discard', projectPath, files }),
316
+ });
317
+ if (!response.ok) throw new Error('Failed to discard changes');
318
+ const { success } = await response.json();
319
+ if (success) {
320
+ await get().fetchStatus(projectPath);
321
+ toast.success('Changes discarded', {
322
+ description: `${files.length} file${files.length > 1 ? 's' : ''} reverted`,
323
+ });
324
+ }
325
+ return success;
326
+ } catch (error) {
327
+ const errorMsg = error instanceof Error ? error.message : 'Failed to discard changes';
328
+ set({ error: errorMsg });
329
+ toast.error('Discard failed', { description: errorMsg });
330
+ return false;
331
+ }
332
+ },
333
+
334
+ initRepo: async (projectPath: string) => {
335
+ set({ isLoading: true });
336
+ try {
337
+ const response = await fetch('/api/git', {
338
+ method: 'POST',
339
+ headers: { 'Content-Type': 'application/json' },
340
+ body: JSON.stringify({ action: 'init', projectPath }),
341
+ });
342
+ if (!response.ok) throw new Error('Failed to init repo');
343
+ const result = await response.json();
344
+ if (result.success) {
345
+ await get().fetchStatus(projectPath);
346
+ toast.success('Repository initialized', {
347
+ description: 'Git repository created successfully',
348
+ });
349
+ }
350
+ set({ isLoading: false });
351
+ return result;
352
+ } catch (error) {
353
+ const errorMsg = error instanceof Error ? error.message : 'Failed to init repo';
354
+ set({ error: errorMsg, isLoading: false });
355
+ toast.error('Init failed', { description: errorMsg });
356
+ return { success: false, error: errorMsg };
357
+ }
358
+ },
359
+
360
+ toggleFileSelection: (file: string) => {
361
+ const { selectedFiles } = get();
362
+ const newSelection = new Set(selectedFiles);
363
+ if (newSelection.has(file)) {
364
+ newSelection.delete(file);
365
+ } else {
366
+ newSelection.add(file);
367
+ }
368
+ set({ selectedFiles: newSelection });
369
+ },
370
+
371
+ selectAllFiles: (files: string[]) => {
372
+ set({ selectedFiles: new Set(files) });
373
+ },
374
+
375
+ clearSelection: () => {
376
+ set({ selectedFiles: new Set() });
377
+ },
378
+
379
+ clearDiff: () => {
380
+ set({ diffContent: null, diffFile: null });
381
+ },
382
+
383
+ setError: (error: string | null) => {
384
+ set({ error });
385
+ },
386
+ }));
@@ -0,0 +1,196 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+ import type { ProjectInfo, HealthStatus } from '@/lib/types';
4
+
5
+ interface RecentProject {
6
+ path: string;
7
+ name: string;
8
+ lastOpened: Date;
9
+ }
10
+
11
+ interface ProjectState {
12
+ // State
13
+ projects: ProjectInfo[];
14
+ activeProjectPath: string | null;
15
+ currentProject: ProjectInfo | null; // Derived from projects + activeProjectPath
16
+ recentProjects: RecentProject[];
17
+ health: HealthStatus | null;
18
+ isLoading: boolean;
19
+ error: string | null;
20
+
21
+ // Actions
22
+ addProject: (path: string) => Promise<void>;
23
+ removeProject: (path: string) => void;
24
+ setActiveProject: (path: string) => void;
25
+ openProject: (path: string) => Promise<void>;
26
+ restoreProjects: () => Promise<void>;
27
+ closeProject: () => void;
28
+ refreshHealth: () => Promise<void>;
29
+ setError: (error: string | null) => void;
30
+ }
31
+
32
+ function deriveCurrentProject(projects: ProjectInfo[], activeProjectPath: string | null): ProjectInfo | null {
33
+ if (!activeProjectPath) return projects[0] || null;
34
+ return projects.find(p => p.path === activeProjectPath) || projects[0] || null;
35
+ }
36
+
37
+ export const useProjectStore = create<ProjectState>()(
38
+ persist(
39
+ (set, get) => ({
40
+ projects: [],
41
+ activeProjectPath: null,
42
+ currentProject: null,
43
+ recentProjects: [],
44
+ health: null,
45
+ isLoading: false,
46
+ error: null,
47
+
48
+ addProject: async (projectPath: string) => {
49
+ const existing = get().projects;
50
+ // If already added, just activate it
51
+ if (existing.some(p => p.path === projectPath)) {
52
+ set({
53
+ activeProjectPath: projectPath,
54
+ currentProject: deriveCurrentProject(existing, projectPath),
55
+ });
56
+ return;
57
+ }
58
+
59
+ set({ isLoading: true, error: null });
60
+
61
+ try {
62
+ const response = await fetch('/api/project/open', {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ path: projectPath }),
66
+ });
67
+
68
+ const data = await response.json();
69
+
70
+ if (!response.ok) {
71
+ throw new Error(data.error || 'Failed to open project');
72
+ }
73
+
74
+ const project = data.project as ProjectInfo;
75
+
76
+ // Update recent projects
77
+ const recentProjects = get().recentProjects.filter(
78
+ (p) => p.path !== projectPath
79
+ );
80
+ recentProjects.unshift({
81
+ path: projectPath,
82
+ name: project.name,
83
+ lastOpened: new Date(),
84
+ });
85
+ if (recentProjects.length > 10) {
86
+ recentProjects.pop();
87
+ }
88
+
89
+ const newProjects = [...get().projects, project];
90
+
91
+ set({
92
+ projects: newProjects,
93
+ activeProjectPath: projectPath,
94
+ currentProject: deriveCurrentProject(newProjects, projectPath),
95
+ recentProjects,
96
+ isLoading: false,
97
+ });
98
+
99
+ get().refreshHealth();
100
+ } catch (error) {
101
+ set({
102
+ error: error instanceof Error ? error.message : 'Unknown error',
103
+ isLoading: false,
104
+ });
105
+ }
106
+ },
107
+
108
+ removeProject: (projectPath: string) => {
109
+ const newProjects = get().projects.filter(p => p.path !== projectPath);
110
+ const newActive = get().activeProjectPath === projectPath
111
+ ? (newProjects[0]?.path || null)
112
+ : get().activeProjectPath;
113
+
114
+ set({
115
+ projects: newProjects,
116
+ activeProjectPath: newActive,
117
+ currentProject: deriveCurrentProject(newProjects, newActive),
118
+ });
119
+ },
120
+
121
+ setActiveProject: (projectPath: string) => {
122
+ set({
123
+ activeProjectPath: projectPath,
124
+ currentProject: deriveCurrentProject(get().projects, projectPath),
125
+ });
126
+ },
127
+
128
+ // Backward-compatible alias
129
+ openProject: async (path: string) => {
130
+ await get().addProject(path);
131
+ },
132
+
133
+ restoreProjects: async () => {
134
+ // Read persisted paths from localStorage (saved by partialize)
135
+ try {
136
+ const raw = localStorage.getItem('devflow-project-store');
137
+ if (!raw) return;
138
+ const stored = JSON.parse(raw);
139
+ const paths: string[] = stored.state?.projectPaths || [];
140
+ const savedActive: string | null = stored.state?.activeProjectPath || null;
141
+
142
+ if (paths.length === 0) return;
143
+
144
+ // Restore each project sequentially to avoid race conditions
145
+ for (const p of paths) {
146
+ await get().addProject(p);
147
+ }
148
+
149
+ // Restore the active project (addProject sets it to the last added)
150
+ if (savedActive && get().projects.some(proj => proj.path === savedActive)) {
151
+ get().setActiveProject(savedActive);
152
+ }
153
+ } catch {
154
+ // Ignore restoration errors
155
+ }
156
+ },
157
+
158
+ closeProject: () => {
159
+ set({
160
+ projects: [],
161
+ activeProjectPath: null,
162
+ currentProject: null,
163
+ health: null,
164
+ error: null,
165
+ });
166
+ },
167
+
168
+ refreshHealth: async () => {
169
+ const project = get().currentProject;
170
+ if (!project) return;
171
+
172
+ try {
173
+ const response = await fetch(
174
+ `/api/health?projectPath=${encodeURIComponent(project.path)}`
175
+ );
176
+ const health = await response.json();
177
+ set({ health });
178
+ } catch (error) {
179
+ console.error('Failed to fetch health:', error);
180
+ }
181
+ },
182
+
183
+ setError: (error) => {
184
+ set({ error });
185
+ },
186
+ }),
187
+ {
188
+ name: 'devflow-project-store',
189
+ partialize: (state) => ({
190
+ recentProjects: state.recentProjects,
191
+ projectPaths: state.projects.map(p => p.path),
192
+ activeProjectPath: state.activeProjectPath,
193
+ }),
194
+ }
195
+ )
196
+ );
@@ -0,0 +1,126 @@
1
+ import { create } from 'zustand';
2
+ import { persist } from 'zustand/middleware';
3
+ import { toast } from 'sonner';
4
+
5
+ /**
6
+ * Settings Store - Gerencia todas as configurações do DevFlow IDE
7
+ * Persistido em localStorage para manter preferências entre sessões
8
+ */
9
+
10
+ export type TabSize = 2 | 4 | 8;
11
+ export type ChatModel = 'sonnet' | 'opus' | 'auto';
12
+ export type PermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions';
13
+
14
+ interface SettingsState {
15
+ // Editor Settings
16
+ editorFontSize: number;
17
+ editorTabSize: TabSize;
18
+ editorWordWrap: boolean;
19
+ editorMinimap: boolean;
20
+ editorLineNumbers: boolean;
21
+
22
+ // Terminal Settings
23
+ terminalFontSize: number;
24
+
25
+ // Chat Settings
26
+ chatDefaultModel: ChatModel;
27
+ chatDefaultAgent: string | null;
28
+ chatPermissionMode: PermissionMode;
29
+
30
+ // General Settings
31
+ autoSave: boolean;
32
+ autoSaveDelay: number;
33
+
34
+ // Settings Panel State
35
+ isSettingsOpen: boolean;
36
+ activeSettingsTab: 'general' | 'editor' | 'terminal' | 'chat' | 'keyboard';
37
+
38
+ // Actions
39
+ setSetting: <K extends keyof SettingsValues>(key: K, value: SettingsValues[K]) => void;
40
+ resetToDefaults: () => void;
41
+ openSettings: () => void;
42
+ closeSettings: () => void;
43
+ setActiveSettingsTab: (tab: SettingsState['activeSettingsTab']) => void;
44
+ }
45
+
46
+ // Type helper for settings values only (excludes actions and UI state)
47
+ type SettingsValues = Omit<SettingsState,
48
+ 'setSetting' | 'resetToDefaults' | 'openSettings' | 'closeSettings' |
49
+ 'setActiveSettingsTab' | 'isSettingsOpen' | 'activeSettingsTab'
50
+ >;
51
+
52
+ const DEFAULT_SETTINGS: SettingsValues = {
53
+ // Editor
54
+ editorFontSize: 14,
55
+ editorTabSize: 2,
56
+ editorWordWrap: true,
57
+ editorMinimap: false,
58
+ editorLineNumbers: true,
59
+
60
+ // Terminal
61
+ terminalFontSize: 13,
62
+
63
+ // Chat
64
+ chatDefaultModel: 'sonnet',
65
+ chatDefaultAgent: null,
66
+ chatPermissionMode: 'acceptEdits',
67
+
68
+ // General
69
+ autoSave: true,
70
+ autoSaveDelay: 1000,
71
+ };
72
+
73
+ export const useSettingsStore = create<SettingsState>()(
74
+ persist(
75
+ (set) => ({
76
+ ...DEFAULT_SETTINGS,
77
+
78
+ // UI State (não persistido)
79
+ isSettingsOpen: false,
80
+ activeSettingsTab: 'general',
81
+
82
+ setSetting: (key, value) => {
83
+ set({ [key]: value } as Partial<SettingsState>);
84
+ },
85
+
86
+ resetToDefaults: () => {
87
+ set(DEFAULT_SETTINGS);
88
+ toast.success('Settings reset', {
89
+ description: 'All settings restored to defaults',
90
+ });
91
+ },
92
+
93
+ openSettings: () => {
94
+ set({ isSettingsOpen: true });
95
+ },
96
+
97
+ closeSettings: () => {
98
+ set({ isSettingsOpen: false });
99
+ },
100
+
101
+ setActiveSettingsTab: (tab) => {
102
+ set({ activeSettingsTab: tab });
103
+ },
104
+ }),
105
+ {
106
+ name: 'devflow-settings',
107
+ // Não persistir estado da UI
108
+ partialize: (state) => ({
109
+ editorFontSize: state.editorFontSize,
110
+ editorTabSize: state.editorTabSize,
111
+ editorWordWrap: state.editorWordWrap,
112
+ editorMinimap: state.editorMinimap,
113
+ editorLineNumbers: state.editorLineNumbers,
114
+ terminalFontSize: state.terminalFontSize,
115
+ chatDefaultModel: state.chatDefaultModel,
116
+ chatDefaultAgent: state.chatDefaultAgent,
117
+ chatPermissionMode: state.chatPermissionMode,
118
+ autoSave: state.autoSave,
119
+ autoSaveDelay: state.autoSaveDelay,
120
+ }),
121
+ }
122
+ )
123
+ );
124
+
125
+ // Export default settings for reference
126
+ export { DEFAULT_SETTINGS };