@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,393 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useMemo, useCallback } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
FileText,
|
|
6
|
+
CheckSquare,
|
|
7
|
+
Scale,
|
|
8
|
+
ListTodo,
|
|
9
|
+
Activity,
|
|
10
|
+
CheckCircle2,
|
|
11
|
+
Clock,
|
|
12
|
+
AlertCircle,
|
|
13
|
+
Terminal,
|
|
14
|
+
GitBranch,
|
|
15
|
+
Wifi,
|
|
16
|
+
RefreshCw
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
import { cn } from '@/lib/utils';
|
|
19
|
+
import { useSpecsStore } from '@/lib/stores/specsStore';
|
|
20
|
+
import { Skeleton, SkeletonCard } from '@/components/ui/Skeleton';
|
|
21
|
+
import type { Task } from '@/lib/types';
|
|
22
|
+
|
|
23
|
+
interface DashboardPanelProps {
|
|
24
|
+
projectPath: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HealthStatus {
|
|
28
|
+
claudeCli: {
|
|
29
|
+
installed: boolean;
|
|
30
|
+
authenticated: boolean;
|
|
31
|
+
version?: string;
|
|
32
|
+
};
|
|
33
|
+
project: {
|
|
34
|
+
valid: boolean;
|
|
35
|
+
hasDevflow: boolean;
|
|
36
|
+
hasClaudeProject: boolean;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface StatCardProps {
|
|
41
|
+
title: string;
|
|
42
|
+
value: number;
|
|
43
|
+
subtitle: string;
|
|
44
|
+
icon: React.ReactNode;
|
|
45
|
+
color: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function StatCard({ title, value, subtitle, icon, color }: StatCardProps) {
|
|
49
|
+
return (
|
|
50
|
+
<div className="bg-[#1a1a24] rounded-lg border border-white/10 p-4">
|
|
51
|
+
<div className="flex items-center justify-between mb-3">
|
|
52
|
+
<div className={cn('p-2 rounded-lg', color)}>
|
|
53
|
+
{icon}
|
|
54
|
+
</div>
|
|
55
|
+
<span className="text-3xl font-bold text-white">{value}</span>
|
|
56
|
+
</div>
|
|
57
|
+
<h3 className="text-sm font-medium text-white">{title}</h3>
|
|
58
|
+
<p className="text-xs text-gray-500">{subtitle}</p>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface HealthItemProps {
|
|
64
|
+
label: string;
|
|
65
|
+
status: 'ok' | 'warning' | 'error';
|
|
66
|
+
message?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function HealthItem({ label, status, message }: HealthItemProps) {
|
|
70
|
+
const statusConfig = {
|
|
71
|
+
ok: { icon: CheckCircle2, color: 'text-green-400', bg: 'bg-green-500/10' },
|
|
72
|
+
warning: { icon: AlertCircle, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
|
73
|
+
error: { icon: AlertCircle, color: 'text-red-400', bg: 'bg-red-500/10' },
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const config = statusConfig[status];
|
|
77
|
+
const Icon = config.icon;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex items-center gap-3 py-2">
|
|
81
|
+
<div className={cn('p-1.5 rounded', config.bg)}>
|
|
82
|
+
<Icon className={cn('w-4 h-4', config.color)} />
|
|
83
|
+
</div>
|
|
84
|
+
<div className="flex-1">
|
|
85
|
+
<p className="text-sm text-white">{label}</p>
|
|
86
|
+
{message && <p className="text-xs text-gray-500">{message}</p>}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function DashboardPanel({ projectPath }: DashboardPanelProps) {
|
|
93
|
+
const { specs, requirements, decisions, tasks, isLoading, loadSpecs } = useSpecsStore();
|
|
94
|
+
const [health, setHealth] = useState<HealthStatus | null>(null);
|
|
95
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
96
|
+
|
|
97
|
+
// Fetch health status
|
|
98
|
+
const fetchHealth = useCallback(async (path: string) => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`/api/health?projectPath=${encodeURIComponent(path)}`);
|
|
101
|
+
if (response.ok) {
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
setHealth(data);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Failed to fetch health:', error);
|
|
107
|
+
}
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
110
|
+
// Load data on mount
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (projectPath) {
|
|
113
|
+
loadSpecs([projectPath]);
|
|
114
|
+
fetchHealth(projectPath);
|
|
115
|
+
}
|
|
116
|
+
}, [projectPath, loadSpecs, fetchHealth]);
|
|
117
|
+
|
|
118
|
+
const handleRefresh = async () => {
|
|
119
|
+
setIsRefreshing(true);
|
|
120
|
+
await Promise.all([loadSpecs([projectPath]), fetchHealth(projectPath)]);
|
|
121
|
+
setIsRefreshing(false);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Calculate stats
|
|
125
|
+
const stats = useMemo(() => {
|
|
126
|
+
const totalTasks = tasks.length;
|
|
127
|
+
const completedTasks = tasks.filter(t => t.status === 'completed').length;
|
|
128
|
+
const pendingTasks = tasks.filter(t => t.status === 'pending').length;
|
|
129
|
+
const inProgressTasks = tasks.filter(t => t.status === 'in_progress').length;
|
|
130
|
+
const blockedTasks = tasks.filter(t => t.status === 'blocked').length;
|
|
131
|
+
|
|
132
|
+
const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
stories: requirements.length,
|
|
136
|
+
tasks: totalTasks,
|
|
137
|
+
completedTasks,
|
|
138
|
+
pendingTasks,
|
|
139
|
+
inProgressTasks,
|
|
140
|
+
blockedTasks,
|
|
141
|
+
decisions: decisions.length,
|
|
142
|
+
specs: specs.length,
|
|
143
|
+
progress,
|
|
144
|
+
};
|
|
145
|
+
}, [specs, requirements, decisions, tasks]);
|
|
146
|
+
|
|
147
|
+
// Recent activity (mock based on tasks status)
|
|
148
|
+
const recentActivity = useMemo(() => {
|
|
149
|
+
const activities: { icon: typeof CheckCircle2; text: string; time: string; color: string }[] = [];
|
|
150
|
+
|
|
151
|
+
// Add completed tasks
|
|
152
|
+
const completed = tasks.filter(t => t.status === 'completed').slice(0, 2);
|
|
153
|
+
completed.forEach(t => {
|
|
154
|
+
activities.push({
|
|
155
|
+
icon: CheckCircle2,
|
|
156
|
+
text: `Task completed: ${t.title.slice(0, 30)}${t.title.length > 30 ? '...' : ''}`,
|
|
157
|
+
time: 'Recently',
|
|
158
|
+
color: 'text-green-400',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Add in progress
|
|
163
|
+
const inProgress = tasks.filter(t => t.status === 'in_progress').slice(0, 2);
|
|
164
|
+
inProgress.forEach(t => {
|
|
165
|
+
activities.push({
|
|
166
|
+
icon: Clock,
|
|
167
|
+
text: `In progress: ${t.title.slice(0, 30)}${t.title.length > 30 ? '...' : ''}`,
|
|
168
|
+
time: 'Active',
|
|
169
|
+
color: 'text-blue-400',
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Add recent specs/stories
|
|
174
|
+
if (requirements.length > 0) {
|
|
175
|
+
activities.push({
|
|
176
|
+
icon: FileText,
|
|
177
|
+
text: `${requirements.length} user stories loaded`,
|
|
178
|
+
time: 'Project',
|
|
179
|
+
color: 'text-purple-400',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (decisions.length > 0) {
|
|
184
|
+
activities.push({
|
|
185
|
+
icon: Scale,
|
|
186
|
+
text: `${decisions.length} ADRs documented`,
|
|
187
|
+
time: 'Project',
|
|
188
|
+
color: 'text-amber-400',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return activities.slice(0, 5);
|
|
193
|
+
}, [tasks, requirements, decisions]);
|
|
194
|
+
|
|
195
|
+
if (isLoading) {
|
|
196
|
+
return (
|
|
197
|
+
<div className="h-full overflow-auto bg-[#0a0a0f] p-6">
|
|
198
|
+
{/* Header Skeleton */}
|
|
199
|
+
<div className="mb-6">
|
|
200
|
+
<Skeleton className="h-6 w-32 mb-2" />
|
|
201
|
+
<Skeleton className="h-4 w-48" />
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Stat Cards Skeleton */}
|
|
205
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
206
|
+
{[1, 2, 3, 4].map((i) => (
|
|
207
|
+
<div key={i} className="bg-[#1a1a24] rounded-lg border border-white/10 p-4">
|
|
208
|
+
<div className="flex items-center justify-between mb-3">
|
|
209
|
+
<Skeleton className="w-10 h-10 rounded-lg" />
|
|
210
|
+
<Skeleton className="w-12 h-8" />
|
|
211
|
+
</div>
|
|
212
|
+
<Skeleton className="h-4 w-20 mb-1" />
|
|
213
|
+
<Skeleton className="h-3 w-24" />
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Progress Skeleton */}
|
|
219
|
+
<div className="bg-[#1a1a24] rounded-lg border border-white/10 p-4 mb-6">
|
|
220
|
+
<Skeleton className="h-4 w-32 mb-3" />
|
|
221
|
+
<Skeleton className="h-3 w-full rounded-full" />
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Two Column Skeleton */}
|
|
225
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
226
|
+
<SkeletonCard />
|
|
227
|
+
<SkeletonCard />
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div className="h-full overflow-auto bg-[#0a0a0f] p-6">
|
|
235
|
+
{/* Header */}
|
|
236
|
+
<div className="flex items-center justify-between mb-6">
|
|
237
|
+
<div>
|
|
238
|
+
<h1 className="text-xl font-semibold text-white">Dashboard</h1>
|
|
239
|
+
<p className="text-sm text-gray-500">Project overview and health status</p>
|
|
240
|
+
</div>
|
|
241
|
+
<button
|
|
242
|
+
onClick={handleRefresh}
|
|
243
|
+
disabled={isRefreshing}
|
|
244
|
+
className="p-2 rounded-lg hover:bg-white/10 text-gray-400 hover:text-white transition-colors disabled:opacity-50"
|
|
245
|
+
title="Refresh"
|
|
246
|
+
>
|
|
247
|
+
<RefreshCw className={cn('w-5 h-5', isRefreshing && 'animate-spin')} />
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Stat Cards */}
|
|
252
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
253
|
+
<StatCard
|
|
254
|
+
title="User Stories"
|
|
255
|
+
value={stats.stories}
|
|
256
|
+
subtitle="Requirements defined"
|
|
257
|
+
icon={<ListTodo className="w-5 h-5 text-blue-400" />}
|
|
258
|
+
color="bg-blue-500/10"
|
|
259
|
+
/>
|
|
260
|
+
<StatCard
|
|
261
|
+
title="Tasks"
|
|
262
|
+
value={stats.tasks}
|
|
263
|
+
subtitle={`${stats.completedTasks} completed`}
|
|
264
|
+
icon={<CheckSquare className="w-5 h-5 text-green-400" />}
|
|
265
|
+
color="bg-green-500/10"
|
|
266
|
+
/>
|
|
267
|
+
<StatCard
|
|
268
|
+
title="Decisions"
|
|
269
|
+
value={stats.decisions}
|
|
270
|
+
subtitle="ADRs documented"
|
|
271
|
+
icon={<Scale className="w-5 h-5 text-amber-400" />}
|
|
272
|
+
color="bg-amber-500/10"
|
|
273
|
+
/>
|
|
274
|
+
<StatCard
|
|
275
|
+
title="Specs"
|
|
276
|
+
value={stats.specs}
|
|
277
|
+
subtitle="Specifications"
|
|
278
|
+
icon={<FileText className="w-5 h-5 text-purple-400" />}
|
|
279
|
+
color="bg-purple-500/10"
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Progress Bar */}
|
|
284
|
+
<div className="bg-[#1a1a24] rounded-lg border border-white/10 p-4 mb-6">
|
|
285
|
+
<div className="flex items-center justify-between mb-2">
|
|
286
|
+
<h3 className="text-sm font-medium text-white">Overall Progress</h3>
|
|
287
|
+
<span className="text-sm font-bold text-purple-400">{stats.progress}%</span>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="h-3 bg-[#0a0a0f] rounded-full overflow-hidden">
|
|
290
|
+
<div
|
|
291
|
+
className="h-full bg-gradient-to-r from-purple-500 to-purple-400 rounded-full transition-all duration-500"
|
|
292
|
+
style={{ width: `${stats.progress}%` }}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex items-center justify-between mt-2 text-xs text-gray-500">
|
|
296
|
+
<span>{stats.completedTasks} completed</span>
|
|
297
|
+
<span>{stats.inProgressTasks} in progress</span>
|
|
298
|
+
<span>{stats.pendingTasks} pending</span>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* Two Column Layout */}
|
|
303
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
304
|
+
{/* Recent Activity */}
|
|
305
|
+
<div className="bg-[#1a1a24] rounded-lg border border-white/10 p-4">
|
|
306
|
+
<div className="flex items-center gap-2 mb-4">
|
|
307
|
+
<Activity className="w-4 h-4 text-purple-400" />
|
|
308
|
+
<h3 className="text-sm font-medium text-white">Recent Activity</h3>
|
|
309
|
+
</div>
|
|
310
|
+
<div className="space-y-3">
|
|
311
|
+
{recentActivity.length > 0 ? (
|
|
312
|
+
recentActivity.map((activity, index) => (
|
|
313
|
+
<div key={index} className="flex items-start gap-3">
|
|
314
|
+
<activity.icon className={cn('w-4 h-4 mt-0.5', activity.color)} />
|
|
315
|
+
<div className="flex-1 min-w-0">
|
|
316
|
+
<p className="text-sm text-white truncate">{activity.text}</p>
|
|
317
|
+
<p className="text-xs text-gray-500">{activity.time}</p>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
))
|
|
321
|
+
) : (
|
|
322
|
+
<p className="text-sm text-gray-500 text-center py-4">No recent activity</p>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* Health Check */}
|
|
328
|
+
<div className="bg-[#1a1a24] rounded-lg border border-white/10 p-4">
|
|
329
|
+
<div className="flex items-center gap-2 mb-4">
|
|
330
|
+
<Wifi className="w-4 h-4 text-purple-400" />
|
|
331
|
+
<h3 className="text-sm font-medium text-white">Health Check</h3>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="space-y-1">
|
|
334
|
+
<HealthItem
|
|
335
|
+
label="Claude CLI"
|
|
336
|
+
status={health?.claudeCli?.installed ? 'ok' : 'error'}
|
|
337
|
+
message={health?.claudeCli?.version || 'Not detected'}
|
|
338
|
+
/>
|
|
339
|
+
<HealthItem
|
|
340
|
+
label="Project Structure"
|
|
341
|
+
status={health?.project?.valid ? 'ok' : 'warning'}
|
|
342
|
+
message={health?.project?.hasDevflow ? 'DevFlow configured' : 'No .devflow folder'}
|
|
343
|
+
/>
|
|
344
|
+
<HealthItem
|
|
345
|
+
label="Git Repository"
|
|
346
|
+
status="ok"
|
|
347
|
+
message="Connected"
|
|
348
|
+
/>
|
|
349
|
+
{stats.blockedTasks > 0 ? (
|
|
350
|
+
<HealthItem
|
|
351
|
+
label="Blocked Tasks"
|
|
352
|
+
status="warning"
|
|
353
|
+
message={`${stats.blockedTasks} tasks need attention`}
|
|
354
|
+
/>
|
|
355
|
+
) : (
|
|
356
|
+
<HealthItem
|
|
357
|
+
label="No Blockers"
|
|
358
|
+
status="ok"
|
|
359
|
+
message="All tasks are progressing"
|
|
360
|
+
/>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Task Breakdown */}
|
|
367
|
+
<div className="mt-6 bg-[#1a1a24] rounded-lg border border-white/10 p-4">
|
|
368
|
+
<div className="flex items-center gap-2 mb-4">
|
|
369
|
+
<CheckSquare className="w-4 h-4 text-purple-400" />
|
|
370
|
+
<h3 className="text-sm font-medium text-white">Task Breakdown</h3>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
373
|
+
<div className="text-center p-3 rounded-lg bg-gray-500/10">
|
|
374
|
+
<p className="text-2xl font-bold text-gray-400">{stats.pendingTasks}</p>
|
|
375
|
+
<p className="text-xs text-gray-500">Pending</p>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="text-center p-3 rounded-lg bg-blue-500/10">
|
|
378
|
+
<p className="text-2xl font-bold text-blue-400">{stats.inProgressTasks}</p>
|
|
379
|
+
<p className="text-xs text-gray-500">In Progress</p>
|
|
380
|
+
</div>
|
|
381
|
+
<div className="text-center p-3 rounded-lg bg-green-500/10">
|
|
382
|
+
<p className="text-2xl font-bold text-green-400">{stats.completedTasks}</p>
|
|
383
|
+
<p className="text-xs text-gray-500">Completed</p>
|
|
384
|
+
</div>
|
|
385
|
+
<div className="text-center p-3 rounded-lg bg-red-500/10">
|
|
386
|
+
<p className="text-2xl font-bold text-red-400">{stats.blockedTasks}</p>
|
|
387
|
+
<p className="text-xs text-gray-500">Blocked</p>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { ChevronRight, Folder, FileText, Home } from 'lucide-react';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { useFileStore } from '@/lib/stores/fileStore';
|
|
7
|
+
import { useProjectStore } from '@/lib/stores/projectStore';
|
|
8
|
+
|
|
9
|
+
interface BreadcrumbsProps {
|
|
10
|
+
path: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BreadcrumbSegment {
|
|
15
|
+
name: string;
|
|
16
|
+
path: string;
|
|
17
|
+
isFile: boolean;
|
|
18
|
+
isLast: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Breadcrumbs component for file path navigation.
|
|
23
|
+
* Displays the current file path as clickable segments.
|
|
24
|
+
* Clicking a folder segment expands it in the file explorer.
|
|
25
|
+
*/
|
|
26
|
+
export function Breadcrumbs({ path, className }: BreadcrumbsProps) {
|
|
27
|
+
const { setExpandedFolders, expandedFolders, toggleFolder } = useFileStore();
|
|
28
|
+
const { currentProject } = useProjectStore();
|
|
29
|
+
const projectPath = currentProject?.path;
|
|
30
|
+
|
|
31
|
+
const segments = useMemo((): BreadcrumbSegment[] => {
|
|
32
|
+
if (!path) return [];
|
|
33
|
+
|
|
34
|
+
// Get relative path from project root
|
|
35
|
+
let relativePath = path;
|
|
36
|
+
if (projectPath && path.startsWith(projectPath)) {
|
|
37
|
+
relativePath = path.slice(projectPath.length);
|
|
38
|
+
if (relativePath.startsWith('/')) {
|
|
39
|
+
relativePath = relativePath.slice(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parts = relativePath.split('/').filter(Boolean);
|
|
44
|
+
let currentPath = projectPath || '';
|
|
45
|
+
|
|
46
|
+
return parts.map((name, index) => {
|
|
47
|
+
currentPath = currentPath ? `${currentPath}/${name}` : name;
|
|
48
|
+
const isLast = index === parts.length - 1;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
path: currentPath,
|
|
53
|
+
isFile: isLast,
|
|
54
|
+
isLast,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}, [path, projectPath]);
|
|
58
|
+
|
|
59
|
+
const handleSegmentClick = (segment: BreadcrumbSegment) => {
|
|
60
|
+
if (segment.isFile) return;
|
|
61
|
+
|
|
62
|
+
// Toggle folder expansion and ensure all parent folders are expanded
|
|
63
|
+
const parentPath = segment.path;
|
|
64
|
+
const pathParts = parentPath.split('/');
|
|
65
|
+
const newExpanded = new Set(expandedFolders);
|
|
66
|
+
|
|
67
|
+
// Expand all parent folders
|
|
68
|
+
let buildPath = '';
|
|
69
|
+
for (const part of pathParts) {
|
|
70
|
+
buildPath = buildPath ? `${buildPath}/${part}` : part;
|
|
71
|
+
newExpanded.add(buildPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setExpandedFolders(newExpanded);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (segments.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<nav
|
|
83
|
+
className={cn(
|
|
84
|
+
'flex items-center gap-1 px-3 py-1.5 text-xs text-gray-400 bg-[#0a0a0f] border-b border-white/5 overflow-x-auto',
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
aria-label="File path breadcrumb"
|
|
88
|
+
>
|
|
89
|
+
{/* Project root icon */}
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => {
|
|
92
|
+
// Collapse all folders to show root
|
|
93
|
+
setExpandedFolders(new Set());
|
|
94
|
+
}}
|
|
95
|
+
className="flex items-center gap-1 hover:text-white transition-colors p-1 rounded hover:bg-white/5"
|
|
96
|
+
aria-label="Go to project root"
|
|
97
|
+
title="Project root"
|
|
98
|
+
>
|
|
99
|
+
<Home className="w-3.5 h-3.5" />
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
{segments.map((segment, index) => (
|
|
103
|
+
<div key={segment.path} className="flex items-center gap-1">
|
|
104
|
+
<ChevronRight className="w-3 h-3 text-gray-600 flex-shrink-0" aria-hidden="true" />
|
|
105
|
+
|
|
106
|
+
{segment.isFile ? (
|
|
107
|
+
// File segment - not clickable
|
|
108
|
+
<span
|
|
109
|
+
className="flex items-center gap-1.5 text-gray-300 font-medium"
|
|
110
|
+
aria-current="page"
|
|
111
|
+
>
|
|
112
|
+
<FileText className="w-3.5 h-3.5 text-gray-500" aria-hidden="true" />
|
|
113
|
+
<span className="truncate max-w-[150px]">{segment.name}</span>
|
|
114
|
+
</span>
|
|
115
|
+
) : (
|
|
116
|
+
// Folder segment - clickable
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => handleSegmentClick(segment)}
|
|
119
|
+
className={cn(
|
|
120
|
+
'flex items-center gap-1.5 hover:text-white transition-colors',
|
|
121
|
+
'p-1 rounded hover:bg-white/5',
|
|
122
|
+
'focus:outline-none focus:ring-1 focus:ring-purple-500/50'
|
|
123
|
+
)}
|
|
124
|
+
title={`Open folder: ${segment.path}`}
|
|
125
|
+
>
|
|
126
|
+
<Folder className="w-3.5 h-3.5 text-yellow-500/70" aria-hidden="true" />
|
|
127
|
+
<span className="truncate max-w-[100px]">{segment.name}</span>
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</nav>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useFileStore } from '@/lib/stores/fileStore';
|
|
4
|
+
import { useUIStore } from '@/lib/stores/uiStore';
|
|
5
|
+
import { EditorTabs } from './EditorTabs';
|
|
6
|
+
import { Breadcrumbs } from './Breadcrumbs';
|
|
7
|
+
import { MonacoEditor } from './MonacoEditor';
|
|
8
|
+
import { MarkdownPreview } from './MarkdownPreview';
|
|
9
|
+
import { FileText, Eye, Columns, Code2, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
|
|
12
|
+
export function EditorPanel() {
|
|
13
|
+
const { openFiles, activeFile, navigateBack, navigateForward, canGoBack, canGoForward } = useFileStore();
|
|
14
|
+
const { previewVisible, togglePreview } = useUIStore();
|
|
15
|
+
|
|
16
|
+
const activeOpenFile = openFiles.find((f) => f.path === activeFile);
|
|
17
|
+
const isMarkdown = activeOpenFile?.language === 'markdown';
|
|
18
|
+
|
|
19
|
+
if (openFiles.length === 0) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="h-full flex items-center justify-center bg-[#0a0a0f] text-gray-500">
|
|
22
|
+
<div className="text-center space-y-4">
|
|
23
|
+
<div className="w-20 h-20 mx-auto bg-white/5 rounded-2xl flex items-center justify-center">
|
|
24
|
+
<Code2 className="w-10 h-10 text-gray-600" />
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<p className="text-lg text-gray-400">No file open</p>
|
|
28
|
+
<p className="text-sm text-gray-600">Select a file from the explorer to start editing</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="h-full flex flex-col bg-[#0a0a0f]">
|
|
37
|
+
{/* Tabs */}
|
|
38
|
+
<div className="flex items-center border-b border-white/10 bg-[#08080c]">
|
|
39
|
+
{/* Navigation buttons */}
|
|
40
|
+
<div className="flex items-center px-1 border-r border-white/10">
|
|
41
|
+
<button
|
|
42
|
+
onClick={navigateBack}
|
|
43
|
+
disabled={!canGoBack()}
|
|
44
|
+
className={cn(
|
|
45
|
+
'p-1.5 rounded-lg transition-colors',
|
|
46
|
+
canGoBack()
|
|
47
|
+
? 'text-gray-400 hover:text-white hover:bg-white/10'
|
|
48
|
+
: 'text-gray-700 cursor-not-allowed'
|
|
49
|
+
)}
|
|
50
|
+
title="Go Back (Alt+Left)"
|
|
51
|
+
aria-label="Go back"
|
|
52
|
+
>
|
|
53
|
+
<ChevronLeft className="w-4 h-4" />
|
|
54
|
+
</button>
|
|
55
|
+
<button
|
|
56
|
+
onClick={navigateForward}
|
|
57
|
+
disabled={!canGoForward()}
|
|
58
|
+
className={cn(
|
|
59
|
+
'p-1.5 rounded-lg transition-colors',
|
|
60
|
+
canGoForward()
|
|
61
|
+
? 'text-gray-400 hover:text-white hover:bg-white/10'
|
|
62
|
+
: 'text-gray-700 cursor-not-allowed'
|
|
63
|
+
)}
|
|
64
|
+
title="Go Forward (Alt+Right)"
|
|
65
|
+
aria-label="Go forward"
|
|
66
|
+
>
|
|
67
|
+
<ChevronRight className="w-4 h-4" />
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<EditorTabs />
|
|
72
|
+
|
|
73
|
+
{/* Preview toggle for markdown */}
|
|
74
|
+
{isMarkdown && (
|
|
75
|
+
<div className="flex items-center px-2 border-l border-white/10">
|
|
76
|
+
<button
|
|
77
|
+
onClick={togglePreview}
|
|
78
|
+
className={cn(
|
|
79
|
+
'p-1.5 rounded-lg transition-colors',
|
|
80
|
+
previewVisible
|
|
81
|
+
? 'bg-purple-500/20 text-purple-400'
|
|
82
|
+
: 'text-gray-500 hover:text-white hover:bg-white/10'
|
|
83
|
+
)}
|
|
84
|
+
title={previewVisible ? 'Hide Preview' : 'Show Preview'}
|
|
85
|
+
aria-label={previewVisible ? 'Hide preview' : 'Show preview'}
|
|
86
|
+
aria-pressed={previewVisible}
|
|
87
|
+
>
|
|
88
|
+
{previewVisible ? (
|
|
89
|
+
<Columns className="w-4 h-4" />
|
|
90
|
+
) : (
|
|
91
|
+
<Eye className="w-4 h-4" />
|
|
92
|
+
)}
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Breadcrumbs */}
|
|
99
|
+
{activeFile && <Breadcrumbs path={activeFile} />}
|
|
100
|
+
|
|
101
|
+
{/* Editor content */}
|
|
102
|
+
<div className="flex-1 flex overflow-hidden">
|
|
103
|
+
{/* Monaco Editor */}
|
|
104
|
+
<div className={cn(
|
|
105
|
+
'h-full',
|
|
106
|
+
previewVisible && isMarkdown ? 'w-1/2' : 'w-full'
|
|
107
|
+
)}>
|
|
108
|
+
{activeOpenFile && <MonacoEditor file={activeOpenFile} />}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Markdown Preview */}
|
|
112
|
+
{previewVisible && isMarkdown && activeOpenFile && (
|
|
113
|
+
<div className="w-1/2 h-full border-l border-white/10 overflow-auto bg-[#0a0a0f]">
|
|
114
|
+
<MarkdownPreview content={activeOpenFile.content} />
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|