total-recall-brain 3.13.2 → 3.14.4

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 (63) hide show
  1. package/bin/total-recall.mjs +3 -0
  2. package/frontend/src/App.tsx +8 -7
  3. package/frontend/src/api.ts +25 -0
  4. package/frontend/src/components/UsageChart.tsx +1 -1
  5. package/frontend/src/pages/ApiKeysPage.tsx +118 -42
  6. package/frontend/src/pages/ChatPage.tsx +1 -1
  7. package/frontend/src/pages/DesignDocsPage.tsx +28 -21
  8. package/frontend/src/pages/FilesPage.tsx +6 -1
  9. package/frontend/src/pages/GraphPage.tsx +5 -1
  10. package/frontend/src/pages/HealthPage.tsx +1 -1
  11. package/frontend/src/pages/InboxPage.tsx +2 -1
  12. package/frontend/src/pages/InstructionsPage.tsx +26 -26
  13. package/frontend/src/pages/IntegrationsPage.tsx +11 -5
  14. package/frontend/src/pages/MemoryPage.tsx +27 -118
  15. package/frontend/src/pages/ModelsPage.tsx +2 -50
  16. package/frontend/src/pages/OnboardingPage.tsx +5 -10
  17. package/frontend/src/pages/OpenWikiPage.tsx +1 -0
  18. package/frontend/src/pages/SettingsPage.tsx +136 -131
  19. package/frontend/src/pages/SkillsPage.tsx +102 -46
  20. package/frontend/src/pages/TasksPage.tsx +60 -25
  21. package/frontend/src/pages/UsagePage.tsx +206 -95
  22. package/frontend/src/types.ts +17 -11
  23. package/frontend/src/utils/onboarding.ts +10 -0
  24. package/package.json +3 -2
  25. package/scaffold/.agent/skills/ssss/SKILL.md +235 -0
  26. package/scaffold/.agent/skills/ssss/evals/evals.json +37 -0
  27. package/scaffold/.agent/skills/ssss/references/contract-reference.md +95 -0
  28. package/scaffold/.agent/skills/ssss/scripts/validate-schema.mjs +215 -0
  29. package/scaffold/.agent/skills/ssss/subagents/audit-ssss-contract.md +22 -0
  30. package/scaffold/.agent/skills/total-recall/SKILL.md +1 -0
  31. package/src/cli/backup.mjs +3 -3
  32. package/src/cli/chat.mjs +2 -2
  33. package/src/cli/config.mjs +2 -9
  34. package/src/cli/connect.mjs +1 -1
  35. package/src/cli/deploy-ui.mjs +1 -1
  36. package/src/cli/deploy.mjs +2 -2
  37. package/src/cli/doctor.mjs +1 -1
  38. package/src/cli/help.mjs +1 -1
  39. package/src/cli/init.mjs +19 -1
  40. package/src/cli/key.mjs +150 -0
  41. package/src/cli/secret.mjs +30 -1
  42. package/src/cli/skill.mjs +0 -2
  43. package/src/cli/sync.mjs +19 -0
  44. package/src/cli/upgrade.mjs +1 -1
  45. package/src/cli/wizard.html +5 -5
  46. package/src/core/crons.mjs +11 -0
  47. package/src/core/daemon-loop.mjs +11 -0
  48. package/src/core/import-rules.mjs +4 -3
  49. package/src/core/okf-adapter.mjs +10 -10
  50. package/src/core/runtime.mjs +0 -2
  51. package/src/core/secrets-store.mjs +16 -0
  52. package/src/core/skills-registry.mjs +38 -0
  53. package/src/core/surface.mjs +1 -1
  54. package/src/core/task_runner.mjs +6 -0
  55. package/src/core/usage-tracker.mjs +16 -3
  56. package/src/core/vault-watcher.mjs +92 -0
  57. package/src/core/vault.mjs +0 -2
  58. package/src/server/api.mjs +1 -1
  59. package/src/server/rest.mjs +69 -57
  60. package/src/server/route-manifest.json +8 -0
  61. package/src/server/routes/_shared.mjs +1 -1
  62. package/src/server/routes/secrets.mjs +4 -4
  63. package/src/server/routes/skills.mjs +92 -10
@@ -62,6 +62,9 @@ const COMMANDS = {
62
62
  task: 'task.mjs',
63
63
  share: 'share.mjs',
64
64
  'generate-pat': 'generate-pat.mjs',
65
+ key: 'key.mjs',
66
+ keys: 'key.mjs',
67
+ pat: 'key.mjs',
65
68
  'hash-password': 'hash-password.mjs',
66
69
  'reset-password': 'reset-password.mjs',
67
70
  compile: 'rebuild.mjs',
@@ -15,7 +15,8 @@ import SkillsPage from './pages/SkillsPage'
15
15
  import HelpPage from './pages/HelpPage'
16
16
  import GraphPage from './pages/GraphPage'
17
17
  import OpenWikiPage from './pages/OpenWikiPage'
18
- import OnboardingPage, { isOnboardingComplete } from './pages/OnboardingPage'
18
+ import OnboardingPage from './pages/OnboardingPage'
19
+ import { isOnboardingComplete } from './utils/onboarding'
19
20
  import BrandMark from './components/brand/BrandMark'
20
21
 
21
22
  // ─── Auth state type ──────────────────────────────────────────────────────────
@@ -122,7 +123,7 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
122
123
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
123
124
  <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
124
125
  </svg>
125
- Secrets
126
+ Keys & Usage
126
127
  </NavLink>
127
128
  <NavLink to="/settings" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-settings">
128
129
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
@@ -299,13 +300,13 @@ function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string;
299
300
  <Route path="/onboarding" element={<OnboardingPage />} />
300
301
  <Route path="/memory" element={<MemoryPage activeBrainId={activeBrainId} />} />
301
302
  <Route path="/graph" element={<GraphPage activeBrainId={activeBrainId} />} />
302
- <Route path="/tasks" element={<TasksPage />} />
303
- <Route path="/skills" element={<SkillsPage />} />
303
+ <Route path="/tasks" element={<TasksPage activeBrainId={activeBrainId} />} />
304
+ <Route path="/skills" element={<SkillsPage activeBrainId={activeBrainId} />} />
304
305
  <Route path="/openwiki" element={<OpenWikiPage activeBrainId={activeBrainId} />} />
305
- <Route path="/integrations" element={<IntegrationsPage />} />
306
+ <Route path="/integrations" element={<IntegrationsPage activeBrainId={activeBrainId} />} />
306
307
  <Route path="/keys" element={<ApiKeysPage />} />
307
- <Route path="/settings" element={<SettingsPage />} />
308
- <Route path="/help" element={<HelpPage />} />
308
+ <Route path="/settings" element={<SettingsPage activeBrainId={activeBrainId} />} />
309
+ <Route path="/help" element={<HelpPage activeBrainId={activeBrainId} />} />
309
310
  {/* Legacy OS-control-plane routes → core product */}
310
311
  <Route path="/vault" element={<Navigate to="/memory" replace />} />
311
312
  <Route path="/inbox" element={<Navigate to="/tasks" replace />} />
@@ -453,6 +453,11 @@ export async function fetchConfigJson(): Promise<ConfigJson> {
453
453
  return res.json()
454
454
  }
455
455
 
456
+ export async function fetchBrains(): Promise<Record<string, unknown>[]> {
457
+ const res = await apiFetch(`${API_BASE}/api/brains`)
458
+ return res.json()
459
+ }
460
+
456
461
  export async function saveConfigJson(config: ConfigJson): Promise<void> {
457
462
  const res = await apiFetch(`${API_BASE}/api/config-json`, {
458
463
  method: 'POST',
@@ -1414,3 +1419,23 @@ export async function postDecision(id: string, action: string, notes?: string):
1414
1419
  }
1415
1420
  return res.json()
1416
1421
  }
1422
+
1423
+ export async function toggleSkillRepo(skillName: string, targetRepo: string, enabled: boolean): Promise<{ success: boolean; message: string }> {
1424
+ const res = await apiFetch(`${API_BASE}/api/skills/toggle`, {
1425
+ method: 'POST',
1426
+ headers: { 'Content-Type': 'application/json' },
1427
+ body: JSON.stringify({ skillName, targetRepo, enabled })
1428
+ });
1429
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
1430
+ return res.json();
1431
+ }
1432
+
1433
+ export async function auditSkill(skillName: string, repoPath?: string): Promise<{ success: boolean; audit: { outdated: boolean; problematic: boolean; reason: string; proposed_changes: string } }> {
1434
+ const res = await apiFetch(`${API_BASE}/api/skills/audit`, {
1435
+ method: 'POST',
1436
+ headers: { 'Content-Type': 'application/json' },
1437
+ body: JSON.stringify({ skillName, repoPath })
1438
+ });
1439
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
1440
+ return res.json();
1441
+ }
@@ -63,7 +63,7 @@ export const UsageChart: React.FC<UsageChartProps> = ({ usageData }) => {
63
63
  <Tooltip
64
64
  contentStyle={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
65
65
  itemStyle={{ fontSize: 12 }}
66
- formatter={(value: any) => [`$${Number(value).toFixed(4)}`, undefined]}
66
+ formatter={(value: number | string) => [`$${Number(value).toFixed(4)}`, undefined]}
67
67
  />
68
68
  <Legend wrapperStyle={{ fontSize: 11 }} />
69
69
  <Bar isAnimationActive={false} dataKey="gemini" name="Gemini" stackId="a" fill="#10b981" radius={[0,0,0,0]} />
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useCallback, type CSSProperties, type ReactNode } from 'react'
1
+ import { useState, useEffect, useCallback, useMemo, type CSSProperties, type ReactNode } from 'react'
2
2
  import {
3
3
  listApiKeys,
4
4
  issueApiKey,
@@ -32,8 +32,9 @@ import {
32
32
  } from '../api'
33
33
  import type { ConfigJson, GeminiModelInfo } from '../types'
34
34
  import { startRegistration, startAuthentication, browserSupportsWebAuthn } from '@simplewebauthn/browser'
35
+ import UsagePage from './UsagePage'
35
36
 
36
- type Tab = 'catalog' | 'pats' | 'import' | 'cloud'
37
+ type Tab = 'catalog' | 'pats' | 'import' | 'cloud' | 'usage'
37
38
 
38
39
  /** Form state — string inputs; converted on save */
39
40
  type MetaEdit = {
@@ -50,6 +51,46 @@ type MetaEdit = {
50
51
  project_path: string
51
52
  }
52
53
 
54
+ function metaForm(k: SecretCatalogKey): MetaEdit {
55
+ return {
56
+ label: k.label || '',
57
+ provider: k.provider || '',
58
+ repos: (k.repos || []).join(', '),
59
+ subscription_tier: k.subscription_tier || '',
60
+ monthly_cost_usd: k.monthly_cost_usd != null ? String(k.monthly_cost_usd) : '',
61
+ monthly_cap_usd: k.monthly_cap_usd != null ? String(k.monthly_cap_usd) : '',
62
+ api_docs_url: k.api_docs_url || '',
63
+ rotate_every_days: k.rotate_every_days != null ? String(k.rotate_every_days) : '',
64
+ auto_rotate: !!k.auto_rotate,
65
+ notes: k.notes || '',
66
+ project_path: k.project_path || '',
67
+ }
68
+ }
69
+
70
+ function groupKeysByProvider(keys: SecretCatalogKey[]) {
71
+ const groups = new Map<string, { id: string; label: string; kind: 'repo'; keys: SecretCatalogKey[] }>()
72
+
73
+ keys.forEach((k) => {
74
+ const provider = (k.provider || 'unknown').toLowerCase()
75
+ if (!groups.has(provider)) {
76
+ groups.set(provider, { id: provider, label: provider.toUpperCase(), kind: 'repo', keys: [] })
77
+ }
78
+ groups.get(provider)!.keys.push(k)
79
+ })
80
+
81
+ return [...groups.values()].sort((a, b) => a.id.localeCompare(b.id))
82
+ }
83
+
84
+ function ProviderLogo({ provider }: { provider?: string }) {
85
+ if (!provider) return null;
86
+ const p = provider.toLowerCase();
87
+ if (p === 'openai') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0462 6.0462 0 0 0 5.3-3.71 6.0415 6.0415 0 0 0 4.4646-6.4463A5.9847 5.9847 0 0 0 22.282 9.8211Z"/></svg>;
88
+ if (p === 'anthropic') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M17.433 2.115L15.939 12h3.048l1.494-9.885h-3.048zM14.616 2.115l-1.392 9.885h3.048l1.392-9.885h-3.048zM10.371 14.542l-5.637 7.343H7.78l3.966-5.166 2.766 5.166h3.018l-7.159-12.836-5.836 12.836h3.043l1.838-4.043h4.636l-3.681-3.3z"/></svg>;
89
+ if (p === 'google') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>;
90
+ if (p === 'github') return <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd"/></svg>;
91
+ return null;
92
+ }
93
+
53
94
  export default function ApiKeysPage() {
54
95
  const [tab, setTab] = useState<Tab>('catalog')
55
96
 
@@ -64,6 +105,7 @@ export default function ApiKeysPage() {
64
105
  const [loading, setLoading] = useState(true)
65
106
  const [error, setError] = useState<string | null>(null)
66
107
  const [filter, setFilter] = useState('')
108
+ const [groupBy, setGroupBy] = useState<'repo' | 'api'>('repo')
67
109
  const [selected, setSelected] = useState<SecretCatalogKey | null>(null)
68
110
  const [edit, setEdit] = useState<MetaEdit>({
69
111
  label: '',
@@ -122,9 +164,11 @@ export default function ApiKeysPage() {
122
164
  setOpenaiModels(open)
123
165
  const or = await fetchOpenRouterModels().catch(() => [])
124
166
  setOrModels(or)
125
- } catch(e) {}
167
+ } catch {
168
+ // ignore fetching models error
169
+ }
126
170
 
127
- const data = await fetchSecretsCatalog()
171
+ const data = await fetchSecretsCatalog()
128
172
  setCatalog(data)
129
173
  if (selected) {
130
174
  const fresh = data.keys.find((k) => k.key === selected.key)
@@ -136,7 +180,7 @@ export default function ApiKeysPage() {
136
180
  } finally {
137
181
  setLoading(false)
138
182
  }
139
- }, [selected?.key])
183
+ }, [selected])
140
184
 
141
185
  const loadPats = useCallback(async () => {
142
186
  try {
@@ -156,10 +200,13 @@ export default function ApiKeysPage() {
156
200
  }, [])
157
201
 
158
202
  useEffect(() => {
203
+ // eslint-disable-next-line
159
204
  void loadCatalog()
205
+
160
206
  void loadPats()
207
+
161
208
  void loadWebAuthn()
162
- }, [])
209
+ }, [loadCatalog, loadPats, loadWebAuthn])
163
210
 
164
211
 
165
212
  const updateSecretsProp = (prop: string, value: string) => {
@@ -194,21 +241,7 @@ export default function ApiKeysPage() {
194
241
  }
195
242
  };
196
243
 
197
- function metaForm(k: SecretCatalogKey): MetaEdit {
198
- return {
199
- label: k.label || '',
200
- provider: k.provider || '',
201
- repos: (k.repos || []).join(', '),
202
- subscription_tier: k.subscription_tier || '',
203
- monthly_cost_usd: k.monthly_cost_usd != null ? String(k.monthly_cost_usd) : '',
204
- monthly_cap_usd: k.monthly_cap_usd != null ? String(k.monthly_cap_usd) : '',
205
- api_docs_url: k.api_docs_url || '',
206
- rotate_every_days: k.rotate_every_days != null ? String(k.rotate_every_days) : '',
207
- auto_rotate: !!k.auto_rotate,
208
- notes: k.notes || '',
209
- project_path: k.project_path || '',
210
- }
211
- }
244
+ // metaForm hoisted
212
245
 
213
246
  function openKey(k: SecretCatalogKey, sectionId?: string) {
214
247
  setSelected(k)
@@ -437,7 +470,18 @@ export default function ApiKeysPage() {
437
470
  })
438
471
 
439
472
  /** Group filtered keys into repo sections (one key → one repo; multi = error; none = developer). */
440
- const repoSections = groupKeysByRepo(keys)
473
+ const repoSections = useMemo<{ id: string; label: string; kind: 'error' | 'developer' | 'repo'; keys: SecretCatalogKey[] }[]>(() => {
474
+ if (!catalog?.keys) return []
475
+ const f = filter.toLowerCase()
476
+ const filtered = catalog.keys.filter(
477
+ (k) =>
478
+ k.key.toLowerCase().includes(f) ||
479
+ (k.provider || '').toLowerCase().includes(f) ||
480
+ (k.repos || []).some((r) => r.toLowerCase().includes(f)) ||
481
+ (k.label || '').toLowerCase().includes(f)
482
+ )
483
+ return groupBy === 'api' ? groupKeysByProvider(filtered) : groupKeysByRepo(filtered)
484
+ }, [catalog, filter, groupBy])
441
485
 
442
486
  // Default accordion: open errors + first product/developer section only
443
487
  useEffect(() => {
@@ -448,7 +492,9 @@ export default function ApiKeysPage() {
448
492
  }
449
493
  const first = repoSections.find((s) => s.kind !== 'error')
450
494
  if (first) next.add(first.id)
495
+ // eslint-disable-next-line
451
496
  setExpandedSections(next)
497
+
452
498
  setSectionsInited(true)
453
499
  }, [repoSections, sectionsInited])
454
500
 
@@ -707,6 +753,12 @@ export default function ApiKeysPage() {
707
753
  ))}
708
754
  </div>
709
755
 
756
+ {tab === 'usage' && (
757
+ <div style={{ height: 'calc(100vh - 280px)', minHeight: 420, position: 'relative', overflowY: 'auto' }}>
758
+ <UsagePage />
759
+ </div>
760
+ )}
761
+
710
762
  {tab === 'catalog' && (
711
763
  <div
712
764
  style={{
@@ -722,13 +774,34 @@ export default function ApiKeysPage() {
722
774
  <div
723
775
  style={{
724
776
  display: 'flex',
725
- gap: 8,
726
- flexWrap: 'wrap',
777
+ gap: 12,
727
778
  alignItems: 'center',
728
- marginBottom: 8,
729
- flexShrink: 0,
779
+ marginBottom: 16,
780
+ flexWrap: 'wrap',
730
781
  }}
731
782
  >
783
+ <div style={{ display: 'flex', background: 'var(--bg-tertiary)', borderRadius: 6, padding: 2 }}>
784
+ <button
785
+ onClick={() => setGroupBy('repo')}
786
+ style={{
787
+ padding: '4px 10px', fontSize: 11, fontWeight: 500, borderRadius: 4, cursor: 'pointer', border: 'none',
788
+ background: groupBy === 'repo' ? 'var(--bg-active)' : 'transparent',
789
+ color: groupBy === 'repo' ? 'var(--text-primary)' : 'var(--text-secondary)'
790
+ }}
791
+ >
792
+ By Repo
793
+ </button>
794
+ <button
795
+ onClick={() => setGroupBy('api')}
796
+ style={{
797
+ padding: '4px 10px', fontSize: 11, fontWeight: 500, borderRadius: 4, cursor: 'pointer', border: 'none',
798
+ background: groupBy === 'api' ? 'var(--bg-active)' : 'transparent',
799
+ color: groupBy === 'api' ? 'var(--text-primary)' : 'var(--text-secondary)'
800
+ }}
801
+ >
802
+ By API
803
+ </button>
804
+ </div>
732
805
  <input
733
806
  value={filter}
734
807
  onChange={(e) => setFilter(e.target.value)}
@@ -741,9 +814,21 @@ export default function ApiKeysPage() {
741
814
  border: '1px solid var(--border)',
742
815
  background: 'var(--bg-primary)',
743
816
  color: 'var(--text-primary)',
744
- fontSize: 13,
817
+ outline: 'none',
745
818
  }}
746
819
  />
820
+ </div>
821
+
822
+ <div
823
+ style={{
824
+ display: 'flex',
825
+ gap: 8,
826
+ flexWrap: 'wrap',
827
+ alignItems: 'center',
828
+ marginBottom: 8,
829
+ flexShrink: 0,
830
+ }}
831
+ >
747
832
  <button
748
833
  type="button"
749
834
  className="btn btn-ghost btn-sm"
@@ -801,6 +886,7 @@ export default function ApiKeysPage() {
801
886
  title={open ? 'Currently expanded' : 'Expand this repo only'}
802
887
  >
803
888
  {open ? '▾ ' : '▸ '}
889
+ {groupBy === 'api' && <ProviderLogo provider={sec.id} />}
804
890
  {sec.label}
805
891
  <span style={{ marginLeft: 6, opacity: 0.7 }}>{sec.keys.length}</span>
806
892
  </button>
@@ -873,7 +959,7 @@ export default function ApiKeysPage() {
873
959
  color: sec.kind === 'error' ? '#f87171' : 'var(--text-primary)',
874
960
  }}
875
961
  >
876
- {open ? '▾' : '▸'} {sec.label}
962
+ {open ? '▾' : '▸'} {groupBy === 'api' && <ProviderLogo provider={sec.id} />} {sec.label}
877
963
  </div>
878
964
  <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 2 }}>
879
965
  {sec.kind === 'error'
@@ -1367,7 +1453,6 @@ export default function ApiKeysPage() {
1367
1453
  ))}
1368
1454
  </div>
1369
1455
  )}
1370
-
1371
1456
  {tab === 'cloud' && (
1372
1457
  <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
1373
1458
  <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
@@ -1380,7 +1465,7 @@ export default function ApiKeysPage() {
1380
1465
  ✓ {configSuccess}
1381
1466
  </div>
1382
1467
  )}
1383
- <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 }}>
1468
+ <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 }}>
1384
1469
  <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
1385
1470
  <span style={{ fontSize: 20 }}>🔑</span>
1386
1471
  <div>
@@ -1673,24 +1758,15 @@ export default function ApiKeysPage() {
1673
1758
  )}
1674
1759
  </div>
1675
1760
 
1676
-
1677
- {/* Search & Tool APIs */}
1678
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6, paddingBottom: 16, borderBottom: '1px solid rgba(255,255,255,0.05)', marginTop: 16 }}>
1679
- <h4 style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 8 }}>Search & External Tools</h4>
1680
- {['tavily_api_key', 'brave_api_key', 'exa_api_key', 'serper_api_key', 'github_token'].map(key => (
1681
- <div key={key} style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 8 }}>
1682
- <label style={{ fontSize: 12, fontWeight: 500 }}>{key.replace(/_/g, ' ').replace('api', 'API').replace('key', 'Key').replace('token', 'Token').replace(/\\b\\w/g, c => c.toUpperCase())}</label>
1683
- <input type="password" placeholder="Enter token..." value={(configData?.secrets as any)?.[key] || ''} onChange={(e) => updateSecretsProp(key, e.target.value)} style={{ background: 'var(--bg-tertiary)', color: 'var(--text-primary)', border: '1px solid var(--border)', padding: '8px 12px', borderRadius: 6, outline: 'none', fontSize: 13 }} />
1684
- </div>
1685
- ))}
1686
- </div>
1687
1761
  <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>
1688
1762
  These keys are stored locally and injected into the CLI reasoning agents on dispatch.
1689
1763
  </span>
1690
- </div>
1764
+ </div>
1691
1765
  </div>
1692
1766
  )}
1693
1767
 
1768
+
1769
+
1694
1770
  <p style={{ marginTop: 24, fontSize: 11, color: 'var(--text-tertiary)' }}>
1695
1771
  CLI: <code>secret catalog</code> · <code>secret meta KEY --repo app --tier pro --monthly-cost 25 --rotate-days 90</code> ·{' '}
1696
1772
  <code>secret rotation-due</code> · <code>secret usage</code> · <code>secret providers</code>
@@ -210,7 +210,7 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
210
210
  ) {
211
211
  loadMemoryNodes()
212
212
  }
213
- } catch (e) {
213
+ } catch {
214
214
  // ignore parsing errors from stream
215
215
  }
216
216
  }
@@ -27,17 +27,6 @@ interface KanbanColumn {
27
27
 
28
28
  // ─── Static Data ────────────────────────────────────────────────────────────────
29
29
 
30
- const CORE_DOCS: DocItem[] = [
31
- { name: 'ARCHITECTURE.md', path: 'docs/ARCHITECTURE.md', icon: '🏗️', category: 'Core Docs' },
32
- { name: 'SSSS.md', path: 'docs/SSSS.md', icon: '🔐', category: 'Core Docs' },
33
- { name: 'IDE_INTEGRATION.md', path: 'docs/IDE_INTEGRATION.md', icon: '🔌', category: 'Core Docs' },
34
- { name: 'CHANGELOG.md', path: 'docs/CHANGELOG.md', icon: '📋', category: 'Core Docs' },
35
- ];
36
-
37
- const DEV_GUIDES: DocItem[] = [
38
- { name: 'README.md', path: 'README.md', icon: '📖', category: 'Developer Guides' },
39
- ];
40
-
41
30
  const KANBAN_COLUMNS: KanbanColumn[] = [
42
31
  {
43
32
  id: 'in-progress',
@@ -173,11 +162,11 @@ export default function DesignDocsPage() {
173
162
  const [activeTab, setActiveTab] = useState<'architecture' | 'board'>('architecture');
174
163
 
175
164
  // Architecture docs state
176
- const [selectedDoc, setSelectedDoc] = useState<DocItem>(CORE_DOCS[0]);
165
+ const [selectedDoc, setSelectedDoc] = useState<DocItem | null>(null);
177
166
  const [docContent, setDocContent] = useState<string>('');
178
167
  const [docLoading, setDocLoading] = useState(false);
179
168
  const [docError, setDocError] = useState<string | null>(null);
180
- const [, setDocsFromApi] = useState<Array<{ name: string; path: string; size: number; modified: string; category: string }>>([]);
169
+ const [docsFromApi, setDocsFromApi] = useState<DocItem[]>([]);
181
170
 
182
171
  // Kanban state
183
172
  const [expandedColumns, setExpandedColumns] = useState<Record<string, boolean>>({});
@@ -187,9 +176,18 @@ export default function DesignDocsPage() {
187
176
  async function loadDocsList() {
188
177
  try {
189
178
  const data = await fetchDesignDocs();
190
- setDocsFromApi(data);
179
+ const docItems: DocItem[] = data.map(d => ({
180
+ name: d.name,
181
+ path: d.path,
182
+ category: d.category || 'Other',
183
+ icon: '📄',
184
+ }));
185
+ setDocsFromApi(docItems);
186
+ if (docItems.length > 0) {
187
+ setSelectedDoc(docItems[0]);
188
+ }
191
189
  } catch {
192
- // Silently fall back to static doc list
190
+ // Silently fall back
193
191
  }
194
192
  }
195
193
  loadDocsList();
@@ -204,11 +202,11 @@ export default function DesignDocsPage() {
204
202
  setDocLoading(true);
205
203
  setDocError(null);
206
204
  try {
207
- const data = await fetchDesignDocContent(selectedDoc.path);
205
+ const data = await fetchDesignDocContent(selectedDoc!.path);
208
206
  if (!cancelled) setDocContent(data.content);
209
207
  } catch {
210
208
  if (!cancelled) {
211
- setDocError(selectedDoc.path);
209
+ setDocError(selectedDoc!.path);
212
210
  setDocContent('');
213
211
  }
214
212
  } finally {
@@ -241,7 +239,7 @@ export default function DesignDocsPage() {
241
239
  {title}
242
240
  </div>
243
241
  {items.map(item => {
244
- const isActive = selectedDoc.path === item.path;
242
+ const isActive = selectedDoc?.path === item.path;
245
243
  return (
246
244
  <button
247
245
  id={`doc-nav-${item.name.replace(/\./g, '-').toLowerCase()}`}
@@ -294,8 +292,17 @@ export default function DesignDocsPage() {
294
292
  overflowY: 'auto',
295
293
  }}
296
294
  >
297
- {renderSidebarSection('Core Docs', CORE_DOCS)}
298
- {renderSidebarSection('Developer Guides', DEV_GUIDES)}
295
+ {Object.entries(
296
+ docsFromApi.reduce((acc, doc) => {
297
+ acc[doc.category] = acc[doc.category] || [];
298
+ acc[doc.category].push(doc);
299
+ return acc;
300
+ }, {} as Record<string, DocItem[]>)
301
+ ).map(([category, items]) => (
302
+ <React.Fragment key={category}>
303
+ {renderSidebarSection(category, items)}
304
+ </React.Fragment>
305
+ ))}
299
306
  </div>
300
307
 
301
308
  {/* Main Viewer */}
@@ -328,7 +335,7 @@ export default function DesignDocsPage() {
328
335
  <span style={{ color: 'var(--text-tertiary)' }}>›</span>
329
336
  <span>docs</span>
330
337
  <span style={{ color: 'var(--text-tertiary)' }}>›</span>
331
- <span style={{ color: 'var(--accent)' }}>{selectedDoc.name}</span>
338
+ <span style={{ color: 'var(--accent)' }}>{selectedDoc?.name}</span>
332
339
  </div>
333
340
 
334
341
  {/* Content */}
@@ -134,7 +134,12 @@ export default function FilesPage() {
134
134
  setNewScriptName("")
135
135
  setIsCreatingScript(false)
136
136
  await fetchScriptsList()
137
- await handleSelectScript(name)
137
+
138
+ // Yield to the event loop to ensure React state (scripts list)
139
+ // is fully committed to the DOM before selecting it.
140
+ setTimeout(() => {
141
+ handleSelectScript(name).catch(console.error)
142
+ }, 0)
138
143
  } catch (e) {
139
144
  setError(`Failed to create script: ${(e as Error).message}`)
140
145
  } finally {
@@ -16,7 +16,7 @@ export default function GraphPage({ activeBrainId }: { activeBrainId?: string })
16
16
  setLoading(true)
17
17
  try {
18
18
  const [nodes, researchRes, threadsRes] = await Promise.all([
19
- listMemory(activeBrainId),
19
+ listMemory(activeBrainId).catch(() => []),
20
20
  listResearch().catch(() => ({ items: [] })),
21
21
  fetchChatThreads().catch(() => [])
22
22
  ])
@@ -44,6 +44,10 @@ export default function GraphPage({ activeBrainId }: { activeBrainId?: string })
44
44
  <div style={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
45
45
  {loading ? (
46
46
  <div className="skeleton" style={{ width: '100%', height: '100%' }} />
47
+ ) : memoryNodes.length === 0 && researchItems.length === 0 && threads.length === 0 ? (
48
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-secondary)' }}>
49
+ No nodes found for graph
50
+ </div>
47
51
  ) : (
48
52
  <Graph3D
49
53
  threads={threads}
@@ -186,7 +186,7 @@ export default function HealthPage() {
186
186
  <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
187
187
  <div>
188
188
  <h1>System Health</h1>
189
- <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
189
+ <p>Live monitoring of your memory brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
190
190
  </div>
191
191
  {updateInfo?.updateAvailable ? (
192
192
  <div className="badge badge-success" style={{
@@ -25,6 +25,7 @@ export default function InboxPage({ activeBrainId }: { activeBrainId?: string })
25
25
  }, [loadData])
26
26
 
27
27
  const handleDecision = async (path: string, action: string) => {
28
+ setError("")
28
29
  try {
29
30
  const doc = await readDoc(path, activeBrainId)
30
31
  // Extract proposal ID from path (e.g. proposals/p2.md -> p2)
@@ -40,7 +41,7 @@ export default function InboxPage({ activeBrainId }: { activeBrainId?: string })
40
41
  }
41
42
  loadData()
42
43
  } catch (err: unknown) {
43
- alert("Decision failed: " + (err instanceof Error ? err.message : "Unknown error"))
44
+ setError("Decision failed: " + (err instanceof Error ? err.message : "Unknown error"))
44
45
  }
45
46
  }
46
47
 
@@ -194,7 +194,29 @@ export default function InstructionsPage() {
194
194
 
195
195
  // ─── Data Loading ─────────────────────────────────────────────────────────────
196
196
 
197
- const loadSurfaces = async () => {
197
+ const applyFallback = useCallback(() => {
198
+ setSurfaces(FALLBACK_SURFACES)
199
+ setIsFallback(true)
200
+ setLastCompiled('')
201
+ setTotalNodes(0)
202
+ const first = FALLBACK_SURFACES[0]
203
+ setSelected(first)
204
+ setContent('Content unavailable — recompile to regenerate')
205
+ }, [])
206
+
207
+ const loadContent = useCallback(async (name: string) => {
208
+ setContentLoading(true)
209
+ try {
210
+ const data = await fetchInstructionContent(name)
211
+ setContent(data.content || 'Empty surface — no compiled content yet.')
212
+ } catch {
213
+ setContent('Content unavailable — recompile to regenerate')
214
+ } finally {
215
+ setContentLoading(false)
216
+ }
217
+ }, [])
218
+
219
+ const loadSurfaces = useCallback(async () => {
198
220
  setLoading(true)
199
221
  setError(null)
200
222
  try {
@@ -220,29 +242,7 @@ export default function InstructionsPage() {
220
242
  } finally {
221
243
  setLoading(false)
222
244
  }
223
- }
224
-
225
- const applyFallback = () => {
226
- setSurfaces(FALLBACK_SURFACES)
227
- setIsFallback(true)
228
- setLastCompiled('')
229
- setTotalNodes(0)
230
- const first = FALLBACK_SURFACES[0]
231
- setSelected(first)
232
- setContent('Content unavailable — recompile to regenerate')
233
- }
234
-
235
- const loadContent = async (name: string) => {
236
- setContentLoading(true)
237
- try {
238
- const data = await fetchInstructionContent(name)
239
- setContent(data.content || 'Empty surface — no compiled content yet.')
240
- } catch {
241
- setContent('Content unavailable — recompile to regenerate')
242
- } finally {
243
- setContentLoading(false)
244
- }
245
- }
245
+ }, [applyFallback, loadContent])
246
246
 
247
247
  const handleSelect = (surface: Surface) => {
248
248
  setSelected(surface)
@@ -268,9 +268,9 @@ export default function InstructionsPage() {
268
268
  }
269
269
 
270
270
  useEffect(() => {
271
- // eslint-disable-next-line react-hooks/set-state-in-effect, react-hooks/exhaustive-deps -- legitimate data fetch on mount
271
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate data fetch on mount
272
272
  void loadSurfaces()
273
- }, [])
273
+ }, [loadSurfaces])
274
274
 
275
275
  // ─── Derived Stats ───────────────────────────────────────────────────────────
276
276