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.
- package/frontend/src/App.tsx +5 -8
- package/frontend/src/api.ts +7 -0
- package/frontend/src/components/BrainSelector.tsx +10 -4
- package/frontend/src/index.css +1 -0
- package/frontend/src/pages/ChatPage.tsx +2 -0
- package/frontend/src/pages/CollabPage.tsx +7 -7
- package/frontend/src/pages/ModelsPage.tsx +380 -0
- package/frontend/src/types.ts +3 -0
- package/frontend/vite.config.ts +3 -3
- package/package.json +1 -1
- package/src/core/daemon-loop.mjs +26 -13
- package/src/server/index.mjs +19 -0
- package/src/server/rest.mjs +19 -0
- package/src/server/route-manifest.json +4 -0
- package/frontend/src/pages/DeploymentsPage.tsx +0 -275
package/frontend/src/App.tsx
CHANGED
|
@@ -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
|
|
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="/
|
|
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
|
-
<
|
|
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
|
-
|
|
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="/
|
|
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 />} />
|
package/frontend/src/api.ts
CHANGED
|
@@ -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
|
-
<
|
|
184
|
+
<div
|
|
185
185
|
key={brain.id}
|
|
186
|
-
onClick={(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
|
-
|
|
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
|
-
</
|
|
256
|
+
</div>
|
|
251
257
|
);
|
|
252
258
|
})}
|
|
253
259
|
</div>
|
package/frontend/src/index.css
CHANGED
|
@@ -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
|
|
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 [
|
|
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 (!
|
|
316
|
+
if (!activeBrowseUrl.trim()) return;
|
|
317
317
|
setChatMessages([]); setSystemLogs([]); setActiveUsers([]);
|
|
318
|
-
setActiveUrl(
|
|
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={
|
|
352
|
-
<button type="submit" className="btn btn-purple">
|
|
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
|
-
|
|
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
|
+
}
|
package/frontend/src/types.ts
CHANGED
|
@@ -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
|
package/frontend/vite.config.ts
CHANGED
|
@@ -8,15 +8,15 @@ export default defineConfig({
|
|
|
8
8
|
port: 5173,
|
|
9
9
|
proxy: {
|
|
10
10
|
'/v1': {
|
|
11
|
-
target: 'http://
|
|
11
|
+
target: 'http://127.0.0.1:3000',
|
|
12
12
|
changeOrigin: true,
|
|
13
13
|
},
|
|
14
14
|
'/health': {
|
|
15
|
-
target: 'http://
|
|
15
|
+
target: 'http://127.0.0.1:3000',
|
|
16
16
|
changeOrigin: true,
|
|
17
17
|
},
|
|
18
18
|
'/api': {
|
|
19
|
-
target: 'http://
|
|
19
|
+
target: 'http://127.0.0.1:3000',
|
|
20
20
|
changeOrigin: true,
|
|
21
21
|
},
|
|
22
22
|
},
|
package/package.json
CHANGED
package/src/core/daemon-loop.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
patch.status = '
|
|
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) {
|
package/src/server/index.mjs
CHANGED
|
@@ -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,
|
package/src/server/rest.mjs
CHANGED
|
@@ -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 ──────────────────────────────────────────────────────────────────
|
|
@@ -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
|
-
}
|