@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.
- package/.claude/commands/agents/architect.md +1162 -0
- package/.claude/commands/agents/architect.meta.yaml +124 -0
- package/.claude/commands/agents/builder.md +1432 -0
- package/.claude/commands/agents/builder.meta.yaml +117 -0
- package/.claude/commands/agents/chronicler.md +633 -0
- package/.claude/commands/agents/chronicler.meta.yaml +217 -0
- package/.claude/commands/agents/guardian.md +456 -0
- package/.claude/commands/agents/guardian.meta.yaml +127 -0
- package/.claude/commands/agents/strategist.md +483 -0
- package/.claude/commands/agents/strategist.meta.yaml +158 -0
- package/.claude/commands/agents/system-designer.md +1137 -0
- package/.claude/commands/agents/system-designer.meta.yaml +156 -0
- package/.claude/commands/devflow-help.md +93 -0
- package/.claude/commands/devflow-status.md +60 -0
- package/.claude/commands/quick/create-adr.md +82 -0
- package/.claude/commands/quick/new-feature.md +57 -0
- package/.claude/commands/quick/security-check.md +54 -0
- package/.claude/commands/quick/system-design.md +58 -0
- package/.claude_project +52 -0
- package/.devflow/agents/architect.meta.yaml +122 -0
- package/.devflow/agents/builder.meta.yaml +116 -0
- package/.devflow/agents/chronicler.meta.yaml +222 -0
- package/.devflow/agents/guardian.meta.yaml +127 -0
- package/.devflow/agents/strategist.meta.yaml +158 -0
- package/.devflow/agents/system-designer.meta.yaml +265 -0
- package/.devflow/project.yaml +242 -0
- package/.gitignore-template +84 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/devflow.js +54 -0
- package/lib/autopilot.js +235 -0
- package/lib/autopilotConstants.js +213 -0
- package/lib/constants.js +95 -0
- package/lib/init.js +200 -0
- package/lib/update.js +181 -0
- package/lib/utils.js +157 -0
- package/lib/web.js +119 -0
- package/package.json +57 -0
- package/web/CHANGELOG.md +192 -0
- package/web/README.md +156 -0
- package/web/app/api/autopilot/execute/route.ts +102 -0
- package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
- package/web/app/api/files/route.ts +280 -0
- package/web/app/api/files/tree/route.ts +160 -0
- package/web/app/api/git/route.ts +201 -0
- package/web/app/api/health/route.ts +94 -0
- package/web/app/api/project/open/route.ts +134 -0
- package/web/app/api/search/route.ts +247 -0
- package/web/app/api/specs/route.ts +405 -0
- package/web/app/api/terminal/route.ts +222 -0
- package/web/app/globals.css +160 -0
- package/web/app/ide/layout.tsx +43 -0
- package/web/app/ide/page.tsx +216 -0
- package/web/app/layout.tsx +34 -0
- package/web/app/page.tsx +303 -0
- package/web/components/agents/AgentIcons.tsx +281 -0
- package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
- package/web/components/autopilot/AutopilotPanel.tsx +299 -0
- package/web/components/dashboard/DashboardPanel.tsx +393 -0
- package/web/components/editor/Breadcrumbs.tsx +134 -0
- package/web/components/editor/EditorPanel.tsx +120 -0
- package/web/components/editor/EditorTabs.tsx +229 -0
- package/web/components/editor/MarkdownPreview.tsx +154 -0
- package/web/components/editor/MermaidDiagram.tsx +113 -0
- package/web/components/editor/MonacoEditor.tsx +177 -0
- package/web/components/editor/TabContextMenu.tsx +207 -0
- package/web/components/git/GitPanel.tsx +534 -0
- package/web/components/layout/Shell.tsx +15 -0
- package/web/components/layout/StatusBar.tsx +100 -0
- package/web/components/modals/CommandPalette.tsx +393 -0
- package/web/components/modals/GlobalSearch.tsx +348 -0
- package/web/components/modals/QuickOpen.tsx +241 -0
- package/web/components/modals/RecentFiles.tsx +208 -0
- package/web/components/projects/ProjectSelector.tsx +147 -0
- package/web/components/settings/SettingItem.tsx +150 -0
- package/web/components/settings/SettingsPanel.tsx +323 -0
- package/web/components/specs/SpecsPanel.tsx +1091 -0
- package/web/components/terminal/TerminalPanel.tsx +683 -0
- package/web/components/ui/ContextMenu.tsx +182 -0
- package/web/components/ui/LoadingSpinner.tsx +66 -0
- package/web/components/ui/ResizeHandle.tsx +110 -0
- package/web/components/ui/Skeleton.tsx +108 -0
- package/web/components/ui/SkipLinks.tsx +37 -0
- package/web/components/ui/Toaster.tsx +57 -0
- package/web/hooks/useFocusTrap.ts +141 -0
- package/web/hooks/useKeyboardShortcuts.ts +169 -0
- package/web/hooks/useListNavigation.ts +237 -0
- package/web/lib/autopilotConstants.ts +213 -0
- package/web/lib/constants/agents.ts +67 -0
- package/web/lib/git.ts +339 -0
- package/web/lib/ptyManager.ts +191 -0
- package/web/lib/specsParser.ts +299 -0
- package/web/lib/stores/autopilotStore.ts +288 -0
- package/web/lib/stores/fileStore.ts +550 -0
- package/web/lib/stores/gitStore.ts +386 -0
- package/web/lib/stores/projectStore.ts +196 -0
- package/web/lib/stores/settingsStore.ts +126 -0
- package/web/lib/stores/specsStore.ts +297 -0
- package/web/lib/stores/uiStore.ts +175 -0
- package/web/lib/types/index.ts +177 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.js +50 -0
- package/web/package.json +54 -0
- package/web/postcss.config.js +6 -0
- package/web/tailwind.config.ts +68 -0
- 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 };
|