@aion0/forge 0.2.25 → 0.2.27
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/api/version/route.ts +1 -1
- package/app/globals.css +27 -1
- package/app/login/page.tsx +3 -0
- package/cli/mw.ts +2 -1
- package/components/Dashboard.tsx +62 -27
- package/components/DocTerminal.tsx +6 -4
- package/components/SessionView.tsx +2 -2
- package/components/WebTerminal.tsx +56 -27
- package/package.json +1 -1
package/app/api/version/route.ts
CHANGED
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/cli/mw.ts
CHANGED
|
@@ -553,7 +553,8 @@ Shortcuts: t=task, ls=tasks, w=watch, s=status, l=log, f=flows, p=projects, pw=p
|
|
|
553
553
|
}
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
|
|
556
|
+
const skipUpdateCheck = ['upgrade', 'uninstall', '--version', '-v'];
|
|
557
|
+
main().then(() => { if (!skipUpdateCheck.includes(cmd)) return checkForUpdate(); }).catch(err => {
|
|
557
558
|
console.error(err.message);
|
|
558
559
|
process.exit(1);
|
|
559
560
|
});
|
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();
|
|
@@ -452,7 +452,7 @@ export default function SessionView({
|
|
|
452
452
|
</div>
|
|
453
453
|
|
|
454
454
|
{/* Right: session content */}
|
|
455
|
-
<div className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
455
|
+
<div className="flex-1 flex flex-col min-w-0 overflow-hidden" style={{ width: 0 }}>
|
|
456
456
|
{activeSession && (
|
|
457
457
|
<div className="border-b border-[var(--border)] px-4 py-2 shrink-0">
|
|
458
458
|
<div className="flex items-center gap-2">
|
|
@@ -508,7 +508,7 @@ export default function SessionView({
|
|
|
508
508
|
</div>
|
|
509
509
|
)}
|
|
510
510
|
|
|
511
|
-
<div className="flex-1 overflow-y-auto overflow-x-
|
|
511
|
+
<div className="flex-1 overflow-y-auto overflow-x-auto p-4 space-y-2">
|
|
512
512
|
{!activeSessionId && (
|
|
513
513
|
<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)] h-full">
|
|
514
514
|
<p>Select a session from the tree to view</p>
|
|
@@ -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
|
|
@@ -507,7 +507,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
507
507
|
))}
|
|
508
508
|
<button
|
|
509
509
|
onClick={() => setShowNewTabModal(true)}
|
|
510
|
-
className="px-2 py-1 text-[11px] text-gray-500 hover:text-white hover:bg-[
|
|
510
|
+
className="px-2 py-1 text-[11px] text-gray-500 hover:text-white hover:bg-[var(--term-border)]"
|
|
511
511
|
title="New tab"
|
|
512
512
|
>
|
|
513
513
|
+
|
|
@@ -517,15 +517,15 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
517
517
|
{/* Toolbar */}
|
|
518
518
|
<div className="flex items-center gap-1 px-2 ml-auto">
|
|
519
519
|
<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-[
|
|
520
|
+
<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
521
|
Split Right
|
|
522
522
|
</button>
|
|
523
|
-
<button onClick={() => onSplit('horizontal')} className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[
|
|
523
|
+
<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
524
|
Split Down
|
|
525
525
|
</button>
|
|
526
526
|
<button
|
|
527
527
|
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-[
|
|
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-[var(--term-border)]'}`}
|
|
529
529
|
>
|
|
530
530
|
Sessions
|
|
531
531
|
{detachedCount > 0 && (
|
|
@@ -559,7 +559,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
559
559
|
</button>
|
|
560
560
|
)}
|
|
561
561
|
{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-[
|
|
562
|
+
<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
563
|
Close Pane
|
|
564
564
|
</button>
|
|
565
565
|
)}
|
|
@@ -568,7 +568,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
568
568
|
|
|
569
569
|
{/* Session management panel */}
|
|
570
570
|
{showSessionPicker && (
|
|
571
|
-
<div className="bg-[
|
|
571
|
+
<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
572
|
<div className="flex items-center justify-between mb-2">
|
|
573
573
|
<span className="text-[10px] text-gray-400 font-semibold uppercase">Tmux Sessions</span>
|
|
574
574
|
<button
|
|
@@ -583,7 +583,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
583
583
|
) : (
|
|
584
584
|
<table className="w-full text-[10px]">
|
|
585
585
|
<thead>
|
|
586
|
-
<tr className="text-gray-500 text-left border-b border-[
|
|
586
|
+
<tr className="text-gray-500 text-left border-b border-[var(--term-border)]">
|
|
587
587
|
<th className="py-1 pr-3 font-medium">Session</th>
|
|
588
588
|
<th className="py-1 pr-3 font-medium">Created</th>
|
|
589
589
|
<th className="py-1 pr-3 font-medium">Status</th>
|
|
@@ -595,7 +595,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
595
595
|
const inUse = usedSessions.includes(s.name);
|
|
596
596
|
const savedLabel = sessionLabelsRef.current[s.name];
|
|
597
597
|
return (
|
|
598
|
-
<tr key={s.name} className="border-b border-[
|
|
598
|
+
<tr key={s.name} className="border-b border-[var(--term-border)]/50 hover:bg-[var(--term-bg)]">
|
|
599
599
|
<td className="py-1.5 pr-3 text-gray-300">
|
|
600
600
|
{savedLabel ? (
|
|
601
601
|
<><span>{savedLabel}</span> <span className="font-mono text-gray-600 text-[9px]">{s.name.replace('mw-', '')}</span></>
|
|
@@ -654,22 +654,22 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
654
654
|
{/* New tab modal */}
|
|
655
655
|
{showNewTabModal && (
|
|
656
656
|
<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-[
|
|
657
|
+
<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()}>
|
|
658
|
+
<div className="px-4 py-3 border-b border-[var(--term-border)]">
|
|
659
659
|
<h3 className="text-sm font-semibold text-white">New Tab</h3>
|
|
660
660
|
</div>
|
|
661
661
|
<div className="flex-1 overflow-y-auto p-2">
|
|
662
662
|
{/* Plain terminal */}
|
|
663
663
|
<button
|
|
664
664
|
onClick={() => { addTab(); setShowNewTabModal(false); setExpandedRoot(null); }}
|
|
665
|
-
className="w-full text-left px-3 py-2 rounded hover:bg-[
|
|
665
|
+
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
666
|
>
|
|
667
667
|
<span className="text-gray-500">▸</span> Terminal
|
|
668
668
|
</button>
|
|
669
669
|
|
|
670
670
|
{/* Project roots */}
|
|
671
671
|
{projectRoots.length > 0 && (
|
|
672
|
-
<div className="mt-2 pt-2 border-t border-[
|
|
672
|
+
<div className="mt-2 pt-2 border-t border-[var(--term-border)]">
|
|
673
673
|
<div className="px-3 py-1 text-[9px] text-gray-500 uppercase">Claude in Project</div>
|
|
674
674
|
{projectRoots.map(root => {
|
|
675
675
|
const rootName = root.split('/').pop() || root;
|
|
@@ -679,7 +679,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
679
679
|
<div key={root}>
|
|
680
680
|
<button
|
|
681
681
|
onClick={() => setExpandedRoot(isExpanded ? null : root)}
|
|
682
|
-
className="w-full text-left px-3 py-2 rounded hover:bg-[
|
|
682
|
+
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
683
|
>
|
|
684
684
|
<span className="text-gray-500 text-[10px] w-3">{isExpanded ? '▾' : '▸'}</span>
|
|
685
685
|
<span>{rootName}</span>
|
|
@@ -691,7 +691,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
691
691
|
<button
|
|
692
692
|
key={p.path}
|
|
693
693
|
onClick={() => { addTab(p.path); setShowNewTabModal(false); setExpandedRoot(null); }}
|
|
694
|
-
className="w-full text-left px-3 py-1.5 rounded hover:bg-[
|
|
694
|
+
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
695
|
title={p.path}
|
|
696
696
|
>
|
|
697
697
|
<span className="text-gray-600 text-[10px]">↳</span> {p.name}
|
|
@@ -708,7 +708,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
708
708
|
</div>
|
|
709
709
|
)}
|
|
710
710
|
</div>
|
|
711
|
-
<div className="px-4 py-2 border-t border-[
|
|
711
|
+
<div className="px-4 py-2 border-t border-[var(--term-border)]">
|
|
712
712
|
<button
|
|
713
713
|
onClick={() => { setShowNewTabModal(false); setExpandedRoot(null); }}
|
|
714
714
|
className="w-full text-center text-[11px] text-gray-500 hover:text-gray-300 py-1"
|
|
@@ -723,7 +723,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
723
723
|
{/* Close confirmation dialog */}
|
|
724
724
|
{closeConfirm && (
|
|
725
725
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={() => setCloseConfirm(null)}>
|
|
726
|
-
<div className="bg-[
|
|
726
|
+
<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
727
|
<h3 className="text-sm font-semibold text-white mb-2">Close Tab</h3>
|
|
728
728
|
<p className="text-xs text-gray-400 mb-1">
|
|
729
729
|
This tab has {closeConfirm.sessions.length} active session{closeConfirm.sessions.length > 1 ? 's' : ''}:
|
|
@@ -950,17 +950,46 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
950
950
|
|
|
951
951
|
let disposed = false; // guard against post-cleanup writes (React Strict Mode)
|
|
952
952
|
|
|
953
|
+
// Read terminal theme from CSS variables
|
|
954
|
+
const cs = getComputedStyle(document.documentElement);
|
|
955
|
+
const tv = (name: string) => cs.getPropertyValue(name).trim();
|
|
956
|
+
const termBg = tv('--term-bg') || '#1a1a2e';
|
|
957
|
+
const termFg = tv('--term-fg') || '#e0e0e0';
|
|
958
|
+
const termCursor = tv('--term-cursor') || '#7c5bf0';
|
|
959
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
960
|
+
|
|
953
961
|
const term = new Terminal({
|
|
954
962
|
cursorBlink: true,
|
|
955
963
|
fontSize: 13,
|
|
956
964
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
957
965
|
scrollback: 10000,
|
|
958
966
|
logger: { trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {} },
|
|
959
|
-
theme: {
|
|
960
|
-
background:
|
|
961
|
-
foreground:
|
|
962
|
-
cursor:
|
|
963
|
-
selectionBackground: '
|
|
967
|
+
theme: isLight ? {
|
|
968
|
+
background: termBg,
|
|
969
|
+
foreground: termFg,
|
|
970
|
+
cursor: termCursor,
|
|
971
|
+
selectionBackground: termCursor + '44',
|
|
972
|
+
black: '#1a1a1a',
|
|
973
|
+
red: '#d32f2f',
|
|
974
|
+
green: '#388e3c',
|
|
975
|
+
yellow: '#f57f17',
|
|
976
|
+
blue: '#1976d2',
|
|
977
|
+
magenta: '#7b1fa2',
|
|
978
|
+
cyan: '#0097a7',
|
|
979
|
+
white: '#424242',
|
|
980
|
+
brightBlack: '#757575',
|
|
981
|
+
brightRed: '#e53935',
|
|
982
|
+
brightGreen: '#43a047',
|
|
983
|
+
brightYellow: '#f9a825',
|
|
984
|
+
brightBlue: '#1e88e5',
|
|
985
|
+
brightMagenta: '#8e24aa',
|
|
986
|
+
brightCyan: '#00acc1',
|
|
987
|
+
brightWhite: '#1a1a1a',
|
|
988
|
+
} : {
|
|
989
|
+
background: termBg,
|
|
990
|
+
foreground: termFg,
|
|
991
|
+
cursor: termCursor,
|
|
992
|
+
selectionBackground: termCursor + '66',
|
|
964
993
|
black: '#1a1a2e',
|
|
965
994
|
red: '#ff6b6b',
|
|
966
995
|
green: '#69db7c',
|
package/package.json
CHANGED