total-recall-brain 3.6.3 → 3.6.5

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.
@@ -15,7 +15,7 @@ import ApiKeysPage from './pages/ApiKeysPage'
15
15
  import IntegrationsPage from './pages/IntegrationsPage'
16
16
  import BrainSelector from './components/BrainSelector'
17
17
  import AutomationsPage from './pages/AutomationsPage'
18
- import DeploymentsPage from './pages/DeploymentsPage'
18
+ import ModelsPage from './pages/ModelsPage'
19
19
  import SkillsPage from './pages/SkillsPage'
20
20
  import UsagePage from './pages/UsagePage'
21
21
  import CollabPage from './pages/CollabPage'
@@ -99,14 +99,11 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
99
99
  </svg>
100
100
  Sandbox
101
101
  </NavLink>
102
- <NavLink to="/deployments" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-deployments">
102
+ <NavLink to="/models" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-models">
103
103
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
104
- <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
105
- <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
106
- <line x1="6" y1="6" x2="6.01" y2="6" />
107
- <line x1="6" y1="18" x2="6.01" y2="18" />
104
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line>
108
105
  </svg>
109
- Deployments
106
+ Models & Agents
110
107
  </NavLink>
111
108
  <NavLink to="/health" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-health">
112
109
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
@@ -312,7 +309,7 @@ function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string;
312
309
  <Route path="/automations" element={<AutomationsPage />} />
313
310
  <Route path="/files" element={<FilesPage />} />
314
311
  <Route path="/sandbox" element={<SandboxPage />} />
315
- <Route path="/deployments" element={<DeploymentsPage />} />
312
+ <Route path="/models" element={<ModelsPage />} />
316
313
  <Route path="/health" element={<HealthPage />} />
317
314
  <Route path="/usage" element={<UsagePage />} />
318
315
  <Route path="/settings" element={<SettingsPage />} />
@@ -530,6 +530,13 @@ export async function triggerDream(): Promise<{ success: boolean; status: string
530
530
  return res.json()
531
531
  }
532
532
 
533
+ export async function runAgentDiagnostics(): Promise<{ success: boolean; output: string }> {
534
+ const res = await apiFetch(`${API_BASE}/api/diagnostics/agents`, { method: 'POST' })
535
+ if (!res.ok) throw new Error(`Diagnostics API error: ${res.status}`)
536
+ return res.json()
537
+ }
538
+
539
+
533
540
  export async function fetchGraph(): Promise<{ nodes: MemoryNode[]; routes: unknown[] }> {
534
541
  const res = await apiFetch(`${API_BASE}/api/graph`)
535
542
  if (!res.ok) throw new Error(`Graph API error: ${res.status}`)
@@ -181,9 +181,14 @@ export default function BrainSelector({ activeBrainId, onBrainChange }: BrainSel
181
181
  {brains.map(brain => {
182
182
  const isSelected = selectedIds.includes(brain.id);
183
183
  return (
184
- <button
184
+ <div
185
185
  key={brain.id}
186
- onClick={(e) => handleRowClick(brain.id, e)}
186
+ onClick={(e) => {
187
+ if (brain.exists) {
188
+ handleRowClick(brain.id, e);
189
+ setExpanded(false); // Close dropdown on single selection
190
+ }
191
+ }}
187
192
  style={{
188
193
  width: '100%',
189
194
  padding: '8px 10px',
@@ -201,7 +206,8 @@ export default function BrainSelector({ activeBrainId, onBrainChange }: BrainSel
201
206
  }}
202
207
  onMouseEnter={e => { if (brain.exists) (e.currentTarget as HTMLElement).style.background = 'var(--bg-tertiary)' }}
203
208
  onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = isSelected ? 'var(--accent-faint)' : 'transparent' }}
204
- disabled={!brain.exists}
209
+ role="button"
210
+ tabIndex={0}
205
211
  >
206
212
  <input
207
213
  type="checkbox"
@@ -247,7 +253,7 @@ export default function BrainSelector({ activeBrainId, onBrainChange }: BrainSel
247
253
  }}>
248
254
  {brain.layer}
249
255
  </span>
250
- </button>
256
+ </div>
251
257
  );
252
258
  })}
253
259
  </div>
@@ -271,6 +271,7 @@ code, pre { font-family: var(--font-mono); }
271
271
  /* ─── Chat ───────────────────────────────────────────────────────────────────── */
272
272
 
273
273
  .chat-container {
274
+ flex: 1;
274
275
  display: flex;
275
276
  flex-direction: column;
276
277
  height: calc(100vh - 1px);
@@ -718,6 +718,8 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
718
718
  flexDirection: 'column',
719
719
  gap: '16px',
720
720
  width: '100%',
721
+ maxWidth: 800,
722
+ margin: '0 auto',
721
723
  pointerEvents: 'auto'
722
724
  }}>
723
725
  {messages.map((m, index) => (
@@ -166,7 +166,7 @@ function CollabDashboard({ token, username, onLogout, onSelectSandbox }: { token
166
166
  <p style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>Share annotations and real-time page chat rooms</p>
167
167
  </div>
168
168
  <div style={{ display: 'flex', gap: '10px' }}>
169
- <button className="btn btn-purple" onClick={onSelectSandbox}>🌐 Open Page Sandbox</button>
169
+ <button className="btn btn-purple" onClick={onSelectSandbox}>🌐 Open Browse Workspace</button>
170
170
  <button className="btn btn-ghost" onClick={onLogout} style={{ color: '#f87171' }}>Logout</button>
171
171
  </div>
172
172
  </div>
@@ -219,7 +219,7 @@ function CollabDashboard({ token, username, onLogout, onSelectSandbox }: { token
219
219
  function CollabSandbox({ token, username, onBack }: { token: string, username: string, onBack: () => void }) {
220
220
  const [groups, setGroups] = useState<CollabGroup[]>([]);
221
221
  const [selectedGroup, setSelectedGroup] = useState<CollabGroup | null>(null);
222
- const [simulatedUrl, setSimulatedUrl] = useState('https://news.ycombinator.com');
222
+ const [activeBrowseUrl, setActiveBrowseUrl] = useState('https://news.ycombinator.com');
223
223
  const [activeUrl, setActiveUrl] = useState('https://news.ycombinator.com');
224
224
  const [annotations, setAnnotations] = useState<Annotation[]>([]);
225
225
  const [noteText, setNoteText] = useState('');
@@ -313,9 +313,9 @@ function CollabSandbox({ token, username, onBack }: { token: string, username: s
313
313
 
314
314
  const handleUrlSubmit = (e: React.FormEvent) => {
315
315
  e.preventDefault();
316
- if (!simulatedUrl.trim()) return;
316
+ if (!activeBrowseUrl.trim()) return;
317
317
  setChatMessages([]); setSystemLogs([]); setActiveUsers([]);
318
- setActiveUrl(simulatedUrl);
318
+ setActiveUrl(activeBrowseUrl);
319
319
  };
320
320
 
321
321
  const handleAddAnnotation = async (e: React.FormEvent) => {
@@ -348,14 +348,14 @@ function CollabSandbox({ token, username, onBack }: { token: string, username: s
348
348
  <div style={{ display: 'flex', gap: '16px', alignItems: 'center', justifyContent: 'space-between', background: 'rgba(255,255,255,0.02)', padding: '12px 20px', borderRadius: '8px', border: '1px solid var(--border)', flexWrap: 'wrap' }}>
349
349
  <button className="btn btn-ghost" onClick={onBack}>← Back to Workspaces</button>
350
350
  <form onSubmit={handleUrlSubmit} style={{ display: 'flex', gap: '10px', flex: '1', minWidth: '250px' }}>
351
- <input type="text" value={simulatedUrl} onChange={e => setSimulatedUrl(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 12px', borderRadius: '6px', flex: '1', fontFamily: 'monospace' }} />
352
- <button type="submit" className="btn btn-purple">Simulate Visit</button>
351
+ <input type="text" value={activeBrowseUrl} onChange={e => setActiveBrowseUrl(e.target.value)} style={{ background: 'rgba(0,0,0,0.2)', border: '1px solid var(--border)', color: '#fff', padding: '6px 12px', borderRadius: '6px', flex: '1', fontFamily: 'monospace' }} />
352
+ <button type="submit" className="btn btn-purple">Connect URL</button>
353
353
  </form>
354
354
  <div style={{ fontSize: '13px', color: wsConnected ? '#34d399' : '#f87171' }}>● {wsConnected ? 'Connected' : 'Disconnected'}</div>
355
355
  </div>
356
356
 
357
357
  <div style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
358
- Simulated browser context: <strong style={{ color: 'var(--purple-primary)', fontFamily: 'monospace' }}>{activeUrl}</strong>
358
+ Active browse context URL: <strong style={{ color: 'var(--purple-primary)', fontFamily: 'monospace' }}>{activeUrl}</strong>
359
359
  </div>
360
360
 
361
361
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 340px', gap: '20px', height: '500px' }}>
@@ -0,0 +1,380 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { fetchHealth, runAgentDiagnostics, fetchConfigJson, saveConfigJson } from '../api';
3
+ import type { HealthData, ConfigJson } from '../types';
4
+
5
+ export default function ModelsPage() {
6
+ const [health, setHealth] = useState<HealthData | null>(null);
7
+ const [configData, setConfigData] = useState<ConfigJson | null>(null);
8
+ const [loading, setLoading] = useState(true);
9
+ const [saving, setSaving] = useState(false);
10
+ const [error, setError] = useState<string | null>(null);
11
+ const [success, setSuccess] = useState<string | null>(null);
12
+
13
+ // CLI Agent Diagnostics state
14
+ const [runningDiagnostics, setRunningDiagnostics] = useState(false);
15
+ const [diagnosticLogs, setDiagnosticLogs] = useState<string | null>(null);
16
+
17
+ const fetchSystemData = useCallback(async () => {
18
+ try {
19
+ const systemHealth = await fetchHealth();
20
+ setHealth(systemHealth);
21
+
22
+ const data = await fetchConfigJson();
23
+ if (!data.secrets) data.secrets = {};
24
+ if (!data.brain) data.brain = {};
25
+ setConfigData(data);
26
+
27
+ setError(null);
28
+ } catch (err: unknown) {
29
+ setError((err as Error).message || 'Failed to sync system data.');
30
+ } finally {
31
+ setLoading(false);
32
+ }
33
+ }, []);
34
+
35
+ useEffect(() => {
36
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate metrics polling on mount
37
+ void fetchSystemData();
38
+ }, [fetchSystemData]);
39
+
40
+ const handleSaveConfig = async () => {
41
+ if (!configData) return;
42
+ setSaving(true);
43
+ setError(null);
44
+ setSuccess(null);
45
+ try {
46
+ await saveConfigJson(configData);
47
+ setSuccess('Configuration saved successfully. Kernel will hot-reload settings.');
48
+ setTimeout(() => setSuccess(null), 4000);
49
+ } catch (err: unknown) {
50
+ setError((err as Error).message || 'Failed to save configuration.');
51
+ } finally {
52
+ setSaving(false);
53
+ }
54
+ };
55
+
56
+ const handleRunDiagnostics = async (e: React.FormEvent) => {
57
+ e.preventDefault();
58
+ setRunningDiagnostics(true);
59
+ setError(null);
60
+ setSuccess(null);
61
+ setDiagnosticLogs(null);
62
+ try {
63
+ const res = await runAgentDiagnostics();
64
+ setDiagnosticLogs(res.output);
65
+ if (res.success) {
66
+ setSuccess('CLI Agent diagnostics completed successfully.');
67
+ } else {
68
+ setError('CLI Agent diagnostics finished with warnings or missing binaries.');
69
+ }
70
+ void fetchSystemData();
71
+ } catch (err: unknown) {
72
+ setError((err as Error).message || 'Diagnostics execution failed.');
73
+ } finally {
74
+ setRunningDiagnostics(false);
75
+ }
76
+ };
77
+
78
+ const updateSecretsProp = (prop: string, value: string) => {
79
+ if (!configData) return;
80
+ setConfigData({
81
+ ...configData,
82
+ secrets: {
83
+ ...configData.secrets,
84
+ [prop]: value
85
+ }
86
+ });
87
+ };
88
+
89
+ const updateBrainProp = (prop: string, value: string) => {
90
+ if (!configData) return;
91
+ setConfigData({
92
+ ...configData,
93
+ brain: {
94
+ ...configData.brain,
95
+ [prop]: value
96
+ }
97
+ });
98
+ };
99
+
100
+ const AGENTS_LIST = [
101
+ { id: 'antigravity', name: 'Antigravity (Gemini SDK)', desc: 'Primary core developer agent' },
102
+ { id: 'gemini', name: 'Gemini CLI', desc: 'Direct Gemini assistant binary' },
103
+ { id: 'claude', name: 'Claude Code', desc: 'Anthropic developer CLI wrapper' },
104
+ { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' }
105
+ ];
106
+
107
+
108
+ return (
109
+ <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflowY: 'auto' }}>
110
+ <div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
111
+ <div>
112
+ <h1>Models & Agents</h1>
113
+ <p>Configure Bring Your Own Model (BYOM) settings and manage active reasoning agents</p>
114
+ </div>
115
+ {configData && (
116
+ <button
117
+ onClick={handleSaveConfig}
118
+ disabled={saving}
119
+ className="btn btn-primary"
120
+ style={{ minWidth: 120 }}
121
+ >
122
+ {saving ? 'Saving...' : 'Save Configuration'}
123
+ </button>
124
+ )}
125
+ </div>
126
+
127
+ {error && (
128
+ <div className="badge badge-error" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', color: '#f87171' }}>
129
+ ⚠️ {error}
130
+ </div>
131
+ )}
132
+ {success && (
133
+ <div className="badge badge-success" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(16, 185, 129, 0.1)', border: '1px solid #10b981', color: '#34d399' }}>
134
+ ✓ {success}
135
+ </div>
136
+ )}
137
+
138
+ {loading ? (
139
+ <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-secondary)' }}>
140
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite', marginBottom: 12 }}>
141
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
142
+ </svg>
143
+ <div>Loading models and agents...</div>
144
+ </div>
145
+ ) : (
146
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 24, flex: 1, minHeight: 0 }}>
147
+
148
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
149
+
150
+ {/* Ollama Panel */}
151
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
152
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
153
+ <span style={{ fontSize: 20 }}>🦙</span>
154
+ <div>
155
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>Local Provider</h3>
156
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Connect Ollama, LM Studio, vLLM, or Llama.cpp</p>
157
+ </div>
158
+ </div>
159
+
160
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
161
+ <label htmlFor="local_endpoint" style={{ fontSize: 13, fontWeight: 500 }}>Provider Base URL</label>
162
+ <input
163
+ id="local_endpoint"
164
+ type="text"
165
+ placeholder="http://127.0.0.1:11434 or http://127.0.0.1:1234/v1"
166
+ value={configData?.brain?.local_endpoint || ''}
167
+ onChange={(e) => updateBrainProp('local_endpoint', e.target.value)}
168
+ style={{
169
+ background: 'var(--bg-tertiary)',
170
+ color: 'var(--text-primary)',
171
+ border: '1px solid var(--border)',
172
+ padding: '8px 12px',
173
+ borderRadius: 6,
174
+ outline: 'none',
175
+ fontSize: 13
176
+ }}
177
+ />
178
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Leave blank to use the default localhost endpoint.</span>
179
+ </div>
180
+
181
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8 }}>
182
+ <span className="badge" style={{
183
+ background: health?.ollama === 'running' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
184
+ color: health?.ollama === 'running' ? '#10b981' : '#ef4444',
185
+ border: health?.ollama === 'running' ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)',
186
+ fontSize: 10,
187
+ padding: '4px 8px'
188
+ }}>
189
+ {health?.ollama === 'running' ? 'Connected' : 'Offline'}
190
+ </span>
191
+ {health?.ollama === 'running' && health?.ollama_models && (
192
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
193
+ {health.ollama_models.length} local models available
194
+ </span>
195
+ )}
196
+ </div>
197
+ </div>
198
+
199
+ {/* Cloud API Keys Panel */}
200
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
201
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
202
+ <span style={{ fontSize: 20 }}>🔑</span>
203
+ <div>
204
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>Cloud Models (API Keys)</h3>
205
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Bring your own cloud models</p>
206
+ </div>
207
+ </div>
208
+
209
+ {/* Google Gemini API Key */}
210
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
211
+ <label htmlFor="google_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Google Gemini API Key</label>
212
+ <input
213
+ id="google_api_key"
214
+ type="password"
215
+ placeholder="AIzaSy..."
216
+ value={configData?.secrets?.google_api_key || ''}
217
+ onChange={(e) => updateSecretsProp('google_api_key', e.target.value)}
218
+ style={{
219
+ background: 'var(--bg-tertiary)',
220
+ color: 'var(--text-primary)',
221
+ border: '1px solid var(--border)',
222
+ padding: '8px 12px',
223
+ borderRadius: 6,
224
+ outline: 'none',
225
+ fontSize: 13
226
+ }}
227
+ />
228
+ </div>
229
+
230
+ {/* Anthropic API Key */}
231
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
232
+ <label htmlFor="anthropic_api_key" style={{ fontSize: 13, fontWeight: 500 }}>Anthropic API Key (Claude)</label>
233
+ <input
234
+ id="anthropic_api_key"
235
+ type="password"
236
+ placeholder="sk-ant-..."
237
+ value={configData?.secrets?.anthropic_api_key || ''}
238
+ onChange={(e) => updateSecretsProp('anthropic_api_key', e.target.value)}
239
+ style={{
240
+ background: 'var(--bg-tertiary)',
241
+ color: 'var(--text-primary)',
242
+ border: '1px solid var(--border)',
243
+ padding: '8px 12px',
244
+ borderRadius: 6,
245
+ outline: 'none',
246
+ fontSize: 13
247
+ }}
248
+ />
249
+ </div>
250
+
251
+ {/* OpenAI API Key */}
252
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
253
+ <label htmlFor="openai_api_key" style={{ fontSize: 13, fontWeight: 500 }}>OpenAI API Key (Codex)</label>
254
+ <input
255
+ id="openai_api_key"
256
+ type="password"
257
+ placeholder="sk-proj-..."
258
+ value={configData?.secrets?.openai_api_key || ''}
259
+ onChange={(e) => updateSecretsProp('openai_api_key', e.target.value)}
260
+ style={{
261
+ background: 'var(--bg-tertiary)',
262
+ color: 'var(--text-primary)',
263
+ border: '1px solid var(--border)',
264
+ padding: '8px 12px',
265
+ borderRadius: 6,
266
+ outline: 'none',
267
+ fontSize: 13
268
+ }}
269
+ />
270
+ </div>
271
+
272
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>
273
+ These keys are stored locally and injected into the CLI reasoning agents on dispatch.
274
+ </span>
275
+ </div>
276
+
277
+ </div>
278
+
279
+ {/* CLI Agents Catalog & Diagnostics */}
280
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
281
+
282
+ {/* Catalog */}
283
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
284
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
285
+ <span style={{ fontSize: 20 }}>🧠</span>
286
+ <div>
287
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>CLI Reasoning Agents</h3>
288
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Headless CLI reasoning agents configured on system</p>
289
+ </div>
290
+ </div>
291
+
292
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
293
+ {AGENTS_LIST.map((a) => {
294
+ const activeAgent = health?.cli_agents?.includes(a.id);
295
+ return (
296
+ <div key={a.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
297
+ <div style={{ flex: 1 }}>
298
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
299
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{a.name}</span>
300
+ <span className="badge" style={{
301
+ fontSize: 9,
302
+ padding: '2px 6px',
303
+ background: activeAgent ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
304
+ color: activeAgent ? '#10b981' : '#ef4444',
305
+ border: activeAgent ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)'
306
+ }}>
307
+ {activeAgent ? 'available' : 'missing'}
308
+ </span>
309
+ </div>
310
+ <div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: 2 }}>{a.desc}</div>
311
+ <div style={{ fontSize: 9, color: 'var(--text-tertiary)', marginTop: 4 }}>Registry ID: {a.id}</div>
312
+ </div>
313
+ </div>
314
+ );
315
+ })}
316
+ </div>
317
+ </div>
318
+
319
+ {/* Diagnostics Form */}
320
+ <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 20 }}>
321
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
322
+ <span style={{ fontSize: 20 }}>📡</span>
323
+ <div>
324
+ <h3 style={{ fontSize: 14, fontWeight: 600 }}>CLI Agent Diagnostics</h3>
325
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Trigger active paths check and verify registered agents</p>
326
+ </div>
327
+ </div>
328
+
329
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
330
+ <p style={{ fontSize: 12, lineHeight: 1.5, color: 'var(--text-secondary)' }}>
331
+ This executes a diagnostic check on registered reasoning agents (equivalent to running <code>npx total-recall upgrade --agents</code> in the background) to audit paths and verify binaries.
332
+ </p>
333
+
334
+ <button
335
+ onClick={handleRunDiagnostics}
336
+ disabled={runningDiagnostics}
337
+ style={{
338
+ background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
339
+ color: '#fff',
340
+ padding: '10px',
341
+ borderRadius: 6,
342
+ fontWeight: 500,
343
+ border: 'none',
344
+ marginTop: 8,
345
+ cursor: runningDiagnostics ? 'not-allowed' : 'pointer'
346
+ }}
347
+ >
348
+ {runningDiagnostics ? '⏳ Running Audit...' : '🚀 Run Diagnostics Audit'}
349
+ </button>
350
+ </div>
351
+
352
+ {diagnosticLogs && (
353
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 10 }}>
354
+ <span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)' }}>Diagnostics Console Output:</span>
355
+ <pre style={{
356
+ background: '#07070a',
357
+ border: '1px solid var(--border)',
358
+ padding: 10,
359
+ borderRadius: 6,
360
+ fontFamily: 'var(--font-mono)',
361
+ fontSize: 11,
362
+ color: '#e6edf3',
363
+ whiteSpace: 'pre-wrap',
364
+ maxHeight: 150,
365
+ overflowY: 'auto'
366
+ }}>
367
+ {diagnosticLogs}
368
+ </pre>
369
+ </div>
370
+ )}
371
+ </div>
372
+
373
+ </div>
374
+
375
+ </div>
376
+ )}
377
+
378
+ </div>
379
+ );
380
+ }
@@ -47,6 +47,8 @@ export interface HealthData {
47
47
  ollama_models?: string[]
48
48
  cli_agents?: string[]
49
49
  daemon?: string
50
+ caddy?: string
51
+ cloudflare?: string
50
52
  emergency_alerts?: string | null
51
53
  }
52
54
 
@@ -158,6 +160,7 @@ export interface ConfigJson {
158
160
  }
159
161
  brain?: {
160
162
  preferred_agent?: string
163
+ local_endpoint?: string
161
164
  }
162
165
  secrets?: {
163
166
  google_api_key?: string
@@ -8,15 +8,15 @@ export default defineConfig({
8
8
  port: 5173,
9
9
  proxy: {
10
10
  '/v1': {
11
- target: 'http://localhost:3000',
11
+ target: 'http://127.0.0.1:3000',
12
12
  changeOrigin: true,
13
13
  },
14
14
  '/health': {
15
- target: 'http://localhost:3000',
15
+ target: 'http://127.0.0.1:3000',
16
16
  changeOrigin: true,
17
17
  },
18
18
  '/api': {
19
- target: 'http://localhost:3000',
19
+ target: 'http://127.0.0.1:3000',
20
20
  changeOrigin: true,
21
21
  },
22
22
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.6.3",
3
+ "version": "3.6.5",
4
4
  "description": "Sovereign AI system — database-free, SSSS-powered personal intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,11 +68,15 @@ function requiresLLM(task) {
68
68
  return false;
69
69
  }
70
70
 
71
- /**
72
- * Dispatch a task to the appropriate engine.
73
- * Only deterministic (non-LLM) tasks are executed.
74
- */
75
71
  async function dispatchTask(task) {
72
+ if (requiresLLM(task)) {
73
+ logger.info({
74
+ subsystem: 'daemon-loop',
75
+ message: `Skipping task (requires LLM): ${task.slug} [${task.category}]`,
76
+ });
77
+ return { success: true, skippedLLM: true, output: 'Skipped — handled by IDE agents' };
78
+ }
79
+
76
80
  const category = task.category;
77
81
 
78
82
  try {
@@ -85,13 +89,11 @@ async function dispatchTask(task) {
85
89
  return await runResearchTask(task);
86
90
 
87
91
  default:
88
- // All other categories (research, deliberation, inference, etc.)
89
- // require LLM and are handled by IDE agents, not the daemon.
90
92
  logger.info({
91
93
  subsystem: 'daemon-loop',
92
94
  message: `Skipping task (not deterministic): ${task.slug} [${category}]`,
93
95
  });
94
- return { success: true, output: 'Skipped — handled by IDE agents' };
96
+ return { success: true, skippedLLM: true, output: 'Skipped — handled by IDE agents' };
95
97
  }
96
98
  } catch (err) {
97
99
  logger.info({
@@ -309,7 +311,11 @@ async function main() {
309
311
  // Mark complete
310
312
  if ((source === 'explicit' || source === 'idle') && task._filepath) {
311
313
  try {
312
- updateTaskStatus(task, result.success ? 'completed' : 'failed', QUEUE_DIR);
314
+ if (result.skippedLLM) {
315
+ updateTaskStatus(task, 'pending', QUEUE_DIR);
316
+ } else {
317
+ updateTaskStatus(task, result.success ? 'completed' : 'failed', QUEUE_DIR);
318
+ }
313
319
  } catch (statusErr) {
314
320
  logger.info({
315
321
  subsystem: 'daemon-loop',
@@ -321,15 +327,22 @@ async function main() {
321
327
  try {
322
328
  const patch = {
323
329
  status: result.success ? 'pending' : 'failed',
324
- notes: result.output || result.error || null,
330
+ // Append the output rather than completely overwriting the original rationale
331
+ notes: result.output ? `Phase output: ${result.output}` : (result.error || null),
325
332
  };
326
333
  if (result.factSlug) {
327
334
  patch.node_slug = result.factSlug;
328
335
  }
329
- // Only advance through acquisition phase — skip LLM phases
330
- if (result.success && task._research_phase === 'acquisition' && result.factSlug) {
331
- // Research stays in acquisition — no LLM phases to advance to
332
- patch.status = 'done';
336
+ // If the task was skipped because it requires an LLM, DO NOT complete the research project.
337
+ // Wait for an IDE agent to run it.
338
+ if (result.skippedLLM) {
339
+ patch.status = 'pending';
340
+ } else if (result.success && task._research_phase === 'acquisition' && result.factSlug) {
341
+ // Acquisition is done, advance to deliberation phase for LLM processing!
342
+ patch.status = 'pending';
343
+ patch.research_phase = 'deliberation';
344
+ // Also notify the user/UI that phase 1 is complete!
345
+ patch.completed_at = new Date().toISOString();
333
346
  }
334
347
  updateQueueItem(task._research_id, patch);
335
348
  } catch (err) {
@@ -138,6 +138,23 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
138
138
  // Determine overall status
139
139
  const hasCriticalIssue = emergencyAlerts.length > 0 || daemonStatus === 'dead' || cliAgents.length === 0;
140
140
 
141
+ // Check Caddy and Cloudflare status
142
+ let caddyStatus = 'inactive';
143
+ let cloudflareStatus = 'inactive';
144
+ try {
145
+ const { exec } = await import('node:child_process');
146
+ const util = await import('node:util');
147
+ const execAsync = util.promisify(exec);
148
+ try {
149
+ await execAsync('pgrep caddy');
150
+ caddyStatus = 'active';
151
+ } catch { }
152
+ try {
153
+ await execAsync('pgrep cloudflared');
154
+ cloudflareStatus = 'active';
155
+ } catch { }
156
+ } catch { }
157
+
141
158
  res.json({
142
159
  status: hasCriticalIssue ? 'degraded' : 'healthy',
143
160
  version: PACKAGE_VERSION,
@@ -146,6 +163,8 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
146
163
  disk,
147
164
  cli_agents: cliAgents,
148
165
  daemon: daemonStatus,
166
+ caddy: caddyStatus,
167
+ cloudflare: cloudflareStatus,
149
168
  emergency_alerts: emergencyAlerts || null,
150
169
  vfs: {
151
170
  exists: vaultExists,
@@ -468,6 +468,25 @@ router.get('/api/vault/status', requireAuth, requireScope('memory:read'), async
468
468
  }
469
469
  });
470
470
 
471
+ /**
472
+ * POST /api/diagnostics/agents
473
+ * Run `upgrade --agents` diagnostics checks and return the console text output.
474
+ */
475
+ router.post('/api/diagnostics/agents', requireAuth, requireScope('health:read'), async (req, res) => {
476
+ try {
477
+ const result = spawnSync('node', [path.join(ROOT, 'bin', 'total-recall.mjs'), 'upgrade', '--agents'], {
478
+ encoding: 'utf8',
479
+ cwd: ROOT,
480
+ env: { ...process.env }
481
+ });
482
+ const output = (result.stdout || '') + (result.stderr || '');
483
+ res.json({ success: result.status === 0, output });
484
+ } catch (err) {
485
+ serverError(res, err);
486
+ }
487
+ });
488
+
489
+
471
490
  // ─── Keys ─────────── (moved to ./routes/keys.mjs)
472
491
  // ─── Sessions ─────── (moved to ./routes/sessions.mjs)
473
492
  // ─── Sandbox ──────────────────────────────────────────────────────────────────
@@ -67,6 +67,10 @@
67
67
  "method": "POST",
68
68
  "path": "/api/context/stream"
69
69
  },
70
+ {
71
+ "method": "POST",
72
+ "path": "/api/diagnostics/agents"
73
+ },
70
74
  {
71
75
  "method": "POST",
72
76
  "path": "/api/dream"
@@ -1,275 +0,0 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { fetchHealth, fetchLogs } from '../api';
3
- import type { HealthData } from '../types';
4
-
5
- export default function DeploymentsPage() {
6
- const [health, setHealth] = useState<HealthData | null>(null);
7
- const [logs, setLogs] = useState('(loading live cognitive activity console streams...)');
8
- const [loading, setLoading] = useState(true);
9
- const [error, setError] = useState<string | null>(null);
10
- const [success, setSuccess] = useState<string | null>(null);
11
-
12
- // Model Pull Form state
13
- const [modelToPull, setModelToPull] = useState('gemma4:26b');
14
- const [pulling, setPulling] = useState(false);
15
-
16
- const consoleEndRef = useRef<HTMLDivElement>(null);
17
-
18
- const fetchSystemData = async () => {
19
- try {
20
- const systemHealth = await fetchHealth();
21
- setHealth(systemHealth);
22
-
23
- const daemonLogs = await fetchLogs('daemon');
24
- setLogs(daemonLogs.content || '(no console activity streamed yet)');
25
- setError(null);
26
- } catch (err: unknown) {
27
- setError((err as Error).message || 'Failed to sync system health.');
28
- } finally {
29
- setLoading(false);
30
- }
31
- };
32
-
33
- useEffect(() => {
34
- // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate metrics polling on mount
35
- void fetchSystemData();
36
- const interval = setInterval(fetchSystemData, 4000);
37
- return () => clearInterval(interval);
38
- }, []);
39
-
40
- useEffect(() => {
41
- // Auto-scroll the retro console log to bottom
42
- if (consoleEndRef.current) {
43
- consoleEndRef.current.scrollIntoView({ behavior: 'smooth' });
44
- }
45
- }, [logs]);
46
-
47
- const handlePullModel = async (e: React.FormEvent) => {
48
- e.preventDefault();
49
- setPulling(true);
50
- setError(null);
51
- setSuccess(null);
52
- try {
53
- // Simulate/trigger model pull trigger enqueued in task manager
54
- setSuccess(`Model pull job successfully enqueued for "${modelToPull}" in the background task runner.`);
55
- setTimeout(() => setSuccess(null), 5000);
56
- } catch (err: unknown) {
57
- setError((err as Error).message || 'Model pull request failed.');
58
- } finally {
59
- setPulling(false);
60
- }
61
- };
62
-
63
- const MODELS_CATALOG = [
64
- { id: 'gemma4:26b', name: 'Gemma 4 (26B Parameter)', size: '15.6 GB', purpose: 'Standard Kernel LLM (Reasoning & Code Harness)', status: 'ready', type: 'ollama' },
65
- { id: 'gemma5:32b', name: 'Gemma 5 Pro (32B Parameter)', size: '19.2 GB', purpose: 'Advanced Reasoning & Deliberation Engine', status: 'available', type: 'ollama' },
66
- { id: 'text-embedding-004', name: 'Gemini Text Embeddings v4', size: 'Cloud-native', purpose: 'High-Dimensional Dense Vectorization', status: 'ready', type: 'gemini' },
67
- { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', size: 'Cloud-native', purpose: 'Frontier Advanced Programming', status: 'ready', type: 'claude' },
68
- ];
69
-
70
- return (
71
- <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflowY: 'auto' }}>
72
- <div className="page-header" style={{ marginBottom: 24 }}>
73
- <h1>Sovereign Stack Deployments</h1>
74
- <p>Monitor your machine services, pull kernel intelligence models, and stream activity logs</p>
75
- </div>
76
-
77
- {error && (
78
- <div className="badge badge-error" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', color: '#f87171' }}>
79
- ⚠️ {error}
80
- </div>
81
- )}
82
- {success && (
83
- <div className="badge badge-success" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(16, 185, 129, 0.1)', border: '1px solid #10b981', color: '#34d399' }}>
84
- ✓ {success}
85
- </div>
86
- )}
87
-
88
- {loading ? (
89
- <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-secondary)' }}>
90
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite', marginBottom: 12 }}>
91
- <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
92
- </svg>
93
- <div>Checking deployment gateways...</div>
94
- </div>
95
- ) : (
96
- <div style={{ display: 'flex', flexDirection: 'column', gap: 24, flex: 1, minHeight: 0 }}>
97
-
98
- {/* Services Status Panels Grid */}
99
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))', gap: 20 }}>
100
-
101
- {/* Service 1: Cognitive Daemon */}
102
- <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.6)', border: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
103
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
104
- <span style={{ fontSize: 24 }}>⚙️</span>
105
- <div>
106
- <h4 style={{ fontSize: 13, fontWeight: 600 }}>Reasoning Daemon</h4>
107
- <p style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>System 2 scheduler loop</p>
108
- </div>
109
- </div>
110
- <span className="badge" style={{
111
- background: health?.daemon === 'running' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
112
- color: health?.daemon === 'running' ? '#10b981' : '#ef4444',
113
- border: health?.daemon === 'running' ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)'
114
- }}>
115
- {health?.daemon || 'dead'}
116
- </span>
117
- </div>
118
-
119
- {/* Service 2: Caddy SSL Tunnel */}
120
- <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.6)', border: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
121
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
122
- <span style={{ fontSize: 24 }}>🔒</span>
123
- <div>
124
- <h4 style={{ fontSize: 13, fontWeight: 600 }}>Caddy SSL Proxy</h4>
125
- <p style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Reverse proxy tunnel gateway</p>
126
- </div>
127
- </div>
128
- <span className="badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: '#10b981', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
129
- active
130
- </span>
131
- </div>
132
-
133
- {/* Service 3: Cloudflare Secure Tunnel */}
134
- <div className="card" style={{ padding: 20, background: 'rgba(18, 18, 26, 0.6)', border: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
135
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
136
- <span style={{ fontSize: 24 }}>☁️</span>
137
- <div>
138
- <h4 style={{ fontSize: 13, fontWeight: 600 }}>Cloudflare Secure Tunnel</h4>
139
- <p style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Expose tunnel behind SSL</p>
140
- </div>
141
- </div>
142
- <span className="badge" style={{ background: 'rgba(16, 185, 129, 0.1)', color: '#10b981', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
143
- active
144
- </span>
145
- </div>
146
-
147
- </div>
148
-
149
- {/* Model Catalog & Pull form */}
150
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
151
-
152
- {/* Catalog */}
153
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 16 }}>
154
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
155
- <span style={{ fontSize: 20 }}>🧠</span>
156
- <div>
157
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Kernel LLM Catalog</h3>
158
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Provisioned local intelligence engines</p>
159
- </div>
160
- </div>
161
-
162
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
163
- {MODELS_CATALOG.map((m) => {
164
- const activeModel = health?.ollama_models?.includes(m.id) || m.type !== 'ollama';
165
- return (
166
- <div key={m.id} style={{ display: 'flex', alignItems: 'center', gap: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 8, border: '1px solid var(--border)' }}>
167
- <div style={{ flex: 1 }}>
168
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
169
- <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-primary)' }}>{m.name}</span>
170
- <span className="badge" style={{
171
- fontSize: 9,
172
- padding: '2px 6px',
173
- background: activeModel ? 'rgba(0, 206, 201, 0.1)' : 'rgba(255, 255, 255, 0.04)',
174
- color: activeModel ? '#00cec9' : 'var(--text-tertiary)',
175
- border: activeModel ? '1px solid rgba(0, 206, 201, 0.2)' : '1px solid var(--border)'
176
- }}>
177
- {activeModel ? 'active' : 'available'}
178
- </span>
179
- </div>
180
- <div style={{ fontSize: 10, color: 'var(--text-secondary)', marginTop: 2 }}>{m.purpose}</div>
181
- <div style={{ fontSize: 9, color: 'var(--text-tertiary)', marginTop: 4 }}>ID: {m.id} • Size: {m.size}</div>
182
- </div>
183
- </div>
184
- );
185
- })}
186
- </div>
187
- </div>
188
-
189
- {/* Pull Model Form */}
190
- <div className="card" style={{ padding: 24, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 20 }}>
191
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
192
- <span style={{ fontSize: 20 }}>📡</span>
193
- <div>
194
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Pull Catalog Model</h3>
195
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Trigger high-performance model downloads in background</p>
196
- </div>
197
- </div>
198
-
199
- <form onSubmit={handlePullModel} style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
200
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
201
- <label htmlFor="model_pull" style={{ fontSize: 12, fontWeight: 500 }}>Select Catalog Model</label>
202
- <select
203
- id="model_pull"
204
- value={modelToPull}
205
- onChange={(e) => setModelToPull(e.target.value)}
206
- style={{
207
- background: 'var(--bg-tertiary)',
208
- color: 'var(--text-primary)',
209
- border: '1px solid var(--border)',
210
- padding: '8px 12px',
211
- borderRadius: 6,
212
- outline: 'none',
213
- fontSize: 13
214
- }}
215
- >
216
- <option value="gemma4:26b">Gemma 4 (26B Parameter) — 15.6 GB</option>
217
- <option value="gemma5:32b">Gemma 5 Pro (32B Parameter) — 19.2 GB</option>
218
- <option value="llama3:70b">Llama 3 (70B Parameter) — 40 GB</option>
219
- </select>
220
- </div>
221
-
222
- <button
223
- type="submit"
224
- disabled={pulling}
225
- style={{
226
- background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
227
- color: '#fff',
228
- padding: '10px',
229
- borderRadius: 6,
230
- fontWeight: 500,
231
- border: 'none',
232
- marginTop: 8,
233
- cursor: pulling ? 'not-allowed' : 'pointer'
234
- }}
235
- >
236
- {pulling ? '⏳ Initiating download...' : '🚀 Trigger Model Download'}
237
- </button>
238
- </form>
239
- </div>
240
-
241
- </div>
242
-
243
- {/* Active Terminal Console Logs */}
244
- <div className="card" style={{ flex: 1, minHeight: 300, background: '#07070a', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', borderRadius: 10, overflow: 'hidden' }}>
245
- <div style={{ background: '#0e0e15', padding: '10px 16px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
246
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ff6b6b' }} />
247
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#fdcb6e' }} />
248
- <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#00cec9' }} />
249
- <span style={{ fontSize: 11, color: 'var(--text-tertiary)', fontFamily: 'var(--font-mono)', marginLeft: 8 }}>bash — daemon-activity.log — 80x24</span>
250
- </div>
251
-
252
- <pre style={{
253
- flex: 1,
254
- overflowY: 'auto',
255
- padding: 16,
256
- margin: 0,
257
- fontFamily: 'var(--font-mono)',
258
- fontSize: 12,
259
- color: '#34d399',
260
- background: '#07070a',
261
- lineHeight: 1.5,
262
- whiteSpace: 'pre-wrap',
263
- wordBreak: 'break-all'
264
- }}>
265
- {logs}
266
- <div ref={consoleEndRef} />
267
- </pre>
268
- </div>
269
-
270
- </div>
271
- )}
272
-
273
- </div>
274
- );
275
- }