@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,534 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { useGitStore } from '@/lib/stores/gitStore';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import {
|
|
7
|
+
GitBranch,
|
|
8
|
+
GitCommit,
|
|
9
|
+
Plus,
|
|
10
|
+
Minus,
|
|
11
|
+
Check,
|
|
12
|
+
X,
|
|
13
|
+
RefreshCw,
|
|
14
|
+
Upload,
|
|
15
|
+
Download,
|
|
16
|
+
ChevronDown,
|
|
17
|
+
ChevronRight,
|
|
18
|
+
FileText,
|
|
19
|
+
FilePlus,
|
|
20
|
+
FileX,
|
|
21
|
+
FileEdit,
|
|
22
|
+
RotateCcw,
|
|
23
|
+
AlertCircle,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
|
|
26
|
+
interface GitPanelProps {
|
|
27
|
+
projectPath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type FileStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'copied';
|
|
31
|
+
|
|
32
|
+
function getFileStatusIcon(status: FileStatus) {
|
|
33
|
+
switch (status) {
|
|
34
|
+
case 'added':
|
|
35
|
+
return <FilePlus className="w-4 h-4 text-green-400" />;
|
|
36
|
+
case 'deleted':
|
|
37
|
+
return <FileX className="w-4 h-4 text-red-400" />;
|
|
38
|
+
case 'modified':
|
|
39
|
+
return <FileEdit className="w-4 h-4 text-yellow-400" />;
|
|
40
|
+
case 'renamed':
|
|
41
|
+
case 'copied':
|
|
42
|
+
return <FileText className="w-4 h-4 text-blue-400" />;
|
|
43
|
+
default:
|
|
44
|
+
return <FileText className="w-4 h-4 text-gray-400" />;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getFileStatusBadge(status: FileStatus) {
|
|
49
|
+
const badges: Record<FileStatus, { text: string; color: string }> = {
|
|
50
|
+
added: { text: 'A', color: 'text-green-400 bg-green-400/10' },
|
|
51
|
+
modified: { text: 'M', color: 'text-yellow-400 bg-yellow-400/10' },
|
|
52
|
+
deleted: { text: 'D', color: 'text-red-400 bg-red-400/10' },
|
|
53
|
+
renamed: { text: 'R', color: 'text-blue-400 bg-blue-400/10' },
|
|
54
|
+
copied: { text: 'C', color: 'text-blue-400 bg-blue-400/10' },
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const badge = badges[status];
|
|
58
|
+
return (
|
|
59
|
+
<span className={cn('text-xs font-mono px-1.5 py-0.5 rounded', badge.color)}>
|
|
60
|
+
{badge.text}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function GitPanel({ projectPath }: GitPanelProps) {
|
|
66
|
+
const {
|
|
67
|
+
status,
|
|
68
|
+
branches,
|
|
69
|
+
isLoading,
|
|
70
|
+
error,
|
|
71
|
+
fetchStatus,
|
|
72
|
+
fetchBranches,
|
|
73
|
+
stageFiles,
|
|
74
|
+
unstageFiles,
|
|
75
|
+
stageAll,
|
|
76
|
+
unstageAll,
|
|
77
|
+
commit,
|
|
78
|
+
push,
|
|
79
|
+
pull,
|
|
80
|
+
checkout,
|
|
81
|
+
createBranch,
|
|
82
|
+
discardChanges,
|
|
83
|
+
initRepo,
|
|
84
|
+
setError,
|
|
85
|
+
} = useGitStore();
|
|
86
|
+
|
|
87
|
+
const [commitMessage, setCommitMessage] = useState('');
|
|
88
|
+
const [showBranches, setShowBranches] = useState(false);
|
|
89
|
+
const [newBranchName, setNewBranchName] = useState('');
|
|
90
|
+
const [showNewBranch, setShowNewBranch] = useState(false);
|
|
91
|
+
const [stagedExpanded, setStagedExpanded] = useState(true);
|
|
92
|
+
const [unstagedExpanded, setUnstagedExpanded] = useState(true);
|
|
93
|
+
const [untrackedExpanded, setUntrackedExpanded] = useState(true);
|
|
94
|
+
|
|
95
|
+
// Fetch status on mount and set up polling
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
fetchStatus(projectPath);
|
|
98
|
+
fetchBranches(projectPath);
|
|
99
|
+
|
|
100
|
+
// Poll for status every 5 seconds
|
|
101
|
+
const interval = setInterval(() => {
|
|
102
|
+
fetchStatus(projectPath);
|
|
103
|
+
}, 5000);
|
|
104
|
+
|
|
105
|
+
return () => clearInterval(interval);
|
|
106
|
+
}, [projectPath, fetchStatus, fetchBranches]);
|
|
107
|
+
|
|
108
|
+
const handleRefresh = useCallback(() => {
|
|
109
|
+
fetchStatus(projectPath);
|
|
110
|
+
fetchBranches(projectPath);
|
|
111
|
+
}, [projectPath, fetchStatus, fetchBranches]);
|
|
112
|
+
|
|
113
|
+
const handleCommit = useCallback(async () => {
|
|
114
|
+
if (!commitMessage.trim()) return;
|
|
115
|
+
|
|
116
|
+
const result = await commit(projectPath, commitMessage);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
setCommitMessage('');
|
|
119
|
+
}
|
|
120
|
+
}, [projectPath, commitMessage, commit]);
|
|
121
|
+
|
|
122
|
+
const handlePush = useCallback(() => {
|
|
123
|
+
push(projectPath);
|
|
124
|
+
}, [projectPath, push]);
|
|
125
|
+
|
|
126
|
+
const handlePull = useCallback(() => {
|
|
127
|
+
pull(projectPath);
|
|
128
|
+
}, [projectPath, pull]);
|
|
129
|
+
|
|
130
|
+
const handleCheckout = useCallback(async (branch: string) => {
|
|
131
|
+
await checkout(projectPath, branch);
|
|
132
|
+
setShowBranches(false);
|
|
133
|
+
}, [projectPath, checkout]);
|
|
134
|
+
|
|
135
|
+
const handleCreateBranch = useCallback(async () => {
|
|
136
|
+
if (!newBranchName.trim()) return;
|
|
137
|
+
|
|
138
|
+
const result = await createBranch(projectPath, newBranchName);
|
|
139
|
+
if (result.success) {
|
|
140
|
+
setNewBranchName('');
|
|
141
|
+
setShowNewBranch(false);
|
|
142
|
+
}
|
|
143
|
+
}, [projectPath, newBranchName, createBranch]);
|
|
144
|
+
|
|
145
|
+
const handleInit = useCallback(() => {
|
|
146
|
+
initRepo(projectPath);
|
|
147
|
+
}, [projectPath, initRepo]);
|
|
148
|
+
|
|
149
|
+
// Not a git repo
|
|
150
|
+
if (status && !status.isRepo) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="h-full flex flex-col bg-[#0a0a0f]">
|
|
153
|
+
<div className="p-4 border-b border-white/10">
|
|
154
|
+
<h2 className="text-sm font-semibold text-white flex items-center gap-2">
|
|
155
|
+
<GitBranch className="w-4 h-4" />
|
|
156
|
+
Source Control
|
|
157
|
+
</h2>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|
|
161
|
+
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mb-4">
|
|
162
|
+
<GitBranch className="w-8 h-8 text-gray-500" />
|
|
163
|
+
</div>
|
|
164
|
+
<p className="text-sm text-gray-400 mb-4">
|
|
165
|
+
This folder is not a Git repository
|
|
166
|
+
</p>
|
|
167
|
+
<button
|
|
168
|
+
onClick={handleInit}
|
|
169
|
+
disabled={isLoading}
|
|
170
|
+
className="px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
|
171
|
+
>
|
|
172
|
+
Initialize Repository
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const totalChanges = (status?.staged.length || 0) + (status?.unstaged.length || 0) + (status?.untracked.length || 0);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div className="h-full flex flex-col bg-[#0a0a0f]">
|
|
183
|
+
{/* Header */}
|
|
184
|
+
<div className="p-3 border-b border-white/10">
|
|
185
|
+
<div className="flex items-center justify-between mb-2">
|
|
186
|
+
<h2 className="text-sm font-semibold text-white flex items-center gap-2">
|
|
187
|
+
<GitBranch className="w-4 h-4" />
|
|
188
|
+
Source Control
|
|
189
|
+
{totalChanges > 0 && (
|
|
190
|
+
<span className="px-1.5 py-0.5 text-xs bg-purple-500/20 text-purple-400 rounded">
|
|
191
|
+
{totalChanges}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
</h2>
|
|
195
|
+
<button
|
|
196
|
+
onClick={handleRefresh}
|
|
197
|
+
disabled={isLoading}
|
|
198
|
+
className="p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded transition-colors disabled:opacity-50"
|
|
199
|
+
>
|
|
200
|
+
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
|
|
201
|
+
</button>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Branch Selector */}
|
|
205
|
+
<div className="relative">
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => setShowBranches(!showBranches)}
|
|
208
|
+
className="w-full flex items-center justify-between gap-2 px-3 py-1.5 bg-white/5 border border-white/10 rounded-lg text-sm hover:bg-white/10 transition-colors"
|
|
209
|
+
>
|
|
210
|
+
<div className="flex items-center gap-2">
|
|
211
|
+
<GitBranch className="w-4 h-4 text-purple-400" />
|
|
212
|
+
<span className="text-white">{status?.branch || 'main'}</span>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex items-center gap-2">
|
|
215
|
+
{status?.ahead ? (
|
|
216
|
+
<span className="text-xs text-green-400">{status.ahead}</span>
|
|
217
|
+
) : null}
|
|
218
|
+
{status?.behind ? (
|
|
219
|
+
<span className="text-xs text-yellow-400">{status.behind}</span>
|
|
220
|
+
) : null}
|
|
221
|
+
<ChevronDown className={cn('w-4 h-4 text-gray-500 transition-transform', showBranches && 'rotate-180')} />
|
|
222
|
+
</div>
|
|
223
|
+
</button>
|
|
224
|
+
|
|
225
|
+
{/* Branch Dropdown */}
|
|
226
|
+
{showBranches && (
|
|
227
|
+
<div className="absolute top-full left-0 right-0 mt-1 bg-[#12121a] border border-white/10 rounded-lg shadow-xl z-50 overflow-hidden">
|
|
228
|
+
<div className="max-h-48 overflow-y-auto">
|
|
229
|
+
{branches.map((branch) => (
|
|
230
|
+
<button
|
|
231
|
+
key={branch.name}
|
|
232
|
+
onClick={() => handleCheckout(branch.name)}
|
|
233
|
+
className={cn(
|
|
234
|
+
'w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-white/5 transition-colors',
|
|
235
|
+
branch.current && 'bg-purple-500/10'
|
|
236
|
+
)}
|
|
237
|
+
>
|
|
238
|
+
{branch.current && <Check className="w-4 h-4 text-purple-400" />}
|
|
239
|
+
{!branch.current && <div className="w-4" />}
|
|
240
|
+
<span className={branch.current ? 'text-purple-400' : 'text-gray-300'}>
|
|
241
|
+
{branch.name}
|
|
242
|
+
</span>
|
|
243
|
+
</button>
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<div className="border-t border-white/10 p-2">
|
|
248
|
+
{showNewBranch ? (
|
|
249
|
+
<div className="flex gap-2">
|
|
250
|
+
<input
|
|
251
|
+
type="text"
|
|
252
|
+
value={newBranchName}
|
|
253
|
+
onChange={(e) => setNewBranchName(e.target.value)}
|
|
254
|
+
onKeyDown={(e) => e.key === 'Enter' && handleCreateBranch()}
|
|
255
|
+
placeholder="New branch name"
|
|
256
|
+
className="flex-1 px-2 py-1 bg-white/5 border border-white/10 rounded text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
|
|
257
|
+
autoFocus
|
|
258
|
+
/>
|
|
259
|
+
<button
|
|
260
|
+
onClick={handleCreateBranch}
|
|
261
|
+
className="p-1 text-green-400 hover:bg-white/10 rounded"
|
|
262
|
+
>
|
|
263
|
+
<Check className="w-4 h-4" />
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => { setShowNewBranch(false); setNewBranchName(''); }}
|
|
267
|
+
className="p-1 text-red-400 hover:bg-white/10 rounded"
|
|
268
|
+
>
|
|
269
|
+
<X className="w-4 h-4" />
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
) : (
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => setShowNewBranch(true)}
|
|
275
|
+
className="w-full flex items-center gap-2 px-2 py-1 text-sm text-gray-400 hover:text-white hover:bg-white/5 rounded transition-colors"
|
|
276
|
+
>
|
|
277
|
+
<Plus className="w-4 h-4" />
|
|
278
|
+
Create new branch
|
|
279
|
+
</button>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{/* Error Display */}
|
|
288
|
+
{error && (
|
|
289
|
+
<div className="mx-3 mt-3 p-2 bg-red-500/10 border border-red-500/30 rounded-lg flex items-center gap-2">
|
|
290
|
+
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
|
291
|
+
<span className="text-xs text-red-400 flex-1">{error}</span>
|
|
292
|
+
<button onClick={() => setError(null)} className="text-red-400 hover:text-red-300">
|
|
293
|
+
<X className="w-4 h-4" />
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
|
|
298
|
+
{/* Changes */}
|
|
299
|
+
<div className="flex-1 overflow-y-auto">
|
|
300
|
+
{/* Staged Changes */}
|
|
301
|
+
{status?.staged && status.staged.length > 0 && (
|
|
302
|
+
<div className="border-b border-white/5">
|
|
303
|
+
<div
|
|
304
|
+
role="button"
|
|
305
|
+
tabIndex={0}
|
|
306
|
+
onClick={() => setStagedExpanded(!stagedExpanded)}
|
|
307
|
+
onKeyDown={(e) => e.key === 'Enter' && setStagedExpanded(!stagedExpanded)}
|
|
308
|
+
className="w-full flex items-center justify-between px-3 py-2 hover:bg-white/5 transition-colors cursor-pointer"
|
|
309
|
+
>
|
|
310
|
+
<div className="flex items-center gap-2">
|
|
311
|
+
{stagedExpanded ? (
|
|
312
|
+
<ChevronDown className="w-4 h-4 text-gray-500" />
|
|
313
|
+
) : (
|
|
314
|
+
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
315
|
+
)}
|
|
316
|
+
<span className="text-xs font-medium text-green-400">Staged Changes</span>
|
|
317
|
+
<span className="text-xs text-gray-500">{status.staged.length}</span>
|
|
318
|
+
</div>
|
|
319
|
+
<button
|
|
320
|
+
onClick={(e) => { e.stopPropagation(); unstageAll(projectPath); }}
|
|
321
|
+
className="p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded"
|
|
322
|
+
title="Unstage All"
|
|
323
|
+
>
|
|
324
|
+
<Minus className="w-3 h-3" />
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
{stagedExpanded && (
|
|
329
|
+
<div className="pb-2">
|
|
330
|
+
{status.staged.map((file) => (
|
|
331
|
+
<div
|
|
332
|
+
key={file.path}
|
|
333
|
+
className="group flex items-center gap-2 px-3 py-1 hover:bg-white/5 transition-colors"
|
|
334
|
+
>
|
|
335
|
+
{getFileStatusIcon(file.status)}
|
|
336
|
+
<span className="flex-1 text-xs text-gray-300 truncate" title={file.path}>
|
|
337
|
+
{file.path}
|
|
338
|
+
</span>
|
|
339
|
+
{getFileStatusBadge(file.status)}
|
|
340
|
+
<button
|
|
341
|
+
onClick={() => unstageFiles(projectPath, [file.path])}
|
|
342
|
+
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded transition-opacity"
|
|
343
|
+
title="Unstage"
|
|
344
|
+
>
|
|
345
|
+
<Minus className="w-3 h-3" />
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Unstaged Changes */}
|
|
355
|
+
{status?.unstaged && status.unstaged.length > 0 && (
|
|
356
|
+
<div className="border-b border-white/5">
|
|
357
|
+
<div
|
|
358
|
+
role="button"
|
|
359
|
+
tabIndex={0}
|
|
360
|
+
onClick={() => setUnstagedExpanded(!unstagedExpanded)}
|
|
361
|
+
onKeyDown={(e) => e.key === 'Enter' && setUnstagedExpanded(!unstagedExpanded)}
|
|
362
|
+
className="w-full flex items-center justify-between px-3 py-2 hover:bg-white/5 transition-colors cursor-pointer"
|
|
363
|
+
>
|
|
364
|
+
<div className="flex items-center gap-2">
|
|
365
|
+
{unstagedExpanded ? (
|
|
366
|
+
<ChevronDown className="w-4 h-4 text-gray-500" />
|
|
367
|
+
) : (
|
|
368
|
+
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
369
|
+
)}
|
|
370
|
+
<span className="text-xs font-medium text-yellow-400">Changes</span>
|
|
371
|
+
<span className="text-xs text-gray-500">{status.unstaged.length}</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="flex gap-1">
|
|
374
|
+
<button
|
|
375
|
+
onClick={(e) => { e.stopPropagation(); discardChanges(projectPath, status.unstaged.map(f => f.path)); }}
|
|
376
|
+
className="p-1 text-gray-400 hover:text-red-400 hover:bg-white/10 rounded"
|
|
377
|
+
title="Discard All"
|
|
378
|
+
>
|
|
379
|
+
<RotateCcw className="w-3 h-3" />
|
|
380
|
+
</button>
|
|
381
|
+
<button
|
|
382
|
+
onClick={(e) => { e.stopPropagation(); stageFiles(projectPath, status.unstaged.map(f => f.path)); }}
|
|
383
|
+
className="p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded"
|
|
384
|
+
title="Stage All"
|
|
385
|
+
>
|
|
386
|
+
<Plus className="w-3 h-3" />
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{unstagedExpanded && (
|
|
392
|
+
<div className="pb-2">
|
|
393
|
+
{status.unstaged.map((file) => (
|
|
394
|
+
<div
|
|
395
|
+
key={file.path}
|
|
396
|
+
className="group flex items-center gap-2 px-3 py-1 hover:bg-white/5 transition-colors"
|
|
397
|
+
>
|
|
398
|
+
{getFileStatusIcon(file.status)}
|
|
399
|
+
<span className="flex-1 text-xs text-gray-300 truncate" title={file.path}>
|
|
400
|
+
{file.path}
|
|
401
|
+
</span>
|
|
402
|
+
{getFileStatusBadge(file.status)}
|
|
403
|
+
<button
|
|
404
|
+
onClick={() => discardChanges(projectPath, [file.path])}
|
|
405
|
+
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 hover:bg-white/10 rounded transition-opacity"
|
|
406
|
+
title="Discard"
|
|
407
|
+
>
|
|
408
|
+
<RotateCcw className="w-3 h-3" />
|
|
409
|
+
</button>
|
|
410
|
+
<button
|
|
411
|
+
onClick={() => stageFiles(projectPath, [file.path])}
|
|
412
|
+
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded transition-opacity"
|
|
413
|
+
title="Stage"
|
|
414
|
+
>
|
|
415
|
+
<Plus className="w-3 h-3" />
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
))}
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* Untracked Files */}
|
|
425
|
+
{status?.untracked && status.untracked.length > 0 && (
|
|
426
|
+
<div className="border-b border-white/5">
|
|
427
|
+
<div
|
|
428
|
+
role="button"
|
|
429
|
+
tabIndex={0}
|
|
430
|
+
onClick={() => setUntrackedExpanded(!untrackedExpanded)}
|
|
431
|
+
onKeyDown={(e) => e.key === 'Enter' && setUntrackedExpanded(!untrackedExpanded)}
|
|
432
|
+
className="w-full flex items-center justify-between px-3 py-2 hover:bg-white/5 transition-colors cursor-pointer"
|
|
433
|
+
>
|
|
434
|
+
<div className="flex items-center gap-2">
|
|
435
|
+
{untrackedExpanded ? (
|
|
436
|
+
<ChevronDown className="w-4 h-4 text-gray-500" />
|
|
437
|
+
) : (
|
|
438
|
+
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
439
|
+
)}
|
|
440
|
+
<span className="text-xs font-medium text-gray-400">Untracked</span>
|
|
441
|
+
<span className="text-xs text-gray-500">{status.untracked.length}</span>
|
|
442
|
+
</div>
|
|
443
|
+
<button
|
|
444
|
+
onClick={(e) => { e.stopPropagation(); stageFiles(projectPath, status.untracked); }}
|
|
445
|
+
className="p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded"
|
|
446
|
+
title="Stage All"
|
|
447
|
+
>
|
|
448
|
+
<Plus className="w-3 h-3" />
|
|
449
|
+
</button>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
{untrackedExpanded && (
|
|
453
|
+
<div className="pb-2">
|
|
454
|
+
{status.untracked.map((file) => (
|
|
455
|
+
<div
|
|
456
|
+
key={file}
|
|
457
|
+
className="group flex items-center gap-2 px-3 py-1 hover:bg-white/5 transition-colors"
|
|
458
|
+
>
|
|
459
|
+
<FilePlus className="w-4 h-4 text-green-400" />
|
|
460
|
+
<span className="flex-1 text-xs text-gray-300 truncate" title={file}>
|
|
461
|
+
{file}
|
|
462
|
+
</span>
|
|
463
|
+
<span className="text-xs font-mono px-1.5 py-0.5 rounded text-green-400 bg-green-400/10">U</span>
|
|
464
|
+
<button
|
|
465
|
+
onClick={() => stageFiles(projectPath, [file])}
|
|
466
|
+
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-white hover:bg-white/10 rounded transition-opacity"
|
|
467
|
+
title="Stage"
|
|
468
|
+
>
|
|
469
|
+
<Plus className="w-3 h-3" />
|
|
470
|
+
</button>
|
|
471
|
+
</div>
|
|
472
|
+
))}
|
|
473
|
+
</div>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{/* No Changes */}
|
|
479
|
+
{totalChanges === 0 && status?.isRepo && (
|
|
480
|
+
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
481
|
+
<Check className="w-8 h-8 text-green-400 mb-2" />
|
|
482
|
+
<p className="text-sm text-gray-400">No changes</p>
|
|
483
|
+
<p className="text-xs text-gray-600">Working tree clean</p>
|
|
484
|
+
</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{/* Commit Section */}
|
|
489
|
+
{status?.isRepo && (
|
|
490
|
+
<div className="border-t border-white/10 p-3">
|
|
491
|
+
{/* Commit Message */}
|
|
492
|
+
<textarea
|
|
493
|
+
value={commitMessage}
|
|
494
|
+
onChange={(e) => setCommitMessage(e.target.value)}
|
|
495
|
+
placeholder="Commit message"
|
|
496
|
+
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 resize-none"
|
|
497
|
+
rows={3}
|
|
498
|
+
/>
|
|
499
|
+
|
|
500
|
+
{/* Action Buttons */}
|
|
501
|
+
<div className="flex gap-2 mt-2">
|
|
502
|
+
<button
|
|
503
|
+
onClick={handleCommit}
|
|
504
|
+
disabled={!commitMessage.trim() || !status?.staged?.length || isLoading}
|
|
505
|
+
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-purple-500 hover:bg-purple-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white text-sm rounded-lg transition-colors"
|
|
506
|
+
>
|
|
507
|
+
<GitCommit className="w-4 h-4" />
|
|
508
|
+
Commit
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<div className="flex gap-2 mt-2">
|
|
513
|
+
<button
|
|
514
|
+
onClick={handlePull}
|
|
515
|
+
disabled={isLoading}
|
|
516
|
+
className="flex-1 flex items-center justify-center gap-2 px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-300 text-xs rounded-lg transition-colors disabled:opacity-50"
|
|
517
|
+
>
|
|
518
|
+
<Download className="w-3 h-3" />
|
|
519
|
+
Pull
|
|
520
|
+
</button>
|
|
521
|
+
<button
|
|
522
|
+
onClick={handlePush}
|
|
523
|
+
disabled={isLoading}
|
|
524
|
+
className="flex-1 flex items-center justify-center gap-2 px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-300 text-xs rounded-lg transition-colors disabled:opacity-50"
|
|
525
|
+
>
|
|
526
|
+
<Upload className="w-3 h-3" />
|
|
527
|
+
Push
|
|
528
|
+
</button>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
interface ShellProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Shell({ children }: ShellProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="flex-1 flex overflow-hidden">
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useProjectStore } from '@/lib/stores/projectStore';
|
|
4
|
+
import { useFileStore } from '@/lib/stores/fileStore';
|
|
5
|
+
import { useUIStore } from '@/lib/stores/uiStore';
|
|
6
|
+
import {
|
|
7
|
+
FolderOpen,
|
|
8
|
+
GitBranch,
|
|
9
|
+
CheckCircle,
|
|
10
|
+
XCircle,
|
|
11
|
+
Zap,
|
|
12
|
+
Sparkles
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
|
|
15
|
+
export function StatusBar() {
|
|
16
|
+
const { currentProject, health } = useProjectStore();
|
|
17
|
+
const { activeFile, openFiles } = useFileStore();
|
|
18
|
+
const { selectedModel, autopilot } = useUIStore();
|
|
19
|
+
|
|
20
|
+
const activeOpenFile = openFiles.find(f => f.path === activeFile);
|
|
21
|
+
|
|
22
|
+
const modelNames: Record<string, string> = {
|
|
23
|
+
'claude-sonnet-4': 'Sonnet 4',
|
|
24
|
+
'claude-opus-4': 'Opus 4',
|
|
25
|
+
'auto': 'Auto',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="h-7 bg-[#12121a] border-t border-white/10 text-gray-400 flex items-center justify-between px-3 text-xs">
|
|
30
|
+
{/* Left side */}
|
|
31
|
+
<div className="flex items-center gap-4">
|
|
32
|
+
{/* Project name */}
|
|
33
|
+
{currentProject && (
|
|
34
|
+
<div className="flex items-center gap-1.5 text-gray-300">
|
|
35
|
+
<FolderOpen className="w-3.5 h-3.5 text-purple-400" />
|
|
36
|
+
<span>{currentProject.name}</span>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{/* Git branch */}
|
|
41
|
+
<div className="flex items-center gap-1.5">
|
|
42
|
+
<GitBranch className="w-3.5 h-3.5" />
|
|
43
|
+
<span>main</span>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Claude CLI status */}
|
|
47
|
+
<div className="flex items-center gap-1.5">
|
|
48
|
+
{health?.claudeCli.installed ? (
|
|
49
|
+
<>
|
|
50
|
+
<CheckCircle className="w-3.5 h-3.5 text-green-400" />
|
|
51
|
+
<span>Claude CLI</span>
|
|
52
|
+
</>
|
|
53
|
+
) : (
|
|
54
|
+
<>
|
|
55
|
+
<XCircle className="w-3.5 h-3.5 text-red-400" />
|
|
56
|
+
<span>Claude CLI not found</span>
|
|
57
|
+
</>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Right side */}
|
|
64
|
+
<div className="flex items-center gap-4">
|
|
65
|
+
{/* Autopilot indicator */}
|
|
66
|
+
{autopilot.enabled && (
|
|
67
|
+
<div className="flex items-center gap-1.5 text-purple-400">
|
|
68
|
+
<Sparkles className="w-3.5 h-3.5 animate-pulse" />
|
|
69
|
+
<span>Autopilot</span>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* Model */}
|
|
74
|
+
<div className="flex items-center gap-1.5">
|
|
75
|
+
<Zap className="w-3.5 h-3.5 text-purple-400" />
|
|
76
|
+
<span>{modelNames[selectedModel] || selectedModel}</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{/* File info */}
|
|
80
|
+
{activeOpenFile && (
|
|
81
|
+
<>
|
|
82
|
+
<span>Ln 1, Col 1</span>
|
|
83
|
+
<span className="text-gray-500">|</span>
|
|
84
|
+
<span>{activeOpenFile.language}</span>
|
|
85
|
+
<span className="text-gray-500">|</span>
|
|
86
|
+
<span>UTF-8</span>
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Stats */}
|
|
91
|
+
{currentProject && (
|
|
92
|
+
<div className="flex items-center gap-2 text-gray-500">
|
|
93
|
+
<span>{currentProject.stats.stories} stories</span>
|
|
94
|
+
<span>{currentProject.stats.adrs} ADRs</span>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|