@aion0/forge 0.1.10 → 0.2.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.
@@ -0,0 +1,474 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
4
+ import type { WebTerminalHandle, WebTerminalProps } from './WebTerminal';
5
+
6
+ const WebTerminal = lazy(() => import('./WebTerminal'));
7
+
8
+ interface FileNode {
9
+ name: string;
10
+ path: string;
11
+ type: 'file' | 'dir';
12
+ children?: FileNode[];
13
+ }
14
+
15
+ // ─── File Tree ───────────────────────────────────────────
16
+
17
+ type GitStatusMap = Map<string, string>; // path → status
18
+
19
+ function TreeNode({ node, depth, selected, onSelect, gitMap }: {
20
+ node: FileNode;
21
+ depth: number;
22
+ selected: string | null;
23
+ onSelect: (path: string) => void;
24
+ gitMap: GitStatusMap;
25
+ }) {
26
+ // Auto-expand if selected file is under this directory
27
+ const containsSelected = selected ? selected.startsWith(node.path + '/') : false;
28
+ const [manualExpanded, setManualExpanded] = useState<boolean | null>(null);
29
+ const expanded = manualExpanded ?? (depth < 1 || containsSelected);
30
+
31
+ if (node.type === 'dir') {
32
+ const dirHasChanges = node.children?.some(c => hasGitChanges(c, gitMap));
33
+ return (
34
+ <div>
35
+ <button
36
+ onClick={() => setManualExpanded(v => v === null ? !expanded : !v)}
37
+ className="w-full text-left flex items-center gap-1 px-1 py-0.5 hover:bg-[var(--bg-tertiary)] rounded text-xs"
38
+ style={{ paddingLeft: depth * 12 + 4 }}
39
+ >
40
+ <span className="text-[10px] text-[var(--text-secondary)] w-3">{expanded ? '▾' : '▸'}</span>
41
+ <span className={dirHasChanges ? 'text-yellow-400' : 'text-[var(--text-primary)]'}>{node.name}</span>
42
+ </button>
43
+ {expanded && node.children?.map(child => (
44
+ <TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} gitMap={gitMap} />
45
+ ))}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ const isSelected = selected === node.path;
51
+ const gitStatus = gitMap.get(node.path);
52
+ const gitColor = gitStatus
53
+ ? gitStatus.includes('M') ? 'text-yellow-400'
54
+ : gitStatus.includes('D') ? 'text-red-400'
55
+ : 'text-green-400' // A, ?, new
56
+ : '';
57
+
58
+ return (
59
+ <button
60
+ onClick={() => onSelect(node.path)}
61
+ className={`w-full text-left flex items-center gap-1 px-1 py-0.5 rounded text-xs truncate ${
62
+ isSelected ? 'bg-[var(--accent)]/20 text-[var(--accent)]'
63
+ : gitColor ? `hover:bg-[var(--bg-tertiary)] ${gitColor}`
64
+ : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
65
+ }`}
66
+ style={{ paddingLeft: depth * 12 + 16 }}
67
+ title={node.path}
68
+ >
69
+ {node.name}
70
+ </button>
71
+ );
72
+ }
73
+
74
+ function hasGitChanges(node: FileNode, gitMap: GitStatusMap): boolean {
75
+ if (node.type === 'file') return gitMap.has(node.path);
76
+ return node.children?.some(c => hasGitChanges(c, gitMap)) || false;
77
+ }
78
+
79
+ function flattenTree(nodes: FileNode[]): FileNode[] {
80
+ const result: FileNode[] = [];
81
+ for (const node of nodes) {
82
+ if (node.type === 'file') result.push(node);
83
+ if (node.children) result.push(...flattenTree(node.children));
84
+ }
85
+ return result;
86
+ }
87
+
88
+ const LANG_MAP: Record<string, string> = {
89
+ ts: 'TypeScript', tsx: 'TypeScript (JSX)', js: 'JavaScript', jsx: 'JavaScript (JSX)',
90
+ py: 'Python', go: 'Go', rs: 'Rust', java: 'Java', kt: 'Kotlin',
91
+ css: 'CSS', scss: 'SCSS', html: 'HTML', json: 'JSON', yaml: 'YAML', yml: 'YAML',
92
+ md: 'Markdown', sh: 'Shell', sql: 'SQL', toml: 'TOML', xml: 'XML',
93
+ };
94
+
95
+ // ─── Simple syntax highlighting ──────────────────────────
96
+
97
+ const KEYWORDS = new Set([
98
+ 'import', 'export', 'from', 'const', 'let', 'var', 'function', 'return',
99
+ 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue',
100
+ 'class', 'extends', 'new', 'this', 'super', 'typeof', 'instanceof',
101
+ 'try', 'catch', 'finally', 'throw', 'async', 'await', 'yield',
102
+ 'default', 'interface', 'type', 'enum', 'implements', 'readonly',
103
+ 'public', 'private', 'protected', 'static', 'abstract',
104
+ 'true', 'false', 'null', 'undefined', 'void',
105
+ 'def', 'self', 'None', 'True', 'False', 'class', 'lambda', 'with', 'as', 'in', 'not', 'and', 'or',
106
+ 'func', 'package', 'struct', 'go', 'defer', 'select', 'chan', 'map', 'range',
107
+ ]);
108
+
109
+ function highlightLine(line: string, lang: string): React.ReactNode {
110
+ if (!line) return ' ';
111
+
112
+ // Comments
113
+ const commentIdx = lang === 'py' ? line.indexOf('#') :
114
+ line.indexOf('//');
115
+ if (commentIdx === 0 || (commentIdx > 0 && /^\s*$/.test(line.slice(0, commentIdx)))) {
116
+ return <span className="text-gray-500 italic">{line}</span>;
117
+ }
118
+
119
+ // Tokenize with regex
120
+ const parts: React.ReactNode[] = [];
121
+ let lastIdx = 0;
122
+
123
+ const regex = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\b\d+\.?\d*\b)|(\/\/.*$|#.*$)|(\b[A-Z_][A-Z_0-9]+\b)|(\b\w+\b)/g;
124
+ let match;
125
+
126
+ while ((match = regex.exec(line)) !== null) {
127
+ // Text before match
128
+ if (match.index > lastIdx) {
129
+ parts.push(line.slice(lastIdx, match.index));
130
+ }
131
+
132
+ if (match[1]) {
133
+ // String
134
+ parts.push(<span key={match.index} className="text-green-400">{match[0]}</span>);
135
+ } else if (match[2]) {
136
+ // Number
137
+ parts.push(<span key={match.index} className="text-orange-300">{match[0]}</span>);
138
+ } else if (match[3]) {
139
+ // Comment
140
+ parts.push(<span key={match.index} className="text-gray-500 italic">{match[0]}</span>);
141
+ } else if (match[4]) {
142
+ // CONSTANT
143
+ parts.push(<span key={match.index} className="text-cyan-300">{match[0]}</span>);
144
+ } else if (match[5] && KEYWORDS.has(match[5])) {
145
+ // Keyword
146
+ parts.push(<span key={match.index} className="text-purple-400">{match[0]}</span>);
147
+ } else {
148
+ parts.push(match[0]);
149
+ }
150
+ lastIdx = match.index + match[0].length;
151
+ }
152
+
153
+ if (lastIdx < line.length) {
154
+ parts.push(line.slice(lastIdx));
155
+ }
156
+
157
+ return parts.length > 0 ? <>{parts}</> : line;
158
+ }
159
+
160
+ // ─── Main Component ──────────────────────────────────────
161
+
162
+ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef: React.RefObject<WebTerminalHandle | null>; onToggleCode?: () => void }) {
163
+ const [currentDir, setCurrentDir] = useState<string | null>(null);
164
+ const [dirName, setDirName] = useState('');
165
+ const [tree, setTree] = useState<FileNode[]>([]);
166
+ const [gitBranch, setGitBranch] = useState('');
167
+ const [gitChanges, setGitChanges] = useState<{ path: string; status: string }[]>([]);
168
+ const [showGit, setShowGit] = useState(false);
169
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
170
+ const [content, setContent] = useState<string | null>(null);
171
+ const [language, setLanguage] = useState('');
172
+ const [loading, setLoading] = useState(false);
173
+ const [search, setSearch] = useState('');
174
+ const [diffContent, setDiffContent] = useState<string | null>(null);
175
+ const [diffFile, setDiffFile] = useState<string | null>(null);
176
+ const [viewMode, setViewMode] = useState<'file' | 'diff'>('file');
177
+ const [sidebarOpen, setSidebarOpen] = useState(true);
178
+ const [codeOpen, setCodeOpen] = useState(false);
179
+ const [terminalHeight, setTerminalHeight] = useState(300);
180
+ const [activeSession, setActiveSession] = useState<string | null>(null);
181
+ const dragRef = useRef<{ startY: number; startH: number } | null>(null);
182
+ const lastDirRef = useRef<string | null>(null);
183
+
184
+ // When active terminal session changes, query its cwd
185
+ useEffect(() => {
186
+ if (!activeSession) return;
187
+ let cancelled = false;
188
+
189
+ const fetchCwd = async () => {
190
+ try {
191
+ const res = await fetch(`/api/terminal-cwd?session=${encodeURIComponent(activeSession)}`);
192
+ const data = await res.json();
193
+ if (!cancelled && data.path && data.path !== lastDirRef.current) {
194
+ lastDirRef.current = data.path;
195
+ setCurrentDir(data.path);
196
+ setSelectedFile(null);
197
+ setContent(null);
198
+ }
199
+ } catch {}
200
+ };
201
+
202
+ fetchCwd();
203
+ // Poll cwd every 5s (user might cd to a different directory)
204
+ const timer = setInterval(fetchCwd, 5000);
205
+ return () => { cancelled = true; clearInterval(timer); };
206
+ }, [activeSession]);
207
+
208
+ // Fetch file tree when directory changes
209
+ useEffect(() => {
210
+ if (!currentDir) return;
211
+ const fetchDir = () => {
212
+ fetch(`/api/code?dir=${encodeURIComponent(currentDir)}`)
213
+ .then(r => r.json())
214
+ .then(data => {
215
+ setTree(data.tree || []);
216
+ setDirName(data.dirName || currentDir.split('/').pop() || '');
217
+ setGitBranch(data.gitBranch || '');
218
+ setGitChanges(data.gitChanges || []);
219
+ })
220
+ .catch(() => setTree([]));
221
+ };
222
+ fetchDir();
223
+ }, [currentDir]);
224
+
225
+ // Build git status map for tree coloring
226
+ const gitMap: GitStatusMap = new Map(gitChanges.map(g => [g.path, g.status]));
227
+
228
+ const openFile = useCallback(async (path: string) => {
229
+ if (!currentDir) return;
230
+ setSelectedFile(path);
231
+ setViewMode('file');
232
+ setLoading(true);
233
+ const res = await fetch(`/api/code?dir=${encodeURIComponent(currentDir)}&file=${encodeURIComponent(path)}`);
234
+ const data = await res.json();
235
+ setContent(data.content || null);
236
+ setLanguage(data.language || '');
237
+ setLoading(false);
238
+ }, [currentDir]);
239
+
240
+ const openDiff = useCallback(async (path: string) => {
241
+ if (!currentDir) return;
242
+ setDiffFile(path);
243
+ setViewMode('diff');
244
+ setLoading(true);
245
+ const res = await fetch(`/api/code?dir=${encodeURIComponent(currentDir)}&diff=${encodeURIComponent(path)}`);
246
+ const data = await res.json();
247
+ setDiffContent(data.diff || null);
248
+ setLoading(false);
249
+ }, [currentDir]);
250
+
251
+ // Open file and auto-expand its parent dirs in tree
252
+ const locateFile = useCallback((path: string) => {
253
+ setSearch(''); // clear search so tree is visible
254
+ openFile(path);
255
+ }, [openFile]);
256
+
257
+ const allFiles = flattenTree(tree);
258
+ const filtered = search
259
+ ? allFiles.filter(f => f.name.toLowerCase().includes(search.toLowerCase()) || f.path.toLowerCase().includes(search.toLowerCase()))
260
+ : null;
261
+
262
+ const onDragStart = (e: React.MouseEvent) => {
263
+ e.preventDefault();
264
+ dragRef.current = { startY: e.clientY, startH: terminalHeight };
265
+ const onMove = (ev: MouseEvent) => {
266
+ if (!dragRef.current) return;
267
+ const delta = ev.clientY - dragRef.current.startY;
268
+ setTerminalHeight(Math.max(100, Math.min(600, dragRef.current.startH + delta)));
269
+ };
270
+ const onUp = () => {
271
+ dragRef.current = null;
272
+ window.removeEventListener('mousemove', onMove);
273
+ window.removeEventListener('mouseup', onUp);
274
+ };
275
+ window.addEventListener('mousemove', onMove);
276
+ window.addEventListener('mouseup', onUp);
277
+ };
278
+
279
+ const handleActiveSession = useCallback((session: string | null) => {
280
+ setActiveSession(session);
281
+ }, []);
282
+
283
+ return (
284
+ <div className="flex-1 flex flex-col min-h-0">
285
+ {/* Terminal — top */}
286
+ <div className={codeOpen ? 'shrink-0' : 'flex-1'} style={codeOpen ? { height: terminalHeight } : undefined}>
287
+ <Suspense fallback={<div className="h-full flex items-center justify-center text-[var(--text-secondary)] text-xs">Loading...</div>}>
288
+ <WebTerminal ref={terminalRef} onActiveSession={handleActiveSession} codeOpen={codeOpen} onToggleCode={() => setCodeOpen(v => !v)} />
289
+ </Suspense>
290
+ </div>
291
+
292
+ {/* Resize handle */}
293
+ {codeOpen && (
294
+ <div
295
+ onMouseDown={onDragStart}
296
+ className="h-1 bg-[var(--border)] cursor-row-resize hover:bg-[var(--accent)]/50 shrink-0"
297
+ />
298
+ )}
299
+
300
+ {/* File browser + code viewer — bottom */}
301
+ {codeOpen && <div className="flex-1 flex min-h-0">
302
+ {/* Sidebar */}
303
+ {sidebarOpen && (
304
+ <aside className="w-56 border-r border-[var(--border)] flex flex-col shrink-0">
305
+ {/* Directory name + git */}
306
+ <div className="px-3 py-2 border-b border-[var(--border)]">
307
+ <div className="flex items-center gap-2">
308
+ <span className="text-[11px] font-semibold text-[var(--text-primary)] truncate">
309
+ {dirName || 'No directory'}
310
+ </span>
311
+ {gitBranch && (
312
+ <span className="text-[9px] text-[var(--accent)] bg-[var(--accent)]/10 px-1.5 py-0.5 rounded shrink-0">
313
+ {gitBranch}
314
+ </span>
315
+ )}
316
+ </div>
317
+ {gitChanges.length > 0 && (
318
+ <button
319
+ onClick={() => setShowGit(v => !v)}
320
+ className="text-[10px] text-yellow-500 hover:text-yellow-400 mt-1 block"
321
+ >
322
+ {gitChanges.length} changes {showGit ? '▾' : '▸'}
323
+ </button>
324
+ )}
325
+ </div>
326
+
327
+ {/* Git changes */}
328
+ {showGit && gitChanges.length > 0 && (
329
+ <div className="border-b border-[var(--border)] max-h-48 overflow-y-auto">
330
+ {gitChanges.map(g => (
331
+ <div
332
+ key={g.path}
333
+ className={`flex items-center px-2 py-1 text-xs hover:bg-[var(--bg-tertiary)] ${
334
+ diffFile === g.path && viewMode === 'diff' ? 'bg-[var(--accent)]/10' : ''
335
+ }`}
336
+ >
337
+ <span className={`text-[10px] font-mono w-4 shrink-0 ${
338
+ g.status.includes('M') ? 'text-yellow-500' :
339
+ g.status.includes('A') || g.status.includes('?') ? 'text-green-500' :
340
+ g.status.includes('D') ? 'text-red-500' :
341
+ 'text-[var(--text-secondary)]'
342
+ }`}>
343
+ {g.status.includes('?') ? '+' : g.status[0]}
344
+ </span>
345
+ <button
346
+ onClick={() => openDiff(g.path)}
347
+ className="flex-1 text-left truncate text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-1"
348
+ title="View diff"
349
+ >
350
+ {g.path}
351
+ </button>
352
+ <button
353
+ onClick={(e) => { e.stopPropagation(); locateFile(g.path); }}
354
+ className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 px-1.5 py-0.5 rounded shrink-0"
355
+ title="Locate in file tree"
356
+ >
357
+ file
358
+ </button>
359
+ </div>
360
+ ))}
361
+ </div>
362
+ )}
363
+
364
+ {/* Search */}
365
+ <div className="p-2 border-b border-[var(--border)]">
366
+ <input
367
+ type="text"
368
+ placeholder="Search..."
369
+ value={search}
370
+ onChange={e => setSearch(e.target.value)}
371
+ className="w-full text-xs bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)]"
372
+ />
373
+ </div>
374
+
375
+ {/* Tree */}
376
+ <div className="flex-1 overflow-y-auto p-1">
377
+ {!currentDir ? (
378
+ <div className="text-xs text-[var(--text-secondary)] p-2">Open a terminal to see files</div>
379
+ ) : filtered ? (
380
+ filtered.length === 0 ? (
381
+ <div className="text-xs text-[var(--text-secondary)] p-2">No matches</div>
382
+ ) : (
383
+ filtered.map(f => (
384
+ <button
385
+ key={f.path}
386
+ onClick={() => { openFile(f.path); setSearch(''); }}
387
+ className={`w-full text-left px-2 py-1 rounded text-xs truncate ${
388
+ selectedFile === f.path ? 'bg-[var(--accent)]/20 text-[var(--accent)]' : 'hover:bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'
389
+ }`}
390
+ title={f.path}
391
+ >
392
+ <span className="text-[var(--text-primary)]">{f.name}</span>
393
+ <span className="text-[9px] text-[var(--text-secondary)] ml-1">{f.path.split('/').slice(0, -1).join('/')}</span>
394
+ </button>
395
+ ))
396
+ )
397
+ ) : (
398
+ tree.map(node => (
399
+ <TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} gitMap={gitMap} />
400
+ ))
401
+ )}
402
+ </div>
403
+ </aside>
404
+ )}
405
+
406
+ {/* Code viewer */}
407
+ <main className="flex-1 flex flex-col min-w-0">
408
+ <div className="px-3 py-1.5 border-b border-[var(--border)] shrink-0 flex items-center gap-2">
409
+ <button
410
+ onClick={() => setSidebarOpen(v => !v)}
411
+ className="text-[10px] px-1.5 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--bg-tertiary)] rounded"
412
+ >
413
+ {sidebarOpen ? '◀' : '▶'}
414
+ </button>
415
+ {viewMode === 'diff' && diffFile ? (
416
+ <>
417
+ <span className="text-xs font-semibold text-yellow-400 truncate">{diffFile}</span>
418
+ <span className="text-[9px] text-[var(--text-secondary)] ml-auto">diff</span>
419
+ </>
420
+ ) : selectedFile ? (
421
+ <>
422
+ <span className="text-xs font-semibold text-[var(--text-primary)] truncate">{selectedFile}</span>
423
+ {language && (
424
+ <span className="text-[9px] text-[var(--text-secondary)] ml-auto">{LANG_MAP[language] || language}</span>
425
+ )}
426
+ </>
427
+ ) : (
428
+ <span className="text-xs text-[var(--text-secondary)]">{dirName || 'Code'}</span>
429
+ )}
430
+ </div>
431
+
432
+ {loading ? (
433
+ <div className="flex-1 flex items-center justify-center">
434
+ <div className="text-xs text-[var(--text-secondary)]">Loading...</div>
435
+ </div>
436
+ ) : viewMode === 'diff' && diffContent ? (
437
+ <div className="flex-1 overflow-auto bg-[var(--bg-primary)]">
438
+ <pre className="p-4 text-[12px] leading-[1.5] font-mono whitespace-pre" style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace', tabSize: 2 }}>
439
+ {diffContent.split('\n').map((line, i) => {
440
+ const color = line.startsWith('+') ? 'text-green-400 bg-green-900/20'
441
+ : line.startsWith('-') ? 'text-red-400 bg-red-900/20'
442
+ : line.startsWith('@@') ? 'text-cyan-400'
443
+ : line.startsWith('diff') || line.startsWith('index') ? 'text-[var(--text-secondary)]'
444
+ : 'text-[var(--text-primary)]';
445
+ return (
446
+ <div key={i} className={`${color} px-2`}>
447
+ {line || ' '}
448
+ </div>
449
+ );
450
+ })}
451
+ </pre>
452
+ </div>
453
+ ) : selectedFile && content !== null ? (
454
+ <div className="flex-1 overflow-auto bg-[var(--bg-primary)]">
455
+ <pre className="p-4 text-[12px] leading-[1.5] font-mono text-[var(--text-primary)] whitespace-pre" style={{ fontFamily: 'Menlo, Monaco, "Courier New", monospace', tabSize: 2 }}>
456
+ {content.split('\n').map((line, i) => (
457
+ <div key={i} className="flex hover:bg-[var(--bg-tertiary)]/50">
458
+ <span className="select-none text-[var(--text-secondary)]/40 text-right pr-4 w-10 shrink-0">{i + 1}</span>
459
+ <span className="flex-1">{highlightLine(line, language)}</span>
460
+ </div>
461
+ ))}
462
+ </pre>
463
+ </div>
464
+ ) : (
465
+ <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
466
+ <p className="text-xs">{currentDir ? 'Select a file to view' : 'Terminal will show files for its working directory'}</p>
467
+ </div>
468
+ )}
469
+ </main>
470
+ </div>}
471
+
472
+ </div>
473
+ );
474
+ }
@@ -11,6 +11,8 @@ import type { Task } from '@/src/types';
11
11
  import type { WebTerminalHandle } from './WebTerminal';
12
12
 
13
13
  const WebTerminal = lazy(() => import('./WebTerminal'));
14
+ const DocsViewer = lazy(() => import('./DocsViewer'));
15
+ const CodeViewer = lazy(() => import('./CodeViewer'));
14
16
 
15
17
  interface UsageSummary {
16
18
  provider: string;
@@ -33,11 +35,12 @@ interface ProjectInfo {
33
35
  }
34
36
 
35
37
  export default function Dashboard({ user }: { user: any }) {
36
- const [viewMode, setViewMode] = useState<'tasks' | 'sessions' | 'terminal'>('tasks');
38
+ const [viewMode, setViewMode] = useState<'tasks' | 'sessions' | 'terminal' | 'docs'>('terminal');
37
39
  const [tasks, setTasks] = useState<Task[]>([]);
38
40
  const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
39
41
  const [showNewTask, setShowNewTask] = useState(false);
40
42
  const [showSettings, setShowSettings] = useState(false);
43
+ const [showCode, setShowCode] = useState(true);
41
44
  const [usage, setUsage] = useState<UsageSummary[]>([]);
42
45
  const [providers, setProviders] = useState<ProviderInfo[]>([]);
43
46
  const [projects, setProjects] = useState<ProjectInfo[]>([]);
@@ -78,34 +81,44 @@ export default function Dashboard({ user }: { user: any }) {
78
81
  {/* View mode toggle */}
79
82
  <div className="flex bg-[var(--bg-tertiary)] rounded p-0.5">
80
83
  <button
81
- onClick={() => setViewMode('tasks')}
84
+ onClick={() => setViewMode('terminal')}
82
85
  className={`text-[11px] px-2.5 py-0.5 rounded transition-colors ${
83
- viewMode === 'tasks'
86
+ viewMode === 'terminal'
84
87
  ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm'
85
88
  : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
86
89
  }`}
87
90
  >
88
- Tasks
91
+ Vibe Coding
89
92
  </button>
90
93
  <button
91
- onClick={() => setViewMode('sessions')}
94
+ onClick={() => setViewMode('docs')}
92
95
  className={`text-[11px] px-2.5 py-0.5 rounded transition-colors ${
93
- viewMode === 'sessions'
96
+ viewMode === 'docs'
94
97
  ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm'
95
98
  : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
96
99
  }`}
97
100
  >
98
- Sessions
101
+ Docs
99
102
  </button>
100
103
  <button
101
- onClick={() => setViewMode('terminal')}
104
+ onClick={() => setViewMode('tasks')}
102
105
  className={`text-[11px] px-2.5 py-0.5 rounded transition-colors ${
103
- viewMode === 'terminal'
106
+ viewMode === 'tasks'
107
+ ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm'
108
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
109
+ }`}
110
+ >
111
+ Tasks
112
+ </button>
113
+ <button
114
+ onClick={() => setViewMode('sessions')}
115
+ className={`text-[11px] px-2.5 py-0.5 rounded transition-colors ${
116
+ viewMode === 'sessions'
104
117
  ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm'
105
118
  : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
106
119
  }`}
107
120
  >
108
- Terminal
121
+ Sessions
109
122
  </button>
110
123
  </div>
111
124
 
@@ -239,10 +252,17 @@ export default function Dashboard({ user }: { user: any }) {
239
252
  />
240
253
  ) : null}
241
254
 
242
- {/* Terminal — always mounted, hidden when not active to keep sessions alive */}
243
- <div className={`flex-1 min-h-0 ${viewMode === 'terminal' ? '' : 'hidden'}`}>
244
- <Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading terminal...</div>}>
245
- <WebTerminal ref={terminalRef} />
255
+ {/* Docs — always mounted to keep terminal session alive */}
256
+ <div className={`flex-1 min-h-0 flex ${viewMode === 'docs' ? '' : 'hidden'}`}>
257
+ <Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
258
+ <DocsViewer />
259
+ </Suspense>
260
+ </div>
261
+
262
+ {/* Code — terminal + file browser, always mounted to keep terminal sessions alive */}
263
+ <div className={`flex-1 min-h-0 flex ${viewMode === 'terminal' ? '' : 'hidden'}`}>
264
+ <Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
265
+ <CodeViewer terminalRef={terminalRef} onToggleCode={() => setShowCode(v => !v)} />
246
266
  </Suspense>
247
267
  </div>
248
268
  </div>