@aion0/forge 0.2.26 → 0.2.28
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/app/globals.css +27 -1
- package/app/login/page.tsx +3 -0
- package/components/Dashboard.tsx +62 -27
- package/components/DocTerminal.tsx +6 -4
- package/components/WebTerminal.tsx +89 -32
- package/package.json +1 -1
package/app/globals.css
CHANGED
|
@@ -11,6 +11,33 @@
|
|
|
11
11
|
--green: #22c55e;
|
|
12
12
|
--yellow: #eab308;
|
|
13
13
|
--red: #ef4444;
|
|
14
|
+
/* Terminal */
|
|
15
|
+
--term-bg: #1a1a2e;
|
|
16
|
+
--term-bar: #12122a;
|
|
17
|
+
--term-border: #2a2a4a;
|
|
18
|
+
--term-fg: #e0e0e0;
|
|
19
|
+
--term-cursor: #7c5bf0;
|
|
20
|
+
--term-dim: #555;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[data-theme="light"] {
|
|
24
|
+
--bg-primary: #ffffff;
|
|
25
|
+
--bg-secondary: #f5f5f5;
|
|
26
|
+
--bg-tertiary: #e8e8e8;
|
|
27
|
+
--border: #d4d4d4;
|
|
28
|
+
--text-primary: #1a1a1a;
|
|
29
|
+
--text-secondary: #666;
|
|
30
|
+
--accent: #2563eb;
|
|
31
|
+
--green: #16a34a;
|
|
32
|
+
--yellow: #ca8a04;
|
|
33
|
+
--red: #dc2626;
|
|
34
|
+
/* Terminal */
|
|
35
|
+
--term-bg: #fafafa;
|
|
36
|
+
--term-bar: #f0f0f0;
|
|
37
|
+
--term-border: #d0d0d0;
|
|
38
|
+
--term-fg: #1a1a1a;
|
|
39
|
+
--term-cursor: #2563eb;
|
|
40
|
+
--term-dim: #999;
|
|
14
41
|
}
|
|
15
42
|
|
|
16
43
|
body {
|
|
@@ -23,4 +50,3 @@ body {
|
|
|
23
50
|
::-webkit-scrollbar { width: 6px; }
|
|
24
51
|
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
|
25
52
|
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
26
|
-
|
package/app/login/page.tsx
CHANGED
|
@@ -12,6 +12,9 @@ export default function LoginPage() {
|
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
const host = window.location.hostname;
|
|
14
14
|
setIsRemote(!['localhost', '127.0.0.1'].includes(host));
|
|
15
|
+
// Restore theme
|
|
16
|
+
const saved = localStorage.getItem('forge-theme');
|
|
17
|
+
if (saved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
15
18
|
}, []);
|
|
16
19
|
|
|
17
20
|
const handleLocal = async (e: React.FormEvent) => {
|
package/components/Dashboard.tsx
CHANGED
|
@@ -54,8 +54,26 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
54
54
|
const [notifications, setNotifications] = useState<any[]>([]);
|
|
55
55
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
56
56
|
const [showNotifications, setShowNotifications] = useState(false);
|
|
57
|
+
const [showUserMenu, setShowUserMenu] = useState(false);
|
|
58
|
+
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
|
57
59
|
const terminalRef = useRef<WebTerminalHandle>(null);
|
|
58
60
|
|
|
61
|
+
// Theme: load from localStorage + apply
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const saved = localStorage.getItem('forge-theme') as 'dark' | 'light' | null;
|
|
64
|
+
if (saved) {
|
|
65
|
+
setTheme(saved);
|
|
66
|
+
document.documentElement.setAttribute('data-theme', saved === 'light' ? 'light' : '');
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const toggleTheme = () => {
|
|
71
|
+
const next = theme === 'dark' ? 'light' : 'dark';
|
|
72
|
+
setTheme(next);
|
|
73
|
+
document.documentElement.setAttribute('data-theme', next === 'light' ? 'light' : '');
|
|
74
|
+
localStorage.setItem('forge-theme', next);
|
|
75
|
+
};
|
|
76
|
+
|
|
59
77
|
// Version check (on mount + every 10 min)
|
|
60
78
|
useEffect(() => {
|
|
61
79
|
const check = () => fetch('/api/version').then(r => r.json()).then(setVersionInfo).catch(() => {});
|
|
@@ -228,11 +246,11 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
228
246
|
</span>
|
|
229
247
|
)}
|
|
230
248
|
</div>
|
|
231
|
-
<div className="flex items-center gap-
|
|
249
|
+
<div className="flex items-center gap-2.5">
|
|
232
250
|
{viewMode === 'tasks' && (
|
|
233
251
|
<button
|
|
234
252
|
onClick={() => setShowNewTask(true)}
|
|
235
|
-
className="text-
|
|
253
|
+
className="text-[10px] px-2.5 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
|
|
236
254
|
>
|
|
237
255
|
+ New Task
|
|
238
256
|
</button>
|
|
@@ -242,24 +260,24 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
242
260
|
<span className="text-[10px] text-[var(--text-secondary)] flex items-center gap-1" title={`${onlineCount.total} online${onlineCount.remote > 0 ? `, ${onlineCount.remote} remote` : ''}`}>
|
|
243
261
|
<span className="text-green-500">●</span>
|
|
244
262
|
{onlineCount.total}
|
|
245
|
-
{onlineCount.remote > 0 && (
|
|
246
|
-
<span className="text-[var(--accent)]">({onlineCount.remote} remote)</span>
|
|
247
|
-
)}
|
|
248
263
|
</span>
|
|
249
264
|
)}
|
|
265
|
+
{/* Alerts */}
|
|
250
266
|
<div className="relative">
|
|
251
267
|
<button
|
|
252
|
-
onClick={() => { setShowNotifications(v => !v); }}
|
|
253
|
-
className="text-
|
|
268
|
+
onClick={() => { setShowNotifications(v => !v); setShowUserMenu(false); }}
|
|
269
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] relative px-1"
|
|
254
270
|
>
|
|
255
271
|
Alerts
|
|
256
272
|
{unreadCount > 0 && (
|
|
257
|
-
<span className="absolute -top-1.5 -right-
|
|
273
|
+
<span className="absolute -top-1.5 -right-1.5 min-w-[14px] h-[14px] rounded-full bg-[var(--red)] text-[8px] text-white flex items-center justify-center px-1 font-bold">
|
|
258
274
|
{unreadCount > 99 ? '99+' : unreadCount}
|
|
259
275
|
</span>
|
|
260
276
|
)}
|
|
261
277
|
</button>
|
|
262
278
|
{showNotifications && (
|
|
279
|
+
<>
|
|
280
|
+
<div className="fixed inset-0 z-40" onClick={() => setShowNotifications(false)} />
|
|
263
281
|
<div className="absolute right-0 top-8 w-[360px] max-h-[480px] bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 flex flex-col">
|
|
264
282
|
<div className="flex items-center justify-between px-3 py-2 border-b border-[var(--border)]">
|
|
265
283
|
<span className="text-xs font-bold text-[var(--text-primary)]">Notifications</span>
|
|
@@ -352,27 +370,44 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
352
370
|
)}
|
|
353
371
|
</div>
|
|
354
372
|
</div>
|
|
373
|
+
</>
|
|
374
|
+
)}
|
|
375
|
+
</div>
|
|
376
|
+
{/* User menu */}
|
|
377
|
+
<div className="relative">
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => { setShowUserMenu(v => !v); setShowNotifications(false); }}
|
|
380
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] flex items-center gap-1 px-1"
|
|
381
|
+
>
|
|
382
|
+
{user?.name || 'local'} <span className="text-[8px]">▾</span>
|
|
383
|
+
</button>
|
|
384
|
+
{showUserMenu && (
|
|
385
|
+
<>
|
|
386
|
+
<div className="fixed inset-0 z-40" onClick={() => setShowUserMenu(false)} />
|
|
387
|
+
<div className="absolute right-0 top-8 w-[140px] bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg shadow-xl z-50 py-1">
|
|
388
|
+
<button
|
|
389
|
+
onClick={() => { setShowMonitor(true); setShowUserMenu(false); }}
|
|
390
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
391
|
+
>
|
|
392
|
+
Monitor
|
|
393
|
+
</button>
|
|
394
|
+
<button
|
|
395
|
+
onClick={() => { setShowSettings(true); setShowUserMenu(false); }}
|
|
396
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)]"
|
|
397
|
+
>
|
|
398
|
+
Settings
|
|
399
|
+
</button>
|
|
400
|
+
<div className="border-t border-[var(--border)] my-1" />
|
|
401
|
+
<button
|
|
402
|
+
onClick={() => signOut({ callbackUrl: '/login' })}
|
|
403
|
+
className="w-full text-left text-[11px] px-3 py-1.5 text-[var(--text-secondary)] hover:text-[var(--red)] hover:bg-[var(--bg-tertiary)]"
|
|
404
|
+
>
|
|
405
|
+
Logout
|
|
406
|
+
</button>
|
|
407
|
+
</div>
|
|
408
|
+
</>
|
|
355
409
|
)}
|
|
356
410
|
</div>
|
|
357
|
-
<button
|
|
358
|
-
onClick={() => setShowMonitor(true)}
|
|
359
|
-
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
360
|
-
>
|
|
361
|
-
Monitor
|
|
362
|
-
</button>
|
|
363
|
-
<button
|
|
364
|
-
onClick={() => setShowSettings(true)}
|
|
365
|
-
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
366
|
-
>
|
|
367
|
-
Settings
|
|
368
|
-
</button>
|
|
369
|
-
<span className="text-xs text-[var(--text-secondary)]">{user?.name || 'local'}</span>
|
|
370
|
-
<button
|
|
371
|
-
onClick={() => signOut({ callbackUrl: '/login' })}
|
|
372
|
-
className="text-xs text-[var(--text-secondary)] hover:text-[var(--red)]"
|
|
373
|
-
>
|
|
374
|
-
Logout
|
|
375
|
-
</button>
|
|
376
411
|
</div>
|
|
377
412
|
</header>
|
|
378
413
|
|
|
@@ -35,6 +35,8 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
|
|
|
35
35
|
if (!containerRef.current) return;
|
|
36
36
|
|
|
37
37
|
let disposed = false;
|
|
38
|
+
const cs = getComputedStyle(document.documentElement);
|
|
39
|
+
const tv = (name: string) => cs.getPropertyValue(name).trim();
|
|
38
40
|
const term = new Terminal({
|
|
39
41
|
cursorBlink: true,
|
|
40
42
|
fontSize: 13,
|
|
@@ -42,10 +44,10 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
|
|
|
42
44
|
scrollback: 5000,
|
|
43
45
|
logger: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
|
44
46
|
theme: {
|
|
45
|
-
background: '#1a1a2e',
|
|
46
|
-
foreground: '#e0e0e0',
|
|
47
|
-
cursor: '#7c5bf0',
|
|
48
|
-
selectionBackground: '#
|
|
47
|
+
background: tv('--term-bg') || '#1a1a2e',
|
|
48
|
+
foreground: tv('--term-fg') || '#e0e0e0',
|
|
49
|
+
cursor: tv('--term-cursor') || '#7c5bf0',
|
|
50
|
+
selectionBackground: (tv('--term-cursor') || '#7c5bf0') + '44',
|
|
49
51
|
},
|
|
50
52
|
});
|
|
51
53
|
const fit = new FitAddon();
|
|
@@ -434,9 +434,9 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
434
434
|
const detachedCount = tmuxSessions.filter(s => !usedSessions.includes(s.name)).length;
|
|
435
435
|
|
|
436
436
|
return (
|
|
437
|
-
<div className="h-full w-full flex-1 flex flex-col bg-[
|
|
437
|
+
<div className="h-full w-full flex-1 flex flex-col bg-[var(--term-bg)] overflow-hidden">
|
|
438
438
|
{/* Tab bar + toolbar */}
|
|
439
|
-
<div className="flex items-center bg-[
|
|
439
|
+
<div className="flex items-center bg-[var(--term-bar)] border-b border-[var(--term-border)] shrink-0">
|
|
440
440
|
{/* Tabs */}
|
|
441
441
|
<div className="flex items-center overflow-x-auto">
|
|
442
442
|
{tabs.map(tab => (
|
|
@@ -463,10 +463,10 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
463
463
|
}
|
|
464
464
|
}}
|
|
465
465
|
onDragEnd={() => { dragTabRef.current = null; }}
|
|
466
|
-
className={`flex items-center gap-1 px-3 py-1 text-[11px] cursor-pointer border-r border-[
|
|
466
|
+
className={`flex items-center gap-1 px-3 py-1 text-[11px] cursor-pointer border-r border-[var(--term-border)] select-none ${
|
|
467
467
|
tab.id === activeTabId
|
|
468
|
-
? 'bg-[
|
|
469
|
-
: 'text-gray-500 hover:text-gray-300 hover:bg-[
|
|
468
|
+
? 'bg-[var(--term-bg)] text-white'
|
|
469
|
+
: 'text-gray-500 hover:text-gray-300 hover:bg-[var(--term-bg)]/50'
|
|
470
470
|
}`}
|
|
471
471
|
onClick={() => setActiveTabId(tab.id)}
|
|
472
472
|
>
|
|
@@ -481,7 +481,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
481
481
|
if (e.key === 'Escape') setEditingTabId(null);
|
|
482
482
|
}}
|
|
483
483
|
onClick={(e) => e.stopPropagation()}
|
|
484
|
-
className="bg-transparent border border-[
|
|
484
|
+
className="bg-transparent border border-[var(--term-border)] rounded px-1 text-[11px] text-white outline-none w-20"
|
|
485
485
|
/>
|
|
486
486
|
) : (
|
|
487
487
|
<span
|
|
@@ -506,8 +506,18 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
506
506
|
</div>
|
|
507
507
|
))}
|
|
508
508
|
<button
|
|
509
|
-
onClick={() =>
|
|
510
|
-
|
|
509
|
+
onClick={() => {
|
|
510
|
+
setShowNewTabModal(true);
|
|
511
|
+
// Refresh projects list when opening modal
|
|
512
|
+
fetch('/api/projects').then(r => r.json())
|
|
513
|
+
.then((p: { name: string; path: string; root: string }[]) => {
|
|
514
|
+
if (!Array.isArray(p)) return;
|
|
515
|
+
setAllProjects(p);
|
|
516
|
+
setProjectRoots([...new Set(p.map(proj => proj.root))]);
|
|
517
|
+
})
|
|
518
|
+
.catch(() => {});
|
|
519
|
+
}}
|
|
520
|
+
className="px-2 py-1 text-[11px] text-gray-500 hover:text-white hover:bg-[var(--term-border)]"
|
|
511
521
|
title="New tab"
|
|
512
522
|
>
|
|
513
523
|
+
|
|
@@ -517,15 +527,15 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
517
527
|
{/* Toolbar */}
|
|
518
528
|
<div className="flex items-center gap-1 px-2 ml-auto">
|
|
519
529
|
<span className="text-[9px] text-gray-600 mr-2">Shift+drag to copy</span>
|
|
520
|
-
<button onClick={() => onSplit('vertical')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[
|
|
530
|
+
<button onClick={() => onSplit('vertical')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded">
|
|
521
531
|
Split Right
|
|
522
532
|
</button>
|
|
523
|
-
<button onClick={() => onSplit('horizontal')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[
|
|
533
|
+
<button onClick={() => onSplit('horizontal')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[var(--term-border)] rounded">
|
|
524
534
|
Split Down
|
|
525
535
|
</button>
|
|
526
536
|
<button
|
|
527
537
|
onClick={() => { refreshSessions(); setShowSessionPicker(v => !v); }}
|
|
528
|
-
className={`text-[10px] px-2 py-0.5 rounded relative ${showSessionPicker ? 'text-white bg-[#7c5bf0]/30' : 'text-gray-400 hover:text-white hover:bg-[
|
|
538
|
+
className={`text-[10px] px-2 py-0.5 rounded relative ${showSessionPicker ? 'text-white bg-[#7c5bf0]/30' : 'text-gray-400 hover:text-white hover:bg-[var(--term-border)]'}`}
|
|
529
539
|
>
|
|
530
540
|
Sessions
|
|
531
541
|
{detachedCount > 0 && (
|
|
@@ -559,7 +569,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
559
569
|
</button>
|
|
560
570
|
)}
|
|
561
571
|
{activeTab && countTerminals(activeTab.tree) > 1 && (
|
|
562
|
-
<button onClick={onClosePane} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-red-400 hover:bg-[
|
|
572
|
+
<button onClick={onClosePane} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-red-400 hover:bg-[var(--term-border)] rounded">
|
|
563
573
|
Close Pane
|
|
564
574
|
</button>
|
|
565
575
|
)}
|
|
@@ -568,7 +578,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
568
578
|
|
|
569
579
|
{/* Session management panel */}
|
|
570
580
|
{showSessionPicker && (
|
|
571
|
-
<div className="bg-[
|
|
581
|
+
<div className="bg-[var(--term-bar)] border-b border-[var(--term-border)] px-3 py-2 shrink-0 max-h-48 overflow-y-auto">
|
|
572
582
|
<div className="flex items-center justify-between mb-2">
|
|
573
583
|
<span className="text-[10px] text-gray-400 font-semibold uppercase">Tmux Sessions</span>
|
|
574
584
|
<button
|
|
@@ -583,7 +593,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
583
593
|
) : (
|
|
584
594
|
<table className="w-full text-[10px]">
|
|
585
595
|
<thead>
|
|
586
|
-
<tr className="text-gray-500 text-left border-b border-[
|
|
596
|
+
<tr className="text-gray-500 text-left border-b border-[var(--term-border)]">
|
|
587
597
|
<th className="py-1 pr-3 font-medium">Session</th>
|
|
588
598
|
<th className="py-1 pr-3 font-medium">Created</th>
|
|
589
599
|
<th className="py-1 pr-3 font-medium">Status</th>
|
|
@@ -595,7 +605,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
595
605
|
const inUse = usedSessions.includes(s.name);
|
|
596
606
|
const savedLabel = sessionLabelsRef.current[s.name];
|
|
597
607
|
return (
|
|
598
|
-
<tr key={s.name} className="border-b border-[
|
|
608
|
+
<tr key={s.name} className="border-b border-[var(--term-border)]/50 hover:bg-[var(--term-bg)]">
|
|
599
609
|
<td className="py-1.5 pr-3 text-gray-300">
|
|
600
610
|
{savedLabel ? (
|
|
601
611
|
<><span>{savedLabel}</span> <span className="font-mono text-gray-600 text-[9px]">{s.name.replace('mw-', '')}</span></>
|
|
@@ -654,22 +664,22 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
654
664
|
{/* New tab modal */}
|
|
655
665
|
{showNewTabModal && (
|
|
656
666
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => { setShowNewTabModal(false); setExpandedRoot(null); }}>
|
|
657
|
-
<div className="bg-[
|
|
658
|
-
<div className="px-4 py-3 border-b border-[
|
|
667
|
+
<div className="bg-[var(--term-bg)] border border-[var(--term-border)] rounded-lg shadow-xl w-[350px] max-h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
668
|
+
<div className="px-4 py-3 border-b border-[var(--term-border)]">
|
|
659
669
|
<h3 className="text-sm font-semibold text-white">New Tab</h3>
|
|
660
670
|
</div>
|
|
661
671
|
<div className="flex-1 overflow-y-auto p-2">
|
|
662
672
|
{/* Plain terminal */}
|
|
663
673
|
<button
|
|
664
674
|
onClick={() => { addTab(); setShowNewTabModal(false); setExpandedRoot(null); }}
|
|
665
|
-
className="w-full text-left px-3 py-2 rounded hover:bg-[
|
|
675
|
+
className="w-full text-left px-3 py-2 rounded hover:bg-[var(--term-border)] text-[12px] text-gray-300 flex items-center gap-2"
|
|
666
676
|
>
|
|
667
677
|
<span className="text-gray-500">▸</span> Terminal
|
|
668
678
|
</button>
|
|
669
679
|
|
|
670
680
|
{/* Project roots */}
|
|
671
681
|
{projectRoots.length > 0 && (
|
|
672
|
-
<div className="mt-2 pt-2 border-t border-[
|
|
682
|
+
<div className="mt-2 pt-2 border-t border-[var(--term-border)]">
|
|
673
683
|
<div className="px-3 py-1 text-[9px] text-gray-500 uppercase">Claude in Project</div>
|
|
674
684
|
{projectRoots.map(root => {
|
|
675
685
|
const rootName = root.split('/').pop() || root;
|
|
@@ -679,7 +689,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
679
689
|
<div key={root}>
|
|
680
690
|
<button
|
|
681
691
|
onClick={() => setExpandedRoot(isExpanded ? null : root)}
|
|
682
|
-
className="w-full text-left px-3 py-2 rounded hover:bg-[
|
|
692
|
+
className="w-full text-left px-3 py-2 rounded hover:bg-[var(--term-border)] text-[12px] text-gray-300 flex items-center gap-2"
|
|
683
693
|
>
|
|
684
694
|
<span className="text-gray-500 text-[10px] w-3">{isExpanded ? '▾' : '▸'}</span>
|
|
685
695
|
<span>{rootName}</span>
|
|
@@ -690,8 +700,26 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
690
700
|
{rootProjects.map(p => (
|
|
691
701
|
<button
|
|
692
702
|
key={p.path}
|
|
693
|
-
onClick={() => {
|
|
694
|
-
|
|
703
|
+
onClick={async () => {
|
|
704
|
+
setShowNewTabModal(false); setExpandedRoot(null);
|
|
705
|
+
// Pre-check sessions before creating tab
|
|
706
|
+
let hasSession = false;
|
|
707
|
+
try {
|
|
708
|
+
const sRes = await fetch(`/api/claude-sessions/${encodeURIComponent(p.name)}`);
|
|
709
|
+
const sData = await sRes.json();
|
|
710
|
+
hasSession = Array.isArray(sData) ? sData.length > 0 : (Array.isArray(sData.sessions) && sData.sessions.length > 0);
|
|
711
|
+
} catch {}
|
|
712
|
+
const skipFlag = skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
713
|
+
const resumeFlag = hasSession ? ' --resume' : '';
|
|
714
|
+
const tree = makeTerminal(undefined, p.path);
|
|
715
|
+
const paneId = firstTerminalId(tree);
|
|
716
|
+
pendingCommands.set(paneId, `cd "${p.path}" && claude${resumeFlag}${skipFlag}\n`);
|
|
717
|
+
const tabNum = tabs.length + 1;
|
|
718
|
+
const newTab: TabState = { id: nextId++, label: p.name || `Terminal ${tabNum}`, tree, ratios: {}, activeId: paneId, projectPath: p.path };
|
|
719
|
+
setTabs(prev => [...prev, newTab]);
|
|
720
|
+
setActiveTabId(newTab.id);
|
|
721
|
+
}}
|
|
722
|
+
className="w-full text-left px-3 py-1.5 rounded hover:bg-[var(--term-border)] text-[11px] text-gray-300 flex items-center gap-2 truncate"
|
|
695
723
|
title={p.path}
|
|
696
724
|
>
|
|
697
725
|
<span className="text-gray-600 text-[10px]">↳</span> {p.name}
|
|
@@ -708,7 +736,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
708
736
|
</div>
|
|
709
737
|
)}
|
|
710
738
|
</div>
|
|
711
|
-
<div className="px-4 py-2 border-t border-[
|
|
739
|
+
<div className="px-4 py-2 border-t border-[var(--term-border)]">
|
|
712
740
|
<button
|
|
713
741
|
onClick={() => { setShowNewTabModal(false); setExpandedRoot(null); }}
|
|
714
742
|
className="w-full text-center text-[11px] text-gray-500 hover:text-gray-300 py-1"
|
|
@@ -723,7 +751,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
723
751
|
{/* Close confirmation dialog */}
|
|
724
752
|
{closeConfirm && (
|
|
725
753
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setCloseConfirm(null)}>
|
|
726
|
-
<div className="bg-[
|
|
754
|
+
<div className="bg-[var(--term-bg)] border border-[var(--term-border)] rounded-lg p-4 shadow-xl max-w-sm" onClick={(e) => e.stopPropagation()}>
|
|
727
755
|
<h3 className="text-sm font-semibold text-white mb-2">Close Tab</h3>
|
|
728
756
|
<p className="text-xs text-gray-400 mb-1">
|
|
729
757
|
This tab has {closeConfirm.sessions.length} active session{closeConfirm.sessions.length > 1 ? 's' : ''}:
|
|
@@ -950,17 +978,46 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
950
978
|
|
|
951
979
|
let disposed = false; // guard against post-cleanup writes (React Strict Mode)
|
|
952
980
|
|
|
981
|
+
// Read terminal theme from CSS variables
|
|
982
|
+
const cs = getComputedStyle(document.documentElement);
|
|
983
|
+
const tv = (name: string) => cs.getPropertyValue(name).trim();
|
|
984
|
+
const termBg = tv('--term-bg') || '#1a1a2e';
|
|
985
|
+
const termFg = tv('--term-fg') || '#e0e0e0';
|
|
986
|
+
const termCursor = tv('--term-cursor') || '#7c5bf0';
|
|
987
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
988
|
+
|
|
953
989
|
const term = new Terminal({
|
|
954
990
|
cursorBlink: true,
|
|
955
991
|
fontSize: 13,
|
|
956
992
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
957
993
|
scrollback: 10000,
|
|
958
994
|
logger: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
|
959
|
-
theme: {
|
|
960
|
-
background:
|
|
961
|
-
foreground:
|
|
962
|
-
cursor:
|
|
963
|
-
selectionBackground: '
|
|
995
|
+
theme: isLight ? {
|
|
996
|
+
background: termBg,
|
|
997
|
+
foreground: termFg,
|
|
998
|
+
cursor: termCursor,
|
|
999
|
+
selectionBackground: termCursor + '44',
|
|
1000
|
+
black: '#1a1a1a',
|
|
1001
|
+
red: '#d32f2f',
|
|
1002
|
+
green: '#388e3c',
|
|
1003
|
+
yellow: '#f57f17',
|
|
1004
|
+
blue: '#1976d2',
|
|
1005
|
+
magenta: '#7b1fa2',
|
|
1006
|
+
cyan: '#0097a7',
|
|
1007
|
+
white: '#424242',
|
|
1008
|
+
brightBlack: '#757575',
|
|
1009
|
+
brightRed: '#e53935',
|
|
1010
|
+
brightGreen: '#43a047',
|
|
1011
|
+
brightYellow: '#f9a825',
|
|
1012
|
+
brightBlue: '#1e88e5',
|
|
1013
|
+
brightMagenta: '#8e24aa',
|
|
1014
|
+
brightCyan: '#00acc1',
|
|
1015
|
+
brightWhite: '#1a1a1a',
|
|
1016
|
+
} : {
|
|
1017
|
+
background: termBg,
|
|
1018
|
+
foreground: termFg,
|
|
1019
|
+
cursor: termCursor,
|
|
1020
|
+
selectionBackground: termCursor + '66',
|
|
964
1021
|
black: '#1a1a2e',
|
|
965
1022
|
red: '#ff6b6b',
|
|
966
1023
|
green: '#69db7c',
|
|
@@ -1061,13 +1118,13 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1061
1118
|
createRetries = 0;
|
|
1062
1119
|
reconnectAttempts = 0;
|
|
1063
1120
|
onSessionConnected(id, msg.sessionName);
|
|
1064
|
-
// Auto-run claude
|
|
1065
|
-
if (isNewlyCreated && projectPathRef.current) {
|
|
1121
|
+
// Auto-run claude for project tabs (only if no pendingCommand already set)
|
|
1122
|
+
if (isNewlyCreated && projectPathRef.current && !pendingCommands.has(id)) {
|
|
1066
1123
|
isNewlyCreated = false;
|
|
1067
1124
|
setTimeout(() => {
|
|
1068
1125
|
if (!disposed && ws?.readyState === WebSocket.OPEN) {
|
|
1069
1126
|
const skipFlag = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
|
|
1070
|
-
ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude
|
|
1127
|
+
ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude${skipFlag}\n` }));
|
|
1071
1128
|
}
|
|
1072
1129
|
}, 300);
|
|
1073
1130
|
}
|
package/package.json
CHANGED