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
@@ -1,20 +1,29 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { fetchConfigJson, saveConfigJson, runSandbox, fetchHealth, runAgentDiagnostics } from '../api';
2
+ import { fetchConfigJson, saveConfigJson, runSandbox, fetchHealth, runAgentDiagnostics, checkUpdate, runUpdate, fetchBrains } from '../api';
3
3
  import type { HealthData } from '../types';
4
4
  import type { ConfigJson } from '../types';
5
5
 
6
- export default function SettingsPage() {
6
+ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string }) {
7
7
  const [configData, setConfigData] = useState<ConfigJson | null>(null);
8
8
  const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
9
9
  const [sandboxLog, setSandboxLog] = useState('');
10
-
10
+
11
+
11
12
  const [health, setHealth] = useState<HealthData | null>(null);
12
13
  const [runningDiagnostics, setRunningDiagnostics] = useState(false);
13
14
  const [diagnosticLogs, setDiagnosticLogs] = useState<string | null>(null);
14
15
 
15
-
16
+ const [updateInfo, setUpdateInfo] = useState<{ updateAvailable: boolean, latestVersion?: string } | null>(null);
17
+ const [updating, setUpdating] = useState(false);
18
+ const [updateMessage, setUpdateMessage] = useState<string | null>(null);
19
+
20
+ const [brains, setBrains] = useState<unknown[]>([]);
21
+ const activeBrain = activeBrainId || localStorage.getItem('total-recall-active-brain') || '';
22
+
16
23
  useEffect(() => {
17
24
  fetchHealth().then(setHealth).catch(console.error);
25
+ checkUpdate().then(setUpdateInfo).catch(console.error);
26
+ fetchBrains().then(setBrains).catch(console.error);
18
27
  fetchConfigJson()
19
28
  .then(data => {
20
29
  // Ensure all nested structures exist for controlled inputs
@@ -33,14 +42,15 @@ export default function SettingsPage() {
33
42
  setConfigData(data);
34
43
  })
35
44
  .catch(console.error);
36
- }, []);
45
+ }, [activeBrainId]);
37
46
 
38
47
 
39
48
  const AGENTS_LIST = [
40
49
  { id: 'antigravity', name: 'Antigravity (Gemini SDK)', desc: 'Primary core developer agent' },
41
50
  { id: 'gemini', name: 'Gemini CLI', desc: 'Direct Gemini assistant binary' },
42
51
  { id: 'claude', name: 'Claude Code', desc: 'Anthropic developer CLI wrapper' },
43
- { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' }
52
+ { id: 'codex', name: 'Codex CLI', desc: 'OpenAI agent binary integration' },
53
+ { id: 'grok', name: 'Grok CLI', desc: 'xAI developer binary integration' }
44
54
  ];
45
55
 
46
56
  const handleRunDiagnostics = async (e: React.FormEvent) => {
@@ -51,13 +61,26 @@ export default function SettingsPage() {
51
61
  const res = await runAgentDiagnostics();
52
62
  setDiagnosticLogs(res.output);
53
63
  fetchHealth().then(setHealth).catch(console.error);
54
- } catch (err: any) {
55
- setDiagnosticLogs('Error: ' + err.message);
64
+ } catch (err: unknown) {
65
+ setDiagnosticLogs('Error: ' + (err as Error).message);
56
66
  } finally {
57
67
  setRunningDiagnostics(false);
58
68
  }
59
69
  };
60
70
 
71
+ const handleRunUpdate = async () => {
72
+ setUpdating(true);
73
+ setUpdateMessage(null);
74
+ try {
75
+ const res = await runUpdate();
76
+ setUpdateMessage(res.success ? 'Update complete. Restarting...' : 'Update failed: ' + res.message);
77
+ } catch (err: unknown) {
78
+ setUpdateMessage('Update error: ' + (err as Error).message);
79
+ } finally {
80
+ setUpdating(false);
81
+ }
82
+ };
83
+
61
84
  const handleSaveVisual = async () => {
62
85
  if (!configData) return;
63
86
  setSaveStatus('saving');
@@ -87,11 +110,9 @@ export default function SettingsPage() {
87
110
  };
88
111
 
89
112
  // State Update Helpers
90
- const updateSecurityProp = (key: string, value: any) => {
91
- setConfigData(prev => prev ? { ...prev, security: { ...prev.security, [key]: value } } : null);
92
- };
93
113
 
94
- const updateSecurityNested = (category: string, key: string, value: any) => {
114
+
115
+ const updateSecurityNested = (category: string, key: string, value: unknown) => {
95
116
  setConfigData(prev => {
96
117
  if (!prev) return prev;
97
118
  return {
@@ -99,7 +120,7 @@ export default function SettingsPage() {
99
120
  security: {
100
121
  ...prev.security,
101
122
  [category]: {
102
- ...((prev.security as any)[category] || {}),
123
+ ...((prev.security as Record<string, unknown>)[category] as Record<string, unknown> || {}),
103
124
  [key]: value
104
125
  }
105
126
  }
@@ -107,7 +128,7 @@ export default function SettingsPage() {
107
128
  });
108
129
  };
109
130
 
110
- const updateBudgetProp = (key: string, value: any) => {
131
+ const updateBudgetProp = (key: string, value: unknown) => {
111
132
  setConfigData(prev => {
112
133
  if (!prev) return prev;
113
134
  return {
@@ -123,13 +144,16 @@ export default function SettingsPage() {
123
144
  });
124
145
  };
125
146
 
126
- const updateBrainProp = (key: string, value: any) => {
147
+ const updateBrainProp = (key: string, value: unknown) => {
127
148
  setConfigData(prev => prev ? { ...prev, brain: { ...prev.brain, [key]: value } } : null);
128
149
  };
129
150
 
130
151
 
131
- const getCsv = (arr: any) => Array.isArray(arr) ? arr.join(', ') : '';
132
- const setCsv = (val: string) => val.split(',').map(s => s.trim()).filter(Boolean);
152
+
153
+ // Instead of parsing it back and forth on every keystroke,
154
+ // we just use simple helpers that don't aggressively trim trailing commas.
155
+ const getCsv = (arr: unknown) => Array.isArray(arr) ? arr.join(', ') : '';
156
+ const setCsv = (str: string) => str.split(',').map(s => s.trimStart());
133
157
 
134
158
  if (!configData) {
135
159
  return (
@@ -362,7 +386,7 @@ export default function SettingsPage() {
362
386
  System Settings
363
387
  </h1>
364
388
  <p style={{ margin: '4px 0 0', color: 'var(--text-secondary)', fontSize: 14 }}>
365
- Configure core OS behavior, security, API keys, and integration layers.
389
+ Configure core system behavior, security, network bindings, and integration layers.
366
390
  </p>
367
391
  </div>
368
392
  <div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
@@ -509,23 +533,10 @@ export default function SettingsPage() {
509
533
  <div className="settings-icon-wrap" style={{ background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.05))', borderColor: 'rgba(16, 185, 129, 0.2)' }}>🛡️</div>
510
534
  <div>
511
535
  <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Security & Privacy</h3>
512
- <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '4px 0 0' }}>Data sovereignty protocols</p>
536
+ <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '4px 0 0' }}>Manage access control and local sandboxing</p>
513
537
  </div>
514
538
  </div>
515
539
 
516
- <div className="field-row">
517
- <div>
518
- <div style={{ fontSize: 14, fontWeight: 500 }}>YOLO Mode</div>
519
- <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Disable write protection for AI agents</div>
520
- </div>
521
- <input
522
- type="checkbox"
523
- className="settings-checkbox"
524
- checked={!!configData.security.yolo_mode}
525
- onChange={(e) => updateSecurityProp('yolo_mode', e.target.checked)}
526
- />
527
- </div>
528
-
529
540
  <div className="field-row">
530
541
  <div>
531
542
  <div style={{ fontSize: 14, fontWeight: 500 }}>Sandbox Enabled</div>
@@ -534,24 +545,11 @@ export default function SettingsPage() {
534
545
  <input
535
546
  type="checkbox"
536
547
  className="settings-checkbox"
537
- checked={!!(configData.security as any).sandbox?.enabled}
548
+ checked={!!configData.security.sandbox?.enabled}
538
549
  onChange={(e) => updateSecurityNested('sandbox', 'enabled', e.target.checked)}
539
550
  />
540
551
  </div>
541
552
 
542
- <div className="field-row">
543
- <div>
544
- <div style={{ fontSize: 14, fontWeight: 500 }}>Enforce Local Only</div>
545
- <div style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>Force agents to run offline models</div>
546
- </div>
547
- <input
548
- type="checkbox"
549
- className="settings-checkbox"
550
- checked={!!configData.security.privacy?.enforce_local_only}
551
- onChange={(e) => updateSecurityNested('privacy', 'enforce_local_only', e.target.checked)}
552
- />
553
- </div>
554
-
555
553
  <div className="field-row">
556
554
  <div>
557
555
  <div style={{ fontSize: 14, fontWeight: 500 }}>Force Password Reset</div>
@@ -573,24 +571,12 @@ export default function SettingsPage() {
573
571
  <input
574
572
  type="checkbox"
575
573
  className="settings-checkbox"
576
- checked={!!(configData.security as any).api?.allow_static_pats}
574
+ checked={!!configData.security.api?.allow_static_pats}
577
575
  onChange={(e) => updateSecurityNested('api', 'allow_static_pats', e.target.checked)}
578
576
  />
579
577
  </div>
580
578
 
581
- <div className="field-col">
582
- <label htmlFor="allow_frontier_export" className="field-label">Allow Frontier Export</label>
583
- <select
584
- id="allow_frontier_export"
585
- className="settings-select"
586
- value={configData.security.privacy?.allow_frontier_export || 'ask_per_skill'}
587
- onChange={(e) => updateSecurityNested('privacy', 'allow_frontier_export', e.target.value)}
588
- >
589
- <option value="always">Always (Not Recommended)</option>
590
- <option value="ask_per_skill">Ask Per Skill (Recommended)</option>
591
- <option value="never">Never (Air-Gapped)</option>
592
- </select>
593
- </div>
579
+
594
580
 
595
581
  <div className="field-col">
596
582
  <label htmlFor="session_ttl_hours" className="field-label">Dashboard Session TTL (Hours)</label>
@@ -636,7 +622,7 @@ export default function SettingsPage() {
636
622
  step="0.01"
637
623
  className="settings-input"
638
624
  value={configData.budget.budget?.daily_cap_usd ?? 5.0}
639
- onChange={(e) => updateBudgetProp('daily_cap_usd', parseFloat(e.target.value))}
625
+ onChange={(e) => updateBudgetProp('monthly_soft_cap', e.target.value === '' ? null : Number(e.target.value))}
640
626
  />
641
627
  </div>
642
628
  <div className="field-col" style={{ flex: 1 }}>
@@ -653,24 +639,59 @@ export default function SettingsPage() {
653
639
  </div>
654
640
  </div>
655
641
 
656
- {/* Brain & Agent Rules */}
642
+ {/* Brain Configuration */}
657
643
  <div className="settings-card">
658
644
  <div style={{ display: 'flex', alignItems: 'center', gap: 16, borderBottom: '1px solid rgba(255,255,255,0.05)', paddingBottom: 20 }}>
659
645
  <div className="settings-icon-wrap" style={{ background: 'linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(147, 51, 234, 0.05))', borderColor: 'rgba(168, 85, 247, 0.2)' }}>🧠</div>
660
646
  <div>
661
- <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Brain & Reasoning</h3>
662
- <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '4px 0 0' }}>Core inference parameters</p>
647
+ <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Brain Configuration</h3>
648
+ <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '4px 0 0' }}>Manage memory kernel identities</p>
663
649
  </div>
664
650
  </div>
665
651
 
666
652
  <div className="field-col">
667
- <label className="field-label">Brain URL</label>
668
- <input
669
- type="text"
670
- className="settings-input"
671
- value={configData.brain?.url || ''}
672
- onChange={(e) => updateBrainProp('url', e.target.value)}
673
- />
653
+ <label className="field-label" style={{ color: 'var(--text-accent)', fontWeight: 600 }}>Select Active Brain</label>
654
+ <select
655
+ className="settings-select"
656
+ style={{ border: '1px solid var(--border-accent)', background: 'rgba(59, 130, 246, 0.05)' }}
657
+ value={activeBrain}
658
+ onChange={(e) => {
659
+ const val = e.target.value;
660
+ if (val) {
661
+ localStorage.setItem('total-recall-active-brain', val);
662
+ } else {
663
+ localStorage.removeItem('total-recall-active-brain');
664
+ }
665
+ window.location.reload();
666
+ }}
667
+ >
668
+ <option value="">Global Brain (Root)</option>
669
+ {brains.map((b: Record<string, unknown>) => (
670
+ <option key={b.name} value={b.name}>{b.name} ({b.role} - {b.nodes} nodes)</option>
671
+ ))}
672
+ </select>
673
+ </div>
674
+
675
+ <div style={{ display: 'flex', gap: 16 }}>
676
+ <div className="field-col" style={{ flex: 1 }}>
677
+ <label className="field-label">Brain URL</label>
678
+ <input
679
+ type="text"
680
+ className="settings-input"
681
+ value={configData.brain?.url || ''}
682
+ onChange={(e) => updateBrainProp('url', e.target.value)}
683
+ />
684
+ </div>
685
+ <div className="field-col" style={{ flex: 1 }}>
686
+ <label className="field-label">Brain Token</label>
687
+ <input
688
+ type="password"
689
+ className="settings-input"
690
+ placeholder={configData.brain?.has_token ? '•••••••••••••••• (Leave blank)' : 'No token set'}
691
+ value={configData.brain?.token || ''}
692
+ onChange={(e) => updateBrainProp('token', e.target.value)}
693
+ />
694
+ </div>
674
695
  </div>
675
696
 
676
697
  <div className="field-col">
@@ -736,9 +757,11 @@ export default function SettingsPage() {
736
757
  onChange={(e) => updateBrainProp('preferred_agent', e.target.value)}
737
758
  >
738
759
  <option value="auto">Auto-Select (Recommended)</option>
739
- <option value="gemini">Gemini (Google Deepmind)</option>
760
+ <option value="antigravity">Antigravity (Google Deepmind)</option>
761
+ <option value="gemini">Gemini (Google)</option>
740
762
  <option value="claude">Claude (Anthropic)</option>
741
763
  <option value="openai">OpenAI (O-Series)</option>
764
+ <option value="grok">Grok (xAI)</option>
742
765
  </select>
743
766
  </div>
744
767
  </div>
@@ -760,29 +783,22 @@ export default function SettingsPage() {
760
783
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
761
784
  Capture context directly from your browser. Install from the Chrome Web Store or load unpacked from <code>/extension</code> directory.
762
785
  </p>
763
- <button className="btn-primary" style={{ width: '100%' }}>Download Extension</button>
786
+ <a href="/api/extension/download" download className="btn-primary" style={{ display: 'block', textAlign: 'center', textDecoration: 'none', width: '100%', boxSizing: 'border-box' }}>Download Extension</a>
764
787
  </div>
765
788
 
766
789
  <div style={{ background: 'rgba(255,255,255,0.03)', padding: 20, borderRadius: 12, border: '1px solid rgba(255,255,255,0.06)' }}>
767
- <div style={{ fontSize: 24, marginBottom: 12 }}>📦</div>
768
- <h4 style={{ margin: '0 0 8px', fontSize: 15 }}>Sovereign Toolbox</h4>
790
+ <div style={{ fontSize: 24, marginBottom: 12 }}>🛠️</div>
791
+ <h4 style={{ margin: '0 0 8px', fontSize: 15 }}>Developer Utilities</h4>
769
792
  <p style={{ fontSize: 13, color: 'var(--text-tertiary)', margin: '0 0 16px', lineHeight: 1.5 }}>
770
- Execute quick administrative operations via the UCW toolkit and sandbox shell.
793
+ Run background maintenance tasks or re-index the local search tree.
771
794
  </p>
772
795
  <div style={{ display: 'flex', gap: 12 }}>
773
796
  <button
774
797
  className="btn-secondary"
775
798
  style={{ flex: 1 }}
776
- onClick={() => executeSandboxCommand('npx total-recall compile', 'Manual Index Recompilation')}
799
+ onClick={() => executeSandboxCommand('npx total-recall compile', 'Rebuilding Search Indexes')}
777
800
  >
778
- Compile
779
- </button>
780
- <button
781
- className="btn-primary"
782
- style={{ flex: 1 }}
783
- onClick={() => executeSandboxCommand('npx @ssss/cli export ./bundle.ucw', 'Exporting UCW Bundle')}
784
- >
785
- Export Bundle
801
+ Rebuild Index
786
802
  </button>
787
803
  </div>
788
804
  </div>
@@ -795,55 +811,43 @@ export default function SettingsPage() {
795
811
  )}
796
812
  </div>
797
813
 
798
- </div>
799
- {/* Ollama Panel */}
800
- <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 }}>
801
- <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
802
- <span style={{ fontSize: 20 }}>🦙</span>
803
- <div>
804
- <h3 style={{ fontSize: 14, fontWeight: 600 }}>Local Provider</h3>
805
- <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Connect Ollama, LM Studio, vLLM, or Llama.cpp</p>
806
- </div>
807
- </div>
808
-
809
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
810
- <label htmlFor="local_endpoint" style={{ fontSize: 13, fontWeight: 500 }}>Provider Base URL</label>
811
- <input
812
- id="local_endpoint"
813
- type="text"
814
- placeholder="http://127.0.0.1:11434 or http://127.0.0.1:1234/v1"
815
- value={configData?.brain?.local_endpoint || ''}
816
- onChange={(e) => updateBrainProp('local_endpoint', e.target.value)}
817
- style={{
818
- background: 'var(--bg-tertiary)',
819
- color: 'var(--text-primary)',
820
- border: '1px solid var(--border)',
821
- padding: '8px 12px',
822
- borderRadius: 6,
823
- outline: 'none',
824
- fontSize: 13
825
- }}
826
- />
827
- <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Leave blank to use the default localhost endpoint.</span>
814
+ {/* System Updates */}
815
+ <div className="settings-card" style={{ gridColumn: '1 / -1' }}>
816
+ <div style={{ display: 'flex', alignItems: 'center', gap: 16, borderBottom: '1px solid rgba(255,255,255,0.05)', paddingBottom: 20 }}>
817
+ <div className="settings-icon-wrap" style={{ background: 'linear-gradient(135deg, rgba(63, 185, 80, 0.2), rgba(34, 197, 94, 0.05))', borderColor: 'rgba(63, 185, 80, 0.2)' }}>🚀</div>
818
+ <div>
819
+ <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>System Update</h3>
820
+ <p style={{ fontSize: 12, color: 'var(--text-tertiary)', margin: '4px 0 0' }}>Manage Total Recall version updates</p>
821
+ </div>
822
+ </div>
823
+
824
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 24, padding: '16px 20px', background: 'rgba(0,0,0,0.2)', borderRadius: 12, border: '1px solid rgba(255,255,255,0.05)' }}>
825
+ <div>
826
+ <div style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' }}>
827
+ {updateInfo?.updateAvailable ? `Update Available (v${updateInfo.latestVersion})` : 'System is up to date'}
828
828
  </div>
829
-
830
- <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginTop: 8 }}>
831
- <span className="badge" style={{
832
- background: health?.ollama === 'running' ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
833
- color: health?.ollama === 'running' ? '#10b981' : '#ef4444',
834
- border: health?.ollama === 'running' ? '1px solid rgba(16, 185, 129, 0.2)' : '1px solid rgba(239, 68, 68, 0.2)',
835
- fontSize: 10,
836
- padding: '4px 8px'
837
- }}>
838
- {health?.ollama === 'running' ? 'Connected' : 'Offline'}
839
- </span>
840
- {health?.ollama === 'running' && health?.ollama_models && (
841
- <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>
842
- {health.ollama_models.length} local models available
843
- </span>
844
- )}
829
+ <div style={{ fontSize: 12, color: 'var(--text-tertiary)', marginTop: 4 }}>
830
+ {updateInfo?.updateAvailable ? 'A new version of Total Recall is available for download.' : 'You are running the latest version.'}
845
831
  </div>
832
+ {updateMessage && (
833
+ <div style={{ fontSize: 12, color: updateMessage.includes('error') || updateMessage.includes('failed') ? '#ef4444' : '#10b981', marginTop: 8 }}>
834
+ {updateMessage}
835
+ </div>
836
+ )}
846
837
  </div>
838
+ {updateInfo?.updateAvailable && (
839
+ <button
840
+ className="btn-primary"
841
+ onClick={handleRunUpdate}
842
+ disabled={updating}
843
+ style={{ background: '#3fb950', border: 'none' }}
844
+ >
845
+ {updating ? 'Updating...' : 'Download & Restart'}
846
+ </button>
847
+ )}
848
+ </div>
849
+ </div>
850
+
847
851
  {/* CLI Agents */}
848
852
  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24 }}>
849
853
 
@@ -937,6 +941,7 @@ export default function SettingsPage() {
937
941
  </div>
938
942
  )}
939
943
  </div>
944
+ </div>
940
945
  </div>
941
946
  </div>
942
947
  );