@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 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
-
@@ -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) => {
@@ -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-3">
249
+ <div className="flex items-center gap-2.5">
232
250
  {viewMode === 'tasks' && (
233
251
  <button
234
252
  onClick={() => setShowNewTask(true)}
235
- className="text-xs px-3 py-1 bg-[var(--accent)] text-white rounded hover:opacity-90"
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-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)] relative"
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-2.5 min-w-[14px] h-[14px] rounded-full bg-[var(--red)] text-[8px] text-white flex items-center justify-center px-1 font-bold">
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: '#7c5bf066',
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-[#1a1a2e] overflow-hidden">
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-[#12122a] border-b border-[#2a2a4a] shrink-0">
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-[#2a2a4a] select-none ${
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-[#1a1a2e] text-white'
469
- : 'text-gray-500 hover:text-gray-300 hover:bg-[#1a1a2e]/50'
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-[#4a4a6a] rounded px-1 text-[11px] text-white outline-none w-20"
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={() => setShowNewTabModal(true)}
510
- className="px-2 py-1 text-[11px] text-gray-500 hover:text-white hover:bg-[#2a2a4a]"
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-[#2a2a4a] rounded">
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-[#2a2a4a] rounded">
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-[#2a2a4a]'}`}
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-[#2a2a4a] rounded">
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-[#0e0e20] border-b border-[#2a2a4a] px-3 py-2 shrink-0 max-h-48 overflow-y-auto">
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-[#2a2a4a]">
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-[#2a2a4a]/50 hover:bg-[#1a1a2e]">
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-[#1a1a2e] border border-[#2a2a4a] 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-[#2a2a4a]">
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-[#2a2a4a] text-[12px] text-gray-300 flex items-center gap-2"
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-[#2a2a4a]">
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-[#2a2a4a] text-[12px] text-gray-300 flex items-center gap-2"
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={() => { addTab(p.path); setShowNewTabModal(false); setExpandedRoot(null); }}
694
- className="w-full text-left px-3 py-1.5 rounded hover:bg-[#2a2a4a] text-[11px] text-gray-300 flex items-center gap-2 truncate"
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-[#2a2a4a]">
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-[#1a1a2e] border border-[#2a2a4a] rounded-lg p-4 shadow-xl max-w-sm" onClick={(e) => e.stopPropagation()}>
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: '#1a1a2e',
961
- foreground: '#e0e0e0',
962
- cursor: '#7c5bf0',
963
- selectionBackground: '#7c5bf066',
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 --resume for project tabs on new session
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 --resume${skipFlag}\n` }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {