@benzsiangco/jarvis 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +5 -0
  2. package/bin/{jarvis.js → jarvis} +1 -1
  3. package/dist/cli.js +476 -350
  4. package/dist/electron/main.js +160 -0
  5. package/dist/electron/preload.js +19 -0
  6. package/package.json +21 -8
  7. package/skills.md +147 -0
  8. package/src/agents/index.ts +248 -0
  9. package/src/brain/loader.ts +136 -0
  10. package/src/cli.ts +411 -0
  11. package/src/config/index.ts +363 -0
  12. package/src/core/executor.ts +222 -0
  13. package/src/core/plugins.ts +148 -0
  14. package/src/core/types.ts +217 -0
  15. package/src/electron/main.ts +192 -0
  16. package/src/electron/preload.ts +25 -0
  17. package/src/electron/types.d.ts +20 -0
  18. package/src/index.ts +12 -0
  19. package/src/providers/antigravity-loader.ts +233 -0
  20. package/src/providers/antigravity.ts +585 -0
  21. package/src/providers/index.ts +523 -0
  22. package/src/sessions/index.ts +194 -0
  23. package/src/tools/index.ts +436 -0
  24. package/src/tui/index.tsx +784 -0
  25. package/src/utils/auth-prompt.ts +394 -0
  26. package/src/utils/index.ts +180 -0
  27. package/src/utils/native-picker.ts +71 -0
  28. package/src/utils/skills.ts +99 -0
  29. package/src/utils/table-integration-examples.ts +617 -0
  30. package/src/utils/table-utils.ts +401 -0
  31. package/src/web/build-ui.ts +27 -0
  32. package/src/web/server.ts +674 -0
  33. package/src/web/ui/dist/.gitkeep +0 -0
  34. package/src/web/ui/dist/main.css +1 -0
  35. package/src/web/ui/dist/main.js +320 -0
  36. package/src/web/ui/dist/main.js.map +20 -0
  37. package/src/web/ui/index.html +46 -0
  38. package/src/web/ui/src/App.tsx +143 -0
  39. package/src/web/ui/src/Modules/Safety/GuardianModal.tsx +83 -0
  40. package/src/web/ui/src/components/Layout/ContextPanel.tsx +243 -0
  41. package/src/web/ui/src/components/Layout/Header.tsx +91 -0
  42. package/src/web/ui/src/components/Layout/ModelSelector.tsx +235 -0
  43. package/src/web/ui/src/components/Layout/SessionStats.tsx +369 -0
  44. package/src/web/ui/src/components/Layout/Sidebar.tsx +895 -0
  45. package/src/web/ui/src/components/Modules/Chat/ChatStage.tsx +620 -0
  46. package/src/web/ui/src/components/Modules/Chat/MessageItem.tsx +446 -0
  47. package/src/web/ui/src/components/Modules/Editor/CommandInspector.tsx +71 -0
  48. package/src/web/ui/src/components/Modules/Editor/DiffViewer.tsx +83 -0
  49. package/src/web/ui/src/components/Modules/Terminal/TabbedTerminal.tsx +202 -0
  50. package/src/web/ui/src/components/Settings/SettingsModal.tsx +935 -0
  51. package/src/web/ui/src/config/models.ts +70 -0
  52. package/src/web/ui/src/main.tsx +13 -0
  53. package/src/web/ui/src/store/agentStore.ts +41 -0
  54. package/src/web/ui/src/store/uiStore.ts +64 -0
  55. package/src/web/ui/src/types/index.ts +54 -0
@@ -0,0 +1,46 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>JARVIS | OS for AI</title>
7
+ <link rel="stylesheet" href="/main.css">
8
+ <script src="https://cdn.tailwindcss.com?plugins=typography"></script>
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap');
11
+
12
+ :root {
13
+ --bg-primary: #09090b;
14
+ --border-primary: #27272a;
15
+ }
16
+
17
+ body {
18
+ font-family: 'Inter', sans-serif;
19
+ background-color: var(--bg-primary);
20
+ color: #fafafa;
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+
25
+ .custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
26
+ .custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
27
+ .custom-scrollbar::-webkit-scrollbar-thumb { background: #27272a; border-radius: 10px; }
28
+
29
+ .no-scrollbar::-webkit-scrollbar { display: none; }
30
+
31
+ /* Resizable panel handle styles */
32
+ [data-panel-group-direction="horizontal"] > [data-panel-resize-handle] {
33
+ width: 1px;
34
+ background-color: var(--border-primary);
35
+ transition: background-color 0.2s;
36
+ }
37
+ [data-panel-group-direction="horizontal"] > [data-panel-resize-handle]:hover {
38
+ background-color: #3f3f46;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body class="bg-zinc-950 text-zinc-100 overflow-hidden selection:bg-cyan-500/30">
43
+ <div id="root"></div>
44
+ <script type="module" src="/main.js"></script>
45
+ </body>
46
+ </html>
@@ -0,0 +1,143 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Panel, Group, Separator } from 'react-resizable-panels';
3
+ import { Sidebar } from './components/Layout/Sidebar';
4
+ import { Header } from './components/Layout/Header';
5
+ import { ChatStage } from './components/Modules/Chat/ChatStage';
6
+ import { ContextPanel } from './components/Layout/ContextPanel';
7
+ import { TabbedTerminal } from './components/Modules/Terminal/TabbedTerminal';
8
+ import { SettingsModal } from './components/Settings/SettingsModal';
9
+ import { AnimatePresence, motion } from 'framer-motion';
10
+ import { useUIStore } from './store/uiStore';
11
+ import { Menu, MessageSquare, Settings, Terminal as TerminalIcon, RefreshCw } from 'lucide-react';
12
+
13
+ export default function App() {
14
+ const {
15
+ isMobile, setMobile,
16
+ isSidebarOpen, toggleSidebar,
17
+ isContextOpen, toggleContext,
18
+ activeModule, setActiveModule,
19
+ terminalVisible, setTerminalVisible,
20
+ isSettingsOpen, setSettingsOpen
21
+ } = useUIStore();
22
+
23
+ const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
24
+
25
+ // Handle Responsiveness
26
+ useEffect(() => {
27
+ const handleResize = () => {
28
+ const mobile = window.innerWidth < 1024;
29
+ setMobile(mobile);
30
+ };
31
+ handleResize();
32
+ window.addEventListener('resize', handleResize);
33
+ return () => window.removeEventListener('resize', handleResize);
34
+ }, [setMobile]);
35
+
36
+ return (
37
+ <div className="flex h-screen w-full bg-zinc-950 text-zinc-200 overflow-hidden font-sans selection:bg-cyan-500/30 selection:text-white">
38
+
39
+ {/* Sidebar - Hidden on Mobile unless triggered */}
40
+ {!isMobile && (
41
+ <Sidebar
42
+ isCollapsed={!isSidebarOpen}
43
+ onToggle={toggleSidebar}
44
+ currentSessionId={currentSessionId}
45
+ onSessionSelect={setCurrentSessionId}
46
+ />
47
+ )}
48
+
49
+ <div className="flex-1 flex flex-col min-w-0 relative h-full">
50
+ {/* Rate-Limit Guardian Banner */}
51
+ <AnimatePresence>
52
+ {false && ( // Logic placeholder: actual trigger would be state-based
53
+ <motion.div
54
+ initial={{ y: -50, opacity: 0 }}
55
+ animate={{ y: 0, opacity: 1 }}
56
+ exit={{ y: -50, opacity: 0 }}
57
+ className="absolute top-0 left-0 right-0 z-[60] bg-amber-500 text-black px-6 py-2 flex items-center justify-center gap-3 shadow-2xl font-black text-[10px] uppercase tracking-[0.2em]"
58
+ >
59
+ <RefreshCw size={14} className="animate-spin" />
60
+ Rate limit reached. Switching to backup account...
61
+ </motion.div>
62
+ )}
63
+ </AnimatePresence>
64
+
65
+ <Header
66
+ onOpenSettings={() => setSettingsOpen(true)}
67
+ onToggleTerminal={() => setTerminalVisible(!terminalVisible)}
68
+ onToggleSidebar={toggleSidebar}
69
+ onToggleContext={toggleContext}
70
+ isSidebarCollapsed={!isSidebarOpen}
71
+ isContextCollapsed={!isContextOpen}
72
+ />
73
+
74
+ <main className="flex-1 relative overflow-hidden">
75
+ {isMobile ? (
76
+ /* MOBILE LAYOUT */
77
+ <div className="h-full flex flex-col">
78
+ <div className="flex-1 overflow-hidden">
79
+ {activeModule === 'chat' && <ChatStage sessionId={currentSessionId} onSessionCreated={setCurrentSessionId} />}
80
+ {activeModule === 'settings' && <div className="p-8 text-zinc-500 italic">Settings Module Coming Soon...</div>}
81
+ </div>
82
+
83
+ {/* Mobile Navigation Bar */}
84
+ <nav className="h-16 bg-zinc-950 border-t border-zinc-900 flex items-center justify-around px-4 pb-2">
85
+ <button
86
+ onClick={() => setActiveModule('chat')}
87
+ className={`flex flex-col items-center gap-1 p-2 rounded-xl transition-all ${activeModule === 'chat' ? 'text-cyan-500 bg-cyan-900/10' : 'text-zinc-600'}`}
88
+ >
89
+ <MessageSquare size={18} />
90
+ <span className="text-[9px] font-bold uppercase tracking-wide">Chat</span>
91
+ </button>
92
+ <button
93
+ onClick={() => setSettingsOpen(true)}
94
+ className="flex flex-col items-center gap-1 p-2 rounded-xl transition-all text-zinc-600 hover:text-zinc-300"
95
+ >
96
+ <Settings size={18} />
97
+ <span className="text-[9px] font-bold uppercase tracking-wide">Setup</span>
98
+ </button>
99
+ </nav>
100
+ </div>
101
+ ) : (
102
+ /* DESKTOP LAYOUT */
103
+ <Group orientation="horizontal">
104
+ <Panel defaultSize={75} minSize={30}>
105
+ <div className="h-full relative flex flex-col overflow-hidden">
106
+ <div className="flex-1 relative overflow-hidden bg-zinc-950/20">
107
+ {activeModule === 'chat' && <ChatStage sessionId={currentSessionId} onSessionCreated={setCurrentSessionId} />}
108
+ </div>
109
+
110
+ <AnimatePresence>
111
+ {terminalVisible && (
112
+ <motion.div
113
+ initial={{ height: 0 }}
114
+ animate={{ height: '35%' }}
115
+ exit={{ height: 0 }}
116
+ className="relative z-40 bg-zinc-950 border-t border-zinc-800 shadow-[0_-10px_40px_rgba(0,0,0,0.5)] overflow-hidden"
117
+ >
118
+ <TabbedTerminal onClose={() => setTerminalVisible(false)} />
119
+ </motion.div>
120
+ )}
121
+ </AnimatePresence>
122
+ </div>
123
+ </Panel>
124
+
125
+ {isContextOpen && (
126
+ <>
127
+ <Separator className="w-px bg-zinc-800 hover:bg-zinc-600 transition-colors" />
128
+ <Panel defaultSize={25} minSize={20}>
129
+ <ContextPanel sessionId={currentSessionId} />
130
+ </Panel>
131
+ </>
132
+ )}
133
+ </Group>
134
+ )}
135
+ </main>
136
+ </div>
137
+
138
+ <AnimatePresence>
139
+ {isSettingsOpen && <SettingsModal onClose={() => setSettingsOpen(false)} />}
140
+ </AnimatePresence>
141
+ </div>
142
+ );
143
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { ShieldAlert, Check, X, Terminal, FileEdit, AlertTriangle } from 'lucide-react';
4
+
5
+ interface GuardianModalProps {
6
+ isOpen: boolean;
7
+ toolName: string;
8
+ args: any;
9
+ onApprove: () => void;
10
+ onDeny: () => void;
11
+ }
12
+
13
+ export function GuardianModal({ isOpen, toolName, args, onApprove, onDeny }: GuardianModalProps) {
14
+ const isDangerous = ['bash', 'edit', 'write'].includes(toolName);
15
+
16
+ return (
17
+ <AnimatePresence>
18
+ {isOpen && (
19
+ <motion.div
20
+ initial={{ opacity: 0 }}
21
+ animate={{ opacity: 1 }}
22
+ exit={{ opacity: 0 }}
23
+ className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/90 backdrop-blur-md"
24
+ >
25
+ <motion.div
26
+ initial={{ scale: 0.9, y: 20 }}
27
+ animate={{ scale: 1, y: 0 }}
28
+ exit={{ scale: 0.9, y: 20 }}
29
+ className="w-full max-w-lg bg-zinc-950 border border-red-500/30 rounded-[2.5rem] shadow-[0_0_100px_rgba(239,68,68,0.2)] overflow-hidden ring-1 ring-red-500/20"
30
+ >
31
+ <div className="p-8 space-y-6">
32
+ <div className="flex items-center gap-4">
33
+ <div className="w-14 h-14 rounded-2xl bg-red-500/10 flex items-center justify-center text-red-500 shadow-inner">
34
+ <ShieldAlert size={32} />
35
+ </div>
36
+ <div>
37
+ <h2 className="text-xl font-black uppercase tracking-tighter text-white italic">Security Intercept</h2>
38
+ <p className="text-[10px] font-black uppercase tracking-widest text-red-500/60">Guardian Protocol Active</p>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="p-6 rounded-3xl bg-zinc-900/50 border border-zinc-800 space-y-4">
43
+ <div className="flex items-center gap-3">
44
+ {toolName === 'bash' ? <Terminal size={18} className="text-zinc-500" /> : <FileEdit size={18} className="text-zinc-500" />}
45
+ <span className="text-sm font-bold text-zinc-200">Requesting: <span className="text-red-400 font-mono underline underline-offset-4">{toolName}</span></span>
46
+ </div>
47
+
48
+ <div className="space-y-2">
49
+ <p className="text-[10px] font-black uppercase tracking-widest text-zinc-600 italic">Payload Analysis</p>
50
+ <div className="bg-zinc-950 p-4 rounded-xl border border-zinc-900 font-mono text-[11px] text-zinc-400 overflow-x-auto max-h-40 custom-scrollbar shadow-inner">
51
+ <pre>{JSON.stringify(args, null, 2)}</pre>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div className="flex items-center gap-3 p-4 bg-amber-500/5 border border-amber-500/20 rounded-2xl">
57
+ <AlertTriangle size={18} className="text-amber-500 shrink-0" />
58
+ <p className="text-[11px] text-amber-200/70 font-medium leading-relaxed">
59
+ This operation has been flagged as <strong>high-risk</strong>. Authorizing this action grants the agent system-level access.
60
+ </p>
61
+ </div>
62
+
63
+ <div className="grid grid-cols-2 gap-4 pt-2">
64
+ <button
65
+ onClick={onDeny}
66
+ className="flex items-center justify-center gap-2 py-4 rounded-2xl bg-zinc-900 text-zinc-400 text-xs font-black uppercase tracking-widest hover:bg-zinc-800 transition-all active:scale-95"
67
+ >
68
+ <X size={16} /> Deny Access
69
+ </button>
70
+ <button
71
+ onClick={onApprove}
72
+ className="flex items-center justify-center gap-2 py-4 rounded-2xl bg-red-600 text-white text-xs font-black uppercase tracking-widest hover:bg-red-500 transition-all active:scale-95 shadow-[0_10px_30px_rgba(220,38,38,0.3)]"
73
+ >
74
+ <Check size={16} strokeWidth={4} /> Approve
75
+ </button>
76
+ </div>
77
+ </div>
78
+ </motion.div>
79
+ </motion.div>
80
+ )}
81
+ </AnimatePresence>
82
+ );
83
+ }
@@ -0,0 +1,243 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Shield, CreditCard, Activity, Cpu, RotateCw, CheckCircle2, AlertTriangle, XCircle, Clock, BarChart3 } from 'lucide-react';
3
+ import { useAgentStore } from '../../store/agentStore';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { SessionStats } from './SessionStats';
6
+
7
+ export function ContextPanel({ sessionId }: { sessionId: string | null }) {
8
+ const { persona } = useAgentStore();
9
+
10
+ // Tab state with localStorage persistence
11
+ type TabId = 'persona' | 'pool' | 'telemetry' | 'session';
12
+ const [activeTab, setActiveTab] = useState<TabId>(() => {
13
+ const saved = localStorage.getItem('intelligenceHub.activeTab');
14
+ return (saved as TabId) || 'pool';
15
+ });
16
+
17
+ // Persist tab selection
18
+ useEffect(() => {
19
+ localStorage.setItem('intelligenceHub.activeTab', activeTab);
20
+ }, [activeTab]);
21
+
22
+ // Real-time state from backend
23
+ const [accounts, setAccounts] = useState<any[]>([]);
24
+ const [activeAccountId, setActiveAccountId] = useState<string | null>(null);
25
+ const [lastRefreshed, setLastRefreshed] = useState<Date>(new Date());
26
+
27
+ // Fetch auth status frequently to show real-time rotation
28
+ useEffect(() => {
29
+ const fetchStatus = async () => {
30
+ try {
31
+ const res = await fetch('/api/auth/status');
32
+ const data = await res.json();
33
+ setAccounts(data.accounts || []);
34
+ setActiveAccountId(data.activeAccount?.id || null);
35
+ setLastRefreshed(new Date());
36
+ } catch (e) { console.error(e); }
37
+ };
38
+
39
+ fetchStatus();
40
+ const interval = setInterval(fetchStatus, 2000); // Poll every 2s for live rotation visualization
41
+ return () => clearInterval(interval);
42
+ }, []);
43
+
44
+ // Sort accounts so active one is always at top? Or keep fixed list and highlight active?
45
+ // Let's keep fixed list to prevent jumping, but highlight active clearly.
46
+
47
+ // Tab configuration
48
+ const tabs = [
49
+ { id: 'persona' as const, label: 'Persona', icon: Shield },
50
+ { id: 'pool' as const, label: 'Pool', icon: CreditCard },
51
+ { id: 'telemetry' as const, label: 'Metrics', icon: Activity },
52
+ { id: 'session' as const, label: 'Session', icon: BarChart3 },
53
+ ];
54
+
55
+ return (
56
+ <div className="h-full flex flex-col bg-zinc-900/50 border-l border-zinc-800 overflow-hidden">
57
+ <div className="p-4 border-b border-zinc-800 flex items-center justify-between bg-zinc-900/80 backdrop-blur-md sticky top-0 z-10">
58
+ <h2 className="text-xs font-black uppercase tracking-widest text-zinc-500 flex items-center gap-2">
59
+ <Cpu size={14} className="text-cyan-500" /> Intelligence Hub
60
+ </h2>
61
+ <span className="text-[9px] font-mono text-zinc-600 flex items-center gap-1">
62
+ <Activity size={10} className="animate-pulse text-emerald-500" />
63
+ LIVE
64
+ </span>
65
+ </div>
66
+
67
+ {/* Tab Navigation */}
68
+ <div className="border-b border-zinc-800 bg-zinc-900/80 backdrop-blur-sm">
69
+ <div className="flex gap-1 p-2">
70
+ {tabs.map(tab => (
71
+ <button
72
+ key={tab.id}
73
+ onClick={() => setActiveTab(tab.id)}
74
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all ${
75
+ activeTab === tab.id
76
+ ? 'bg-cyan-900/30 border border-cyan-500/50 text-cyan-400 shadow-[0_0_10px_rgba(6,182,212,0.1)]'
77
+ : 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50'
78
+ }`}
79
+ >
80
+ <tab.icon size={12} />
81
+ <span>{tab.label}</span>
82
+ </button>
83
+ ))}
84
+ </div>
85
+ </div>
86
+
87
+ <div className="flex-1 overflow-y-auto p-4 space-y-8 custom-scrollbar">
88
+ {/* Persona Section */}
89
+ {activeTab === 'persona' && (
90
+ <section>
91
+ <div className="flex items-center gap-2 mb-3 text-zinc-400">
92
+ <Shield size={14} />
93
+ <span className="text-[10px] font-bold uppercase tracking-wider">Active Persona</span>
94
+ </div>
95
+ <div className="p-4 rounded-xl bg-zinc-800/30 border border-zinc-800/60 ring-1 ring-white/5 relative overflow-hidden group">
96
+ <div className="absolute inset-0 bg-gradient-to-br from-cyan-500/5 to-transparent opacity-50" />
97
+ <div className="relative z-10">
98
+ <p className="text-sm font-black text-zinc-200 capitalize tracking-wide">{persona.replace('-', ' ')}</p>
99
+ <div className="flex items-center gap-2 mt-2">
100
+ <span className="px-1.5 py-0.5 rounded bg-cyan-900/30 border border-cyan-800 text-[9px] font-mono text-cyan-400">RBAC: FULL</span>
101
+ <span className="px-1.5 py-0.5 rounded bg-zinc-900 border border-zinc-800 text-[9px] font-mono text-zinc-500">v1.0.3</span>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </section>
106
+ )}
107
+
108
+ {/* Account Pool / Rate Limit Guardian */}
109
+ {activeTab === 'pool' && (
110
+ <section>
111
+ <div className="flex items-center justify-between mb-3 text-zinc-400">
112
+ <div className="flex items-center gap-2">
113
+ <CreditCard size={14} />
114
+ <span className="text-[10px] font-bold uppercase tracking-wider">Rotation Pool</span>
115
+ </div>
116
+ <div className="text-[9px] font-mono text-zinc-600 bg-zinc-900 px-1.5 py-0.5 rounded border border-zinc-800">
117
+ {accounts.length} NODES
118
+ </div>
119
+ </div>
120
+
121
+ <div className="space-y-3">
122
+ <AnimatePresence mode='popLayout'>
123
+ {accounts.length > 0 ? accounts.map((acc: any) => {
124
+ const isActive = activeAccountId === acc.account.id;
125
+ const isRateLimited = acc.status === 'rate-limited';
126
+ const isExpired = acc.status === 'token-expired';
127
+
128
+ return (
129
+ <motion.div
130
+ layout
131
+ key={acc.account.id}
132
+ initial={{ opacity: 0, scale: 0.95 }}
133
+ animate={{ opacity: 1, scale: 1 }}
134
+ exit={{ opacity: 0, scale: 0.95 }}
135
+ transition={{ duration: 0.2 }}
136
+ className={`relative p-3 rounded-xl border transition-all duration-300 overflow-hidden ${
137
+ isActive
138
+ ? 'bg-cyan-900/10 border-cyan-500/50 shadow-[0_0_15px_rgba(6,182,212,0.1)]'
139
+ : 'bg-zinc-900/40 border-zinc-800/60'
140
+ }`}
141
+ >
142
+ {isActive && (
143
+ <motion.div
144
+ layoutId="active-glow"
145
+ className="absolute inset-0 bg-cyan-500/5 pointer-events-none"
146
+ />
147
+ )}
148
+
149
+ <div className="relative z-10 flex items-center justify-between">
150
+ <div className="flex items-center gap-3 overflow-hidden">
151
+ <div className={`w-2 h-2 rounded-full shrink-0 ${
152
+ isActive ? 'bg-cyan-500 shadow-[0_0_8px_#06b6d4] animate-pulse' :
153
+ isRateLimited ? 'bg-amber-500' :
154
+ isExpired ? 'bg-red-500' : 'bg-zinc-600'
155
+ }`} />
156
+ <div className="flex flex-col min-w-0">
157
+ <p className={`text-[10px] font-bold truncate transition-colors ${isActive ? 'text-cyan-100' : 'text-zinc-400'}`}>
158
+ {acc.account.email || acc.account.id}
159
+ </p>
160
+ <div className="flex items-center gap-2">
161
+ <span className="text-[9px] font-mono text-zinc-600 uppercase tracking-wider">
162
+ {acc.account.quotaType || 'Google'}
163
+ </span>
164
+ {isActive && (
165
+ <span className="text-[9px] font-bold text-cyan-500 animate-in fade-in zoom-in duration-300">
166
+ ACTIVE
167
+ </span>
168
+ )}
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <div className="shrink-0 pl-2">
174
+ {isRateLimited ? (
175
+ <div className="text-amber-500" title="Rate Limited"><Clock size={14} /></div>
176
+ ) : isExpired ? (
177
+ <div className="text-red-500" title="Token Expired"><XCircle size={14} /></div>
178
+ ) : isActive ? (
179
+ <div className="text-cyan-500"><RotateCw size={14} className="animate-spin duration-[3000ms]" /></div>
180
+ ) : (
181
+ <div className="text-zinc-700"><CheckCircle2 size={14} /></div>
182
+ )}
183
+ </div>
184
+ </div>
185
+
186
+ {/* Rate Limit Progress Bar if applicable */}
187
+ {isRateLimited && acc.rateLimitRemaining && (
188
+ <div className="mt-2 h-0.5 bg-zinc-800 rounded-full overflow-hidden">
189
+ <motion.div
190
+ className="h-full bg-amber-500"
191
+ initial={{ width: "100%" }}
192
+ animate={{ width: "0%" }}
193
+ transition={{ duration: acc.rateLimitRemaining / 1000, ease: "linear" }}
194
+ />
195
+ </div>
196
+ )}
197
+ </motion.div>
198
+ );
199
+ }) : (
200
+ <div className="p-4 rounded-xl border border-dashed border-zinc-800 text-center">
201
+ <AlertTriangle size={16} className="mx-auto text-zinc-600 mb-2" />
202
+ <p className="text-[10px] text-zinc-500 italic">No nodes in rotation pool</p>
203
+ </div>
204
+ )}
205
+ </AnimatePresence>
206
+ </div>
207
+ </section>
208
+ )}
209
+
210
+ {/* Live Status */}
211
+ {activeTab === 'telemetry' && (
212
+ <section>
213
+ <div className="flex items-center gap-2 mb-3 text-zinc-400">
214
+ <Activity size={14} />
215
+ <span className="text-[10px] font-bold uppercase tracking-wider">System Telemetry</span>
216
+ </div>
217
+ <div className="grid grid-cols-2 gap-3">
218
+ <div className="p-3 rounded-xl bg-zinc-900/40 border border-zinc-800/60 text-center relative overflow-hidden group">
219
+ <div className="absolute inset-0 bg-cyan-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
220
+ <p className="text-[9px] text-zinc-500 font-black uppercase tracking-widest mb-1">Latency</p>
221
+ <p className="text-sm font-mono font-bold text-cyan-500 flex items-center justify-center gap-1">
222
+ ~24<span className="text-[10px] text-cyan-500/50">ms</span>
223
+ </p>
224
+ </div>
225
+ <div className="p-3 rounded-xl bg-zinc-900/40 border border-zinc-800/60 text-center relative overflow-hidden group">
226
+ <div className="absolute inset-0 bg-amber-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
227
+ <p className="text-[9px] text-zinc-500 font-black uppercase tracking-widest mb-1">Context</p>
228
+ <p className="text-sm font-mono font-bold text-amber-500 flex items-center justify-center gap-1">
229
+ 1.2<span className="text-[10px] text-amber-500/50">k</span>
230
+ </p>
231
+ </div>
232
+ </div>
233
+ </section>
234
+ )}
235
+
236
+ {/* Session Stats */}
237
+ {activeTab === 'session' && (
238
+ <SessionStats sessionId={sessionId} />
239
+ )}
240
+ </div>
241
+ </div>
242
+ );
243
+ }
@@ -0,0 +1,91 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Settings, Terminal as LucideTerminal,
4
+ Menu, Layout, Zap
5
+ } from 'lucide-react';
6
+ import type { Agent } from '../../types';
7
+
8
+ export interface HeaderProps {
9
+ onOpenSettings: () => void;
10
+ onToggleTerminal: () => void;
11
+ onToggleSidebar: () => void;
12
+ onToggleContext: () => void;
13
+ isSidebarCollapsed: boolean;
14
+ isContextCollapsed: boolean;
15
+ }
16
+
17
+ const cn = (...classes: any[]) => classes.filter(Boolean).join(' ');
18
+
19
+ import { useUIStore } from '../../store/uiStore';
20
+ import { useAgentStore } from '../../store/agentStore';
21
+ import { ModelSelector } from './ModelSelector';
22
+
23
+ export function Header({ onOpenSettings, onToggleTerminal, onToggleSidebar, onToggleContext, isSidebarCollapsed, isContextCollapsed }: HeaderProps) {
24
+ const { setActiveModule, setTerminalVisible, terminalVisible, toggleSidebar, toggleContext } = useUIStore();
25
+ const { persona, setPersona, selectedModelId, setSelectedModelId, selectedAgentId, setSelectedAgentId } = useAgentStore();
26
+
27
+ const [workdir, setWorkdir] = useState('');
28
+
29
+ useEffect(() => {
30
+ // Load config to get workdir
31
+ fetch('/api/config')
32
+ .then(r => r.json())
33
+ .then(data => {
34
+ setWorkdir(data.workdir || '');
35
+ if (data.model && !selectedModelId) setSelectedModelId(data.model);
36
+ })
37
+ .catch(console.error);
38
+ }, []);
39
+
40
+ const handleAgentToggle = (id: 'plan' | 'build') => {
41
+ setSelectedAgentId(id);
42
+ // Sync persona based on agent mode if needed
43
+ if (id === 'plan') setPersona('minimalist');
44
+ else setPersona('it-expert');
45
+ };
46
+
47
+ return (
48
+ <header className="h-14 md:h-16 border-b border-zinc-800/50 flex items-center justify-between px-3 md:px-4 bg-zinc-950/80 backdrop-blur-xl z-50 shrink-0">
49
+ <div className="flex items-center gap-2 md:gap-4 overflow-hidden">
50
+ {isSidebarCollapsed && (
51
+ <button onClick={onToggleSidebar} className="p-2 hover:bg-zinc-900 rounded-lg text-zinc-500 hover:text-white transition-all shrink-0">
52
+ <Menu size={18} className="md:w-5 md:h-5" />
53
+ </button>
54
+ )}
55
+
56
+ <div className="hidden md:flex items-center gap-2 text-xs font-medium text-zinc-600 shrink-0">
57
+ <span className="truncate max-w-[150px] font-mono">{workdir.split(/[\\\/]/).pop()}</span>
58
+ </div>
59
+
60
+ <ModelSelector
61
+ selectedModelId={selectedModelId}
62
+ onModelChange={setSelectedModelId}
63
+ />
64
+
65
+ <div className="hidden sm:flex bg-zinc-900/50 rounded-lg p-1 border border-zinc-800/50 shadow-inner shrink-0">
66
+ <button onClick={() => handleAgentToggle('plan')} className={cn("px-3 py-1 md:px-4 md:py-1.5 rounded-md text-[9px] font-black uppercase tracking-widest transition-all", selectedAgentId === 'plan' ? "bg-zinc-800 text-amber-500 shadow-md ring-1 ring-amber-500/20" : "text-zinc-600 hover:text-zinc-400")}>Plan</button>
67
+ <button onClick={() => handleAgentToggle('build')} className={cn("px-3 py-1 md:px-4 md:py-1.5 rounded-md text-[9px] font-black uppercase tracking-widest transition-all", selectedAgentId === 'build' ? "bg-zinc-800 text-cyan-400 shadow-md ring-1 ring-cyan-500/20" : "text-zinc-600 hover:text-zinc-400")}>Build</button>
68
+ </div>
69
+ </div>
70
+
71
+ <div className="flex items-center gap-2 shrink-0">
72
+ <div className="hidden xl:flex items-center gap-2 px-3 py-1.5 bg-zinc-900/30 border border-zinc-800 rounded-lg text-[10px] font-black uppercase tracking-widest text-zinc-600">
73
+ <Zap size={12} className="text-emerald-500 animate-pulse" />
74
+ <span className="text-emerald-600/80 italic">Secure Link</span>
75
+ </div>
76
+
77
+ <button
78
+ onClick={onToggleTerminal}
79
+ className="flex items-center gap-2 px-3 py-1.5 md:px-4 md:py-2 rounded-lg transition-all font-bold text-[10px] uppercase tracking-widest bg-zinc-900/50 border border-zinc-800 text-zinc-500 hover:text-zinc-200 hover:border-zinc-700 shrink-0"
80
+ >
81
+ <LucideTerminal size={14} />
82
+ <span className="hidden sm:inline">Console</span>
83
+ </button>
84
+
85
+ <button onClick={onToggleContext} className={cn("p-2 rounded-lg transition-all shrink-0", !isContextCollapsed ? "text-cyan-500 bg-zinc-900 shadow-inner" : "text-zinc-600 hover:text-white")}>
86
+ <Layout size={18} className="md:w-[18px] md:h-[18px]" />
87
+ </button>
88
+ </div>
89
+ </header>
90
+ );
91
+ }