@evolve.labs/devflow 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. package/web/tsconfig.json +41 -0
@@ -0,0 +1,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
+ }