@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,229 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { useFileStore } from '@/lib/stores/fileStore';
5
+ import { cn, getFileName } from '@/lib/utils';
6
+ import { X, FileText, FileCode, FileJson, Settings, Loader2, Pin, ChevronLeft, ChevronRight } from 'lucide-react';
7
+ import { TabContextMenu } from './TabContextMenu';
8
+
9
+ // Get icon by language
10
+ function getLanguageIcon(language: string) {
11
+ switch (language) {
12
+ case 'markdown':
13
+ return FileText;
14
+ case 'typescript':
15
+ case 'javascript':
16
+ return FileCode;
17
+ case 'json':
18
+ return FileJson;
19
+ case 'yaml':
20
+ return Settings;
21
+ default:
22
+ return FileText;
23
+ }
24
+ }
25
+
26
+ // Get color by language
27
+ function getLanguageColor(language: string): string {
28
+ switch (language) {
29
+ case 'typescript':
30
+ return 'text-blue-400';
31
+ case 'javascript':
32
+ return 'text-yellow-400';
33
+ case 'markdown':
34
+ return 'text-gray-400';
35
+ case 'json':
36
+ return 'text-amber-400';
37
+ case 'yaml':
38
+ return 'text-purple-400';
39
+ default:
40
+ return 'text-gray-400';
41
+ }
42
+ }
43
+
44
+ interface ContextMenuState {
45
+ path: string;
46
+ position: { x: number; y: number };
47
+ }
48
+
49
+ export function EditorTabs() {
50
+ const {
51
+ openFiles,
52
+ activeFile,
53
+ setActiveFile,
54
+ closeFile,
55
+ isSaving,
56
+ savingFile,
57
+ pinnedFiles,
58
+ navigateBack,
59
+ navigateForward,
60
+ canGoBack,
61
+ canGoForward,
62
+ } = useFileStore();
63
+ const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
64
+
65
+ const handleNavigateBack = useCallback(() => {
66
+ if (canGoBack()) {
67
+ navigateBack();
68
+ }
69
+ }, [canGoBack, navigateBack]);
70
+
71
+ const handleNavigateForward = useCallback(() => {
72
+ if (canGoForward()) {
73
+ navigateForward();
74
+ }
75
+ }, [canGoForward, navigateForward]);
76
+
77
+ const handleClose = useCallback((e: React.MouseEvent, path: string) => {
78
+ e.stopPropagation();
79
+
80
+ // Check if dirty
81
+ const file = openFiles.find((f) => f.path === path);
82
+ if (file?.isDirty) {
83
+ const shouldClose = confirm('File has unsaved changes. Close anyway?');
84
+ if (!shouldClose) return;
85
+ }
86
+
87
+ closeFile(path);
88
+ }, [openFiles, closeFile]);
89
+
90
+ const handleMiddleClick = useCallback((e: React.MouseEvent, path: string) => {
91
+ if (e.button === 1) {
92
+ handleClose(e, path);
93
+ }
94
+ }, [handleClose]);
95
+
96
+ const handleContextMenu = useCallback((e: React.MouseEvent, path: string) => {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+ setContextMenu({
100
+ path,
101
+ position: { x: e.clientX, y: e.clientY },
102
+ });
103
+ }, []);
104
+
105
+ const closeContextMenu = useCallback(() => {
106
+ setContextMenu(null);
107
+ }, []);
108
+
109
+ return (
110
+ <>
111
+ {/* Navigation buttons */}
112
+ <div className="flex items-center gap-0.5 px-1 border-r border-white/10">
113
+ <button
114
+ onClick={handleNavigateBack}
115
+ disabled={!canGoBack()}
116
+ className={cn(
117
+ 'p-1.5 rounded transition-colors',
118
+ canGoBack()
119
+ ? 'text-gray-400 hover:text-white hover:bg-white/10'
120
+ : 'text-gray-600 cursor-not-allowed'
121
+ )}
122
+ aria-label="Go back"
123
+ title="Go back (Alt+Left)"
124
+ >
125
+ <ChevronLeft className="w-4 h-4" aria-hidden="true" />
126
+ </button>
127
+ <button
128
+ onClick={handleNavigateForward}
129
+ disabled={!canGoForward()}
130
+ className={cn(
131
+ 'p-1.5 rounded transition-colors',
132
+ canGoForward()
133
+ ? 'text-gray-400 hover:text-white hover:bg-white/10'
134
+ : 'text-gray-600 cursor-not-allowed'
135
+ )}
136
+ aria-label="Go forward"
137
+ title="Go forward (Alt+Right)"
138
+ >
139
+ <ChevronRight className="w-4 h-4" aria-hidden="true" />
140
+ </button>
141
+ </div>
142
+
143
+ <div
144
+ className="flex-1 flex items-center overflow-x-auto"
145
+ role="tablist"
146
+ aria-label="Open files"
147
+ >
148
+ {openFiles.map((file) => {
149
+ const Icon = getLanguageIcon(file.language);
150
+ const iconColor = getLanguageColor(file.language);
151
+ const isActive = activeFile === file.path;
152
+ const isPinned = pinnedFiles.includes(file.path);
153
+
154
+ return (
155
+ <div
156
+ key={file.path}
157
+ className={cn(
158
+ 'group flex items-center gap-2 px-3 py-2 border-r border-white/10 cursor-pointer transition-colors',
159
+ isActive
160
+ ? 'bg-[#0a0a0f] text-white'
161
+ : 'bg-[#08080c] text-gray-400 hover:text-white hover:bg-white/5',
162
+ isPinned && 'border-l-2 border-l-purple-500/50'
163
+ )}
164
+ onClick={() => setActiveFile(file.path)}
165
+ onMouseDown={(e) => handleMiddleClick(e, file.path)}
166
+ onContextMenu={(e) => handleContextMenu(e, file.path)}
167
+ role="tab"
168
+ aria-selected={isActive}
169
+ aria-label={`${file.name}${file.isDirty ? ' (unsaved)' : ''}${isPinned ? ' (pinned)' : ''}`}
170
+ tabIndex={isActive ? 0 : -1}
171
+ >
172
+ {/* Pin indicator */}
173
+ {isPinned && (
174
+ <Pin
175
+ className="w-3 h-3 text-purple-400 flex-shrink-0"
176
+ aria-hidden="true"
177
+ />
178
+ )}
179
+
180
+ <Icon className={cn('w-4 h-4 flex-shrink-0', iconColor)} aria-hidden="true" />
181
+
182
+ <span className="text-sm truncate max-w-[120px]">
183
+ {file.name}
184
+ </span>
185
+
186
+ {/* Saving/Dirty indicator */}
187
+ {isSaving && savingFile === file.path ? (
188
+ <Loader2
189
+ className="w-3 h-3 animate-spin text-purple-400 flex-shrink-0"
190
+ aria-label="Saving"
191
+ />
192
+ ) : file.isDirty ? (
193
+ <span
194
+ className="w-2 h-2 rounded-full bg-purple-500 flex-shrink-0"
195
+ aria-label="Unsaved changes"
196
+ />
197
+ ) : null}
198
+
199
+ {/* Close button - hidden for pinned tabs unless hovered */}
200
+ <button
201
+ onClick={(e) => handleClose(e, file.path)}
202
+ className={cn(
203
+ 'p-0.5 rounded hover:bg-white/10 transition-colors flex-shrink-0',
204
+ isPinned
205
+ ? 'opacity-0 group-hover:opacity-100'
206
+ : isActive
207
+ ? 'opacity-100'
208
+ : 'opacity-0 group-hover:opacity-100'
209
+ )}
210
+ aria-label={`Close ${file.name}`}
211
+ >
212
+ <X className="w-3 h-3" />
213
+ </button>
214
+ </div>
215
+ );
216
+ })}
217
+ </div>
218
+
219
+ {/* Context Menu */}
220
+ {contextMenu && (
221
+ <TabContextMenu
222
+ path={contextMenu.path}
223
+ position={contextMenu.position}
224
+ onClose={closeContextMenu}
225
+ />
226
+ )}
227
+ </>
228
+ );
229
+ }
@@ -0,0 +1,154 @@
1
+ 'use client';
2
+
3
+ import { useMemo, Suspense, lazy } from 'react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+
7
+ // Lazy load Mermaid - only loaded when markdown contains mermaid blocks
8
+ const MermaidDiagram = lazy(() => import('./MermaidDiagram').then(m => ({ default: m.MermaidDiagram })));
9
+
10
+ interface MarkdownPreviewProps {
11
+ content: string;
12
+ }
13
+
14
+ function MermaidFallback() {
15
+ return (
16
+ <div className="my-4 p-4 bg-muted rounded-lg animate-pulse">
17
+ <div className="h-32 bg-white/5 rounded" />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ export function MarkdownPreview({ content }: MarkdownPreviewProps) {
23
+ // Memoize components object to prevent recreation on every render
24
+ const components = useMemo(() => ({
25
+ // Custom heading styles
26
+ h1: ({ children }: { children?: React.ReactNode }) => (
27
+ <h1 className="text-2xl font-bold border-b border-border pb-2 mb-4">
28
+ {children}
29
+ </h1>
30
+ ),
31
+ h2: ({ children }: { children?: React.ReactNode }) => (
32
+ <h2 className="text-xl font-semibold mt-6 mb-3">{children}</h2>
33
+ ),
34
+ h3: ({ children }: { children?: React.ReactNode }) => (
35
+ <h3 className="text-lg font-semibold mt-4 mb-2">{children}</h3>
36
+ ),
37
+
38
+ // Code blocks
39
+ code: ({ className, children, ...props }: { className?: string; children?: React.ReactNode }) => {
40
+ const match = /language-(\w+)/.exec(className || '');
41
+ const language = match ? match[1] : '';
42
+ const codeContent = String(children).replace(/\n$/, '');
43
+
44
+ // Handle mermaid diagrams with lazy loading
45
+ if (language === 'mermaid') {
46
+ return (
47
+ <Suspense fallback={<MermaidFallback />}>
48
+ <MermaidDiagram chart={codeContent} className="my-4" />
49
+ </Suspense>
50
+ );
51
+ }
52
+
53
+ // Inline code (no className and no newlines)
54
+ const isInline = !className && !codeContent.includes('\n');
55
+ if (isInline) {
56
+ return (
57
+ <code
58
+ className="px-1.5 py-0.5 rounded bg-muted text-sm font-mono"
59
+ {...props}
60
+ >
61
+ {children}
62
+ </code>
63
+ );
64
+ }
65
+
66
+ // Block code with optional language label
67
+ return (
68
+ <div className="relative group my-4">
69
+ {language && (
70
+ <div className="absolute top-2 right-2 px-2 py-0.5 text-xs text-gray-500 bg-black/30 rounded">
71
+ {language}
72
+ </div>
73
+ )}
74
+ <code
75
+ className="block p-4 rounded-lg bg-muted text-sm font-mono overflow-x-auto"
76
+ {...props}
77
+ >
78
+ {children}
79
+ </code>
80
+ </div>
81
+ );
82
+ },
83
+
84
+ // Tables
85
+ table: ({ children }: { children?: React.ReactNode }) => (
86
+ <div className="overflow-x-auto my-4">
87
+ <table className="min-w-full border-collapse border border-border">
88
+ {children}
89
+ </table>
90
+ </div>
91
+ ),
92
+ th: ({ children }: { children?: React.ReactNode }) => (
93
+ <th className="border border-border bg-muted px-3 py-2 text-left font-semibold">
94
+ {children}
95
+ </th>
96
+ ),
97
+ td: ({ children }: { children?: React.ReactNode }) => (
98
+ <td className="border border-border px-3 py-2">{children}</td>
99
+ ),
100
+
101
+ // Links
102
+ a: ({ href, children }: { href?: string; children?: React.ReactNode }) => (
103
+ <a
104
+ href={href}
105
+ className="text-primary hover:underline"
106
+ target={href?.startsWith('http') ? '_blank' : undefined}
107
+ rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
108
+ >
109
+ {children}
110
+ </a>
111
+ ),
112
+
113
+ // Blockquotes
114
+ blockquote: ({ children }: { children?: React.ReactNode }) => (
115
+ <blockquote className="border-l-4 border-primary/50 pl-4 italic text-muted-foreground my-4">
116
+ {children}
117
+ </blockquote>
118
+ ),
119
+
120
+ // Lists
121
+ ul: ({ children }: { children?: React.ReactNode }) => (
122
+ <ul className="list-disc list-inside my-2 space-y-1">{children}</ul>
123
+ ),
124
+ ol: ({ children }: { children?: React.ReactNode }) => (
125
+ <ol className="list-decimal list-inside my-2 space-y-1">{children}</ol>
126
+ ),
127
+
128
+ // Checkboxes (GFM)
129
+ input: ({ type, checked }: { type?: string; checked?: boolean }) => {
130
+ if (type === 'checkbox') {
131
+ return (
132
+ <input
133
+ type="checkbox"
134
+ checked={checked}
135
+ readOnly
136
+ className="mr-2 rounded"
137
+ />
138
+ );
139
+ }
140
+ return null;
141
+ },
142
+ }), []);
143
+
144
+ return (
145
+ <div className="p-6 prose prose-invert prose-sm max-w-none">
146
+ <ReactMarkdown
147
+ remarkPlugins={[remarkGfm]}
148
+ components={components}
149
+ >
150
+ {content}
151
+ </ReactMarkdown>
152
+ </div>
153
+ );
154
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import mermaid from 'mermaid';
5
+ import { AlertCircle, RefreshCw } from 'lucide-react';
6
+ import { cn } from '@/lib/utils';
7
+
8
+ interface MermaidDiagramProps {
9
+ chart: string;
10
+ className?: string;
11
+ }
12
+
13
+ let mermaidInitialized = false;
14
+
15
+ function initMermaid() {
16
+ if (mermaidInitialized) return;
17
+
18
+ mermaid.initialize({
19
+ startOnLoad: false,
20
+ theme: 'dark',
21
+ securityLevel: 'strict',
22
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
23
+ themeVariables: {
24
+ primaryColor: '#8b5cf6',
25
+ primaryTextColor: '#fff',
26
+ primaryBorderColor: '#6d28d9',
27
+ lineColor: '#64748b',
28
+ secondaryColor: '#1e1b4b',
29
+ tertiaryColor: '#1a1a24',
30
+ background: '#0a0a0f',
31
+ mainBkg: '#1a1a24',
32
+ nodeBorder: '#6d28d9',
33
+ clusterBkg: '#1e1b4b',
34
+ clusterBorder: '#4c1d95',
35
+ titleColor: '#e2e8f0',
36
+ edgeLabelBackground: '#1a1a24',
37
+ textColor: '#e2e8f0',
38
+ nodeTextColor: '#fff',
39
+ },
40
+ });
41
+
42
+ mermaidInitialized = true;
43
+ }
44
+
45
+ export function MermaidDiagram({ chart, className }: MermaidDiagramProps) {
46
+ const containerRef = useRef<HTMLDivElement>(null);
47
+ const [svg, setSvg] = useState<string>('');
48
+ const [error, setError] = useState<string | null>(null);
49
+ const [isLoading, setIsLoading] = useState(true);
50
+ const idRef = useRef(`mermaid-${Math.random().toString(36).substring(2, 11)}`);
51
+
52
+ useEffect(() => {
53
+ initMermaid();
54
+
55
+ const renderDiagram = async () => {
56
+ setIsLoading(true);
57
+ setError(null);
58
+
59
+ try {
60
+ const { svg } = await mermaid.render(idRef.current, chart);
61
+ setSvg(svg);
62
+ } catch (err) {
63
+ console.error('Mermaid render error:', err);
64
+ setError(err instanceof Error ? err.message : 'Failed to render diagram');
65
+ } finally {
66
+ setIsLoading(false);
67
+ }
68
+ };
69
+
70
+ renderDiagram();
71
+ }, [chart]);
72
+
73
+ if (isLoading) {
74
+ return (
75
+ <div className={cn('flex items-center justify-center p-8 bg-[#1a1a24] rounded-lg', className)}>
76
+ <RefreshCw className="w-5 h-5 animate-spin text-purple-400" />
77
+ <span className="ml-2 text-sm text-gray-400">Rendering diagram...</span>
78
+ </div>
79
+ );
80
+ }
81
+
82
+ if (error) {
83
+ return (
84
+ <div className={cn('p-4 bg-red-500/10 border border-red-500/30 rounded-lg', className)}>
85
+ <div className="flex items-center gap-2 text-red-400 mb-2">
86
+ <AlertCircle className="w-4 h-4" />
87
+ <span className="text-sm font-medium">Diagram Error</span>
88
+ </div>
89
+ <pre className="text-xs text-red-300/80 overflow-x-auto whitespace-pre-wrap">{error}</pre>
90
+ <details className="mt-3">
91
+ <summary className="text-xs text-gray-500 cursor-pointer hover:text-gray-400">
92
+ Show source code
93
+ </summary>
94
+ <pre className="mt-2 p-3 text-xs text-gray-400 bg-black/20 rounded overflow-x-auto">
95
+ {chart}
96
+ </pre>
97
+ </details>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ return (
103
+ <div
104
+ ref={containerRef}
105
+ className={cn(
106
+ 'mermaid-diagram bg-[#1a1a24] rounded-lg p-4 overflow-x-auto',
107
+ '[&_svg]:max-w-full [&_svg]:h-auto',
108
+ className
109
+ )}
110
+ dangerouslySetInnerHTML={{ __html: svg }}
111
+ />
112
+ );
113
+ }
@@ -0,0 +1,177 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useRef, memo } from 'react';
4
+ import dynamic from 'next/dynamic';
5
+ import { useFileStore } from '@/lib/stores/fileStore';
6
+ import { useSettingsStore } from '@/lib/stores/settingsStore';
7
+ import { Skeleton, SkeletonText } from '@/components/ui/Skeleton';
8
+ import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
9
+ import type { OpenFile } from '@/lib/types';
10
+ import type { editor } from 'monaco-editor';
11
+ import type * as Monaco from 'monaco-editor';
12
+
13
+ // Editor Loading Skeleton
14
+ function EditorSkeleton() {
15
+ return (
16
+ <div className="h-full bg-[#0a0a0f] p-4">
17
+ {/* Line numbers + code skeleton */}
18
+ <div className="flex gap-4">
19
+ <div className="flex flex-col gap-2 w-8">
20
+ {Array.from({ length: 20 }).map((_, i) => (
21
+ <Skeleton key={i} className="h-4 w-6" />
22
+ ))}
23
+ </div>
24
+ <div className="flex-1 space-y-2">
25
+ {Array.from({ length: 20 }).map((_, i) => (
26
+ <Skeleton
27
+ key={i}
28
+ className="h-4"
29
+ style={{ width: `${Math.random() * 40 + 30}%` }}
30
+ />
31
+ ))}
32
+ </div>
33
+ </div>
34
+ {/* Loading indicator */}
35
+ <div className="absolute inset-0 flex items-center justify-center bg-black/20 backdrop-blur-sm">
36
+ <div className="flex flex-col items-center gap-3">
37
+ <LoadingSpinner size="lg" />
38
+ <span className="text-sm text-gray-400">Loading editor...</span>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ // Lazy load Monaco Editor
46
+ const Editor = dynamic(() => import('@monaco-editor/react'), {
47
+ ssr: false,
48
+ loading: () => <EditorSkeleton />,
49
+ });
50
+
51
+ interface MonacoEditorProps {
52
+ file: OpenFile;
53
+ }
54
+
55
+ function MonacoEditorComponent({ file }: MonacoEditorProps) {
56
+ const { updateFileContent, saveFile, scrollToLine, setScrollToLine } = useFileStore();
57
+ const {
58
+ editorFontSize,
59
+ editorTabSize,
60
+ editorWordWrap,
61
+ editorMinimap,
62
+ editorLineNumbers,
63
+ } = useSettingsStore();
64
+ const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
65
+ const monacoRef = useRef<typeof Monaco | null>(null);
66
+
67
+ const handleEditorDidMount = useCallback(
68
+ (editor: editor.IStandaloneCodeEditor, monaco: typeof Monaco) => {
69
+ editorRef.current = editor;
70
+ monacoRef.current = monaco;
71
+
72
+ // Add save command (Cmd/Ctrl + S)
73
+ editor.addCommand(
74
+ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
75
+ () => {
76
+ saveFile(file.path);
77
+ }
78
+ );
79
+
80
+ // Focus the editor
81
+ editor.focus();
82
+ },
83
+ [file.path, saveFile]
84
+ );
85
+
86
+ // Scroll to specific line when requested
87
+ useEffect(() => {
88
+ if (scrollToLine && editorRef.current && monacoRef.current) {
89
+ const editor = editorRef.current;
90
+ const monaco = monacoRef.current;
91
+
92
+ // Set cursor position to the line
93
+ editor.setPosition({ lineNumber: scrollToLine, column: 1 });
94
+
95
+ // Reveal the line in the center of the viewport
96
+ editor.revealLineInCenter(scrollToLine);
97
+
98
+ // Add a highlight decoration
99
+ const decorations = editor.deltaDecorations([], [
100
+ {
101
+ range: new monaco.Range(scrollToLine, 1, scrollToLine, 1),
102
+ options: {
103
+ isWholeLine: true,
104
+ className: 'highlight-line-animation',
105
+ linesDecorationsClassName: 'highlight-line-margin',
106
+ },
107
+ },
108
+ ]);
109
+
110
+ // Remove decoration after animation
111
+ setTimeout(() => {
112
+ editor.deltaDecorations(decorations, []);
113
+ }, 2000);
114
+
115
+ // Clear the scrollToLine state
116
+ setScrollToLine(null);
117
+ }
118
+ }, [scrollToLine, setScrollToLine]);
119
+
120
+ const handleChange = useCallback(
121
+ (value: string | undefined) => {
122
+ if (value !== undefined) {
123
+ updateFileContent(file.path, value);
124
+ }
125
+ },
126
+ [file.path, updateFileContent]
127
+ );
128
+
129
+ // Focus editor when file changes
130
+ useEffect(() => {
131
+ editorRef.current?.focus();
132
+ }, [file.path]);
133
+
134
+ return (
135
+ <Editor
136
+ height="100%"
137
+ language={file.language}
138
+ value={file.content}
139
+ theme="vs-dark"
140
+ onChange={handleChange}
141
+ onMount={handleEditorDidMount}
142
+ options={{
143
+ fontSize: editorFontSize,
144
+ fontFamily: 'JetBrains Mono, Fira Code, Menlo, Monaco, monospace',
145
+ minimap: { enabled: editorMinimap },
146
+ wordWrap: editorWordWrap ? 'on' : 'off',
147
+ lineNumbers: editorLineNumbers ? 'on' : 'off',
148
+ scrollBeyondLastLine: false,
149
+ automaticLayout: true,
150
+ tabSize: editorTabSize,
151
+ insertSpaces: true,
152
+ renderWhitespace: 'selection',
153
+ bracketPairColorization: { enabled: true },
154
+ guides: {
155
+ indentation: true,
156
+ bracketPairs: true,
157
+ },
158
+ padding: { top: 16, bottom: 16 },
159
+ smoothScrolling: true,
160
+ cursorBlinking: 'smooth',
161
+ cursorSmoothCaretAnimation: 'on',
162
+ }}
163
+ />
164
+ );
165
+ }
166
+
167
+ // Memoize to prevent unnecessary re-renders
168
+ // Note: Settings changes will trigger re-render via useSettingsStore hook
169
+ export const MonacoEditor = memo(MonacoEditorComponent, (prevProps, nextProps) => {
170
+ // Only re-render if file path or language changes
171
+ // Content updates are handled internally by Monaco
172
+ // Settings updates are handled via Zustand subscription
173
+ return (
174
+ prevProps.file.path === nextProps.file.path &&
175
+ prevProps.file.language === nextProps.file.language
176
+ );
177
+ });