total-recall-brain 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/bin/antigravity.mjs +19 -8
  3. package/extension/background.js +13 -1
  4. package/extension/content-script.js +18 -1
  5. package/extension/lib/brain-client.js +18 -5
  6. package/extension/options/options.js +9 -4
  7. package/frontend/index.html +8 -0
  8. package/frontend/src/api.ts +20 -1
  9. package/frontend/src/pages/ChatPage.tsx +9 -6
  10. package/frontend/src/pages/HealthPage.tsx +179 -7
  11. package/frontend/src/pages/IntegrationsPage.tsx +4 -2
  12. package/frontend/src/pages/SettingsPage.tsx +197 -2
  13. package/frontend/src/types.ts +16 -0
  14. package/package.json +2 -2
  15. package/scaffold/.agent/skills/total-recall/SKILL.md +55 -6
  16. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +2 -2
  17. package/scaffold/.agent/skills/total-recall/research-queue.jsonl +1 -0
  18. package/scaffold/.agent/skills/total-recall/skills/cli-agents/agents.yml +1 -6
  19. package/scaffold/.agent/skills/total-recall/skills/ssss/SKILL.md +19 -0
  20. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +33 -5
  21. package/src/cli/forget.mjs +18 -12
  22. package/src/cli/ingest/index.mjs +3 -2
  23. package/src/cli/map.mjs +2 -2
  24. package/src/cli/rebuild.mjs +1 -1
  25. package/src/cli/remember.mjs +21 -11
  26. package/src/cli/share.mjs +7 -2
  27. package/src/core/append-log.mjs +131 -0
  28. package/src/core/clarity-rewriter.mjs +10 -6
  29. package/src/core/conclusion-writer.mjs +3 -2
  30. package/src/core/config.mjs +6 -0
  31. package/src/core/conflict-detector.mjs +4 -2
  32. package/src/core/context-compiler.mjs +412 -0
  33. package/src/core/daemon-loop.mjs +15 -0
  34. package/src/core/dream.mjs +4 -4
  35. package/src/core/drift-detector.mjs +3 -3
  36. package/src/core/embeddings.mjs +60 -10
  37. package/src/core/fact-seeker.mjs +70 -44
  38. package/src/core/import-rules.mjs +3 -2
  39. package/src/core/inference-engine.mjs +5 -4
  40. package/src/core/migrate.mjs +4 -3
  41. package/src/core/optimizer.mjs +6 -5
  42. package/src/core/parallel-context.mjs +288 -0
  43. package/src/core/post-mortem.mjs +4 -3
  44. package/src/core/research-queue.mjs +4 -3
  45. package/src/core/runtime.mjs +30 -7
  46. package/src/core/sandbox.mjs +56 -26
  47. package/src/core/scheduler.mjs +6 -5
  48. package/src/core/schema.mjs +483 -2
  49. package/src/core/search.mjs +167 -101
  50. package/src/core/surface.mjs +203 -53
  51. package/src/core/validated-write.mjs +100 -0
  52. package/src/core/vault-cache.mjs +46 -18
  53. package/src/core/vault.mjs +17 -29
  54. package/src/core/vector-field.mjs +659 -0
  55. package/src/core/vector-store.mjs +85 -0
  56. package/src/server/api.mjs +4 -3
  57. package/src/server/index.mjs +13 -61
  58. package/src/server/rest.mjs +341 -63
  59. package/src/server/route-manifest.json +352 -77
  60. package/src/server/routes/memory.mjs +85 -39
  61. package/src/server/routes/sessions.mjs +6 -2
  62. package/src/server/routes/share.mjs +9 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Total Recall — SSSS Sovereign AI OS
2
2
 
3
- [![Version](https://img.shields.io/badge/version-3.0.0-indigo.svg)](package.json)
3
+ [![Version](https://img.shields.io/badge/version-3.3.0-indigo.svg)](package.json)
4
4
  [![License](https://img.shields.io/badge/license-MIT-emerald.svg)](LICENSE)
5
5
  [![Continuous Integration](https://img.shields.io/badge/CI-passing-teal.svg)](.github/workflows/test.yml)
6
6
 
@@ -83,10 +83,9 @@ if (!process.env.GOOGLE_API_KEY) {
83
83
  }
84
84
  }
85
85
 
86
- const apiKey = process.env.GOOGLE_API_KEY;
86
+ const apiKey = process.env.GOOGLE_API_KEY || '';
87
87
  if (!apiKey) {
88
- console.error('Error: GOOGLE_API_KEY environment variable is not configured.');
89
- process.exit(1);
88
+ console.warn('Warning: GOOGLE_API_KEY environment variable is not configured. Attempting connection via GCP Application Default Credentials or ambient environment...');
90
89
  }
91
90
 
92
91
  // Resolve the model dynamically from the API registry based on the category/alias selected
@@ -109,7 +108,13 @@ async function resolveGenerativeModel(selectedModel) {
109
108
  }
110
109
 
111
110
  try {
112
- const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
111
+ const keyParam = apiKey ? `key=${apiKey}` : '';
112
+ const headers = {};
113
+ if (!apiKey && process.env.GCLOUD_TOKEN) {
114
+ headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
115
+ }
116
+ const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?${keyParam}`, {
117
+ headers,
113
118
  signal: AbortSignal.timeout(5000)
114
119
  });
115
120
  if (res.ok) {
@@ -154,13 +159,19 @@ async function resolveGenerativeModel(selectedModel) {
154
159
  async function main() {
155
160
  try {
156
161
  const resolvedModel = await resolveGenerativeModel(requestedModel);
157
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${resolvedModel}:generateContent?key=${apiKey}`;
162
+ const keyParam = apiKey ? `?key=${apiKey}` : '';
163
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${resolvedModel}:generateContent${keyParam}`;
164
+
165
+ const headers = {
166
+ 'Content-Type': 'application/json',
167
+ };
168
+ if (!apiKey && process.env.GCLOUD_TOKEN) {
169
+ headers['Authorization'] = `Bearer ${process.env.GCLOUD_TOKEN}`;
170
+ }
158
171
 
159
172
  const res = await fetch(url, {
160
173
  method: 'POST',
161
- headers: {
162
- 'Content-Type': 'application/json',
163
- },
174
+ headers,
164
175
  body: JSON.stringify({
165
176
  contents: [{
166
177
  parts: [{ text: prompt }]
@@ -75,7 +75,7 @@ function showBadge(text, color) {
75
75
  // ---------------------------------------------------------------------------
76
76
  // Message listener (popup, content-script, sidepanel → background)
77
77
  // ---------------------------------------------------------------------------
78
- chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
78
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
79
79
  if (msg.type === 'QUERY_BRAIN') {
80
80
  search(msg.query, msg.topK || 3)
81
81
  .then(data => sendResponse({ memories: data.results || [] }))
@@ -96,4 +96,16 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
96
96
  .catch(() => sendResponse({ connected: false }));
97
97
  return true;
98
98
  }
99
+
100
+ if (msg.type === 'OPEN_SIDE_PANEL') {
101
+ const tabId = sender.tab?.id;
102
+ if (!chrome.sidePanel?.open || !tabId) {
103
+ sendResponse({ success: false, error: 'Side panel is unavailable for this tab.' });
104
+ return false;
105
+ }
106
+ chrome.sidePanel.open({ tabId })
107
+ .then(() => sendResponse({ success: true }))
108
+ .catch(err => sendResponse({ success: false, error: err.message }));
109
+ return true;
110
+ }
99
111
  });
@@ -112,8 +112,25 @@
112
112
  document.body.appendChild(pillHost);
113
113
  }
114
114
 
115
+ function isBlocked(blocklist) {
116
+ if (!Array.isArray(blocklist) || blocklist.length === 0) return false;
117
+ let host = '';
118
+ try {
119
+ host = new URL(pageContext.url).hostname.toLowerCase();
120
+ } catch {
121
+ return false;
122
+ }
123
+ return blocklist.some((entry) => {
124
+ const pattern = String(entry || '').trim().toLowerCase();
125
+ return pattern && (host === pattern || host.endsWith(`.${pattern}`) || pageContext.url.toLowerCase().includes(pattern));
126
+ });
127
+ }
128
+
115
129
  // ---- Query brain after delay ----
116
- function queryBrain() {
130
+ async function queryBrain() {
131
+ const { passiveTracking = false, blocklist = [] } = await chrome.storage.sync.get(['passiveTracking', 'blocklist']);
132
+ if (!passiveTracking || isBlocked(blocklist)) return;
133
+
117
134
  const query = pageContext.title || pageContext.url;
118
135
  if (!query) return;
119
136
 
@@ -6,8 +6,21 @@
6
6
  const DEFAULT_BRAIN_URL = 'http://127.0.0.1:3000';
7
7
 
8
8
  async function getConfig() {
9
- const config = await chrome.storage.sync.get(['brainUrl', 'pat']);
9
+ const [localConfig, syncConfig] = await Promise.all([
10
+ chrome.storage.local.get(['brainUrl', 'pat']),
11
+ chrome.storage.sync.get(['brainUrl', 'pat'])
12
+ ]);
10
13
  const pre = self.PreConfigured || {};
14
+ const config = {
15
+ brainUrl: localConfig.brainUrl || syncConfig.brainUrl,
16
+ pat: localConfig.pat || syncConfig.pat
17
+ };
18
+
19
+ if (syncConfig.pat && !localConfig.pat) {
20
+ await chrome.storage.local.set({ pat: syncConfig.pat });
21
+ await chrome.storage.sync.remove('pat');
22
+ }
23
+
11
24
  return {
12
25
  brainUrl: config.brainUrl || pre.brainUrl || DEFAULT_BRAIN_URL,
13
26
  pat: config.pat || pre.pat || ''
@@ -67,13 +80,13 @@
67
80
  return res.json();
68
81
  }
69
82
 
70
- // Self-heal stale storage: if preconfigured values exist, automatically write them to chrome.storage.sync
83
+ // Self-heal stale storage: if preconfigured values exist, write them to extension-local storage.
71
84
  try {
72
- if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
73
- chrome.storage.sync.get(['brainUrl', 'pat']).then((config) => {
85
+ if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
86
+ chrome.storage.local.get(['brainUrl', 'pat']).then((config) => {
74
87
  const pre = self.PreConfigured || {};
75
88
  if (pre.pat && (!config.pat || config.pat !== pre.pat)) {
76
- chrome.storage.sync.set({ brainUrl: pre.brainUrl, pat: pre.pat });
89
+ chrome.storage.local.set({ brainUrl: pre.brainUrl, pat: pre.pat });
77
90
  }
78
91
  });
79
92
  }
@@ -13,12 +13,14 @@
13
13
 
14
14
  // ---- Load saved settings ----
15
15
  async function loadSettings() {
16
- const settings = await chrome.storage.sync.get([
17
- 'brainUrl',
18
- 'pat',
16
+ const [localSettings, syncSettings] = await Promise.all([
17
+ chrome.storage.local.get(['brainUrl', 'pat']),
18
+ chrome.storage.sync.get([
19
19
  'blocklist',
20
20
  'captureGranularity'
21
+ ])
21
22
  ]);
23
+ const settings = { ...syncSettings, ...localSettings };
22
24
 
23
25
  const pre = self.PreConfigured || {};
24
26
  brainUrlInput.value = settings.brainUrl || pre.brainUrl || 'http://127.0.0.1:3000';
@@ -48,9 +50,12 @@
48
50
  const granularityRadio = document.querySelector('input[name="granularity"]:checked');
49
51
  const captureGranularity = granularityRadio ? granularityRadio.value : 'minimal';
50
52
 
51
- await chrome.storage.sync.set({
53
+ await chrome.storage.local.set({
52
54
  brainUrl: brainUrlInput.value.trim() || 'http://127.0.0.1:3000',
53
55
  pat: patInput.value.trim(),
56
+ });
57
+ await chrome.storage.sync.remove('pat');
58
+ await chrome.storage.sync.set({
54
59
  blocklist,
55
60
  captureGranularity
56
61
  });
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="description" content="Total Recall — Sovereign AI Brain Dashboard. Memory explorer, chat interface, code sandbox, and system health monitoring." />
7
7
  <meta name="theme-color" content="#0a0a0f" />
8
+ <link rel="manifest" href="/manifest.json" />
8
9
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
11
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
@@ -13,5 +14,12 @@
13
14
  <body>
14
15
  <div id="root"></div>
15
16
  <script type="module" src="/src/main.tsx"></script>
17
+ <script>
18
+ if ('serviceWorker' in navigator) {
19
+ window.addEventListener('load', () => {
20
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
21
+ });
22
+ }
23
+ </script>
16
24
  </body>
17
25
  </html>
@@ -563,7 +563,7 @@ export async function resolveConflict(
563
563
 
564
564
  export async function saveMemory(slug: string, node: Partial<MemoryNode>): Promise<MemoryNode> {
565
565
  const res = await apiFetch(`${API_BASE}/api/memory/${slug}`, {
566
- method: "PUT",
566
+ method: "PATCH",
567
567
  headers: { "Content-Type": "application/json" },
568
568
  body: JSON.stringify(node),
569
569
  })
@@ -686,3 +686,22 @@ export async function fetchExtensionStatus(): Promise<{ available: boolean; conn
686
686
  return res.json()
687
687
  }
688
688
 
689
+ // ─── Auto-Update APIs ──────────────────────────────────────────────────────────
690
+
691
+ export interface UpdateCheckResult {
692
+ currentVersion: string
693
+ latestVersion: string
694
+ updateAvailable: boolean
695
+ }
696
+
697
+ export async function checkUpdate(): Promise<UpdateCheckResult> {
698
+ const res = await apiFetch(`${API_BASE}/api/update/check`)
699
+ if (!res.ok) throw new Error(`Check update API error: ${res.status}`)
700
+ return res.json()
701
+ }
702
+
703
+ export async function runUpdate(): Promise<{ success: boolean; message: string }> {
704
+ const res = await apiFetch(`${API_BASE}/api/update/run`, { method: 'POST' })
705
+ if (!res.ok) throw new Error(`Run update API error: ${res.status}`)
706
+ return res.json()
707
+ }
@@ -57,7 +57,10 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
57
57
 
58
58
  // Model Selector state
59
59
  const [availableModels, setAvailableModels] = useState<string[]>([])
60
- const [selectedModel, setSelectedModel] = useState<string>(() => localStorage.getItem('selectedModel') || '')
60
+ const [selectedModel, setSelectedModel] = useState<string>(() => {
61
+ const saved = localStorage.getItem('selectedModel');
62
+ return (saved && saved !== 'gemini') ? saved : 'antigravity';
63
+ })
61
64
  const [selectedSubModel, setSelectedSubModel] = useState<string>(() => localStorage.getItem('selectedSubModel') || '')
62
65
  const [geminiModels, setGeminiModels] = useState<{ id: string; displayName: string }[]>([])
63
66
  const [showModelDropdown, setShowModelDropdown] = useState(false)
@@ -104,27 +107,27 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
104
107
  .then(health => {
105
108
  const agents = health.cli_agents && health.cli_agents.length > 0
106
109
  ? health.cli_agents
107
- : ['antigravity', 'gemini', 'claude', 'codex'];
110
+ : ['antigravity', 'claude', 'codex'];
108
111
  setAvailableModels(agents);
109
112
 
110
113
  const saved = localStorage.getItem('selectedModel');
111
114
  if (saved && agents.includes(saved)) {
112
115
  setSelectedModel(saved);
113
116
  } else {
114
- const defaultAgent = agents.includes('gemini') ? 'gemini' : agents[0];
117
+ const defaultAgent = agents.includes('antigravity') ? 'antigravity' : agents[0];
115
118
  setSelectedModel(defaultAgent);
116
119
  localStorage.setItem('selectedModel', defaultAgent);
117
120
  }
118
121
  })
119
122
  .catch(() => {
120
- const agents = ['antigravity', 'gemini', 'claude', 'codex'];
123
+ const agents = ['antigravity', 'claude', 'codex'];
121
124
  setAvailableModels(agents);
122
125
  const saved = localStorage.getItem('selectedModel');
123
126
  if (saved && agents.includes(saved)) {
124
127
  setSelectedModel(saved);
125
128
  } else {
126
- setSelectedModel('gemini');
127
- localStorage.setItem('selectedModel', 'gemini');
129
+ setSelectedModel('antigravity');
130
+ localStorage.setItem('selectedModel', 'antigravity');
128
131
  }
129
132
  })
130
133
 
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { fetchHealth } from '../api'
3
- import type { HealthData } from '../types'
2
+ import { fetchHealth, checkUpdate, runUpdate } from '../api'
3
+ import type { HealthData, UpdateCheckResult } from '../types'
4
4
 
5
5
  function formatUptime(seconds: number): string {
6
6
  const d = Math.floor(seconds / 86400)
@@ -15,6 +15,12 @@ export default function HealthPage() {
15
15
  const [health, setHealth] = useState<HealthData | null>(null)
16
16
  const [error, setError] = useState('')
17
17
  const [lastPoll, setLastPoll] = useState<Date | null>(null)
18
+
19
+ // Auto-Update States
20
+ const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null)
21
+ const [isUpdating, setIsUpdating] = useState(false)
22
+ const [showConfirmModal, setShowConfirmModal] = useState(false)
23
+ const [updateError, setUpdateError] = useState('')
18
24
 
19
25
  useEffect(() => {
20
26
  let active = true
@@ -33,17 +39,166 @@ export default function HealthPage() {
33
39
 
34
40
  poll()
35
41
  const interval = setInterval(poll, 5000)
42
+
43
+ // Check for updates
44
+ const checkVersion = async () => {
45
+ try {
46
+ const data = await checkUpdate()
47
+ if (active) setUpdateInfo(data)
48
+ } catch (e) {
49
+ console.error('Failed to check updates', e)
50
+ }
51
+ }
52
+ checkVersion()
53
+
36
54
  return () => { active = false; clearInterval(interval) }
37
55
  }, [])
38
56
 
57
+ const handleStartUpdate = async () => {
58
+ setShowConfirmModal(false)
59
+ setIsUpdating(true)
60
+ setUpdateError('')
61
+
62
+ try {
63
+ await runUpdate()
64
+
65
+ // Keep polling health to check when it goes down and recovers
66
+ let serverDown = false
67
+ const pollInterval = setInterval(async () => {
68
+ try {
69
+ const res = await fetchHealth()
70
+ if (serverDown && res.status === 'healthy') {
71
+ clearInterval(pollInterval)
72
+ window.location.reload()
73
+ }
74
+ } catch {
75
+ // Connection failed = server is offline / restarting
76
+ serverDown = true
77
+ }
78
+ }, 2000)
79
+
80
+ } catch (e) {
81
+ setUpdateError((e as Error).message)
82
+ setIsUpdating(false)
83
+ }
84
+ }
85
+
39
86
  return (
40
- <div className="page">
41
- <div className="page-header">
42
- <h1>System Health</h1>
43
- <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
87
+ <div className="page" style={{ position: 'relative' }}>
88
+ {isUpdating && (
89
+ <div style={{
90
+ position: 'fixed',
91
+ inset: 0,
92
+ background: 'rgba(13, 17, 23, 0.9)',
93
+ zIndex: 1000,
94
+ display: 'flex',
95
+ flexDirection: 'column',
96
+ alignItems: 'center',
97
+ justifyContent: 'center',
98
+ backdropFilter: 'blur(8px)',
99
+ animation: 'fadeIn 0.3s ease'
100
+ }}>
101
+ <div className="spinner" style={{
102
+ width: 50,
103
+ height: 50,
104
+ border: '4px solid var(--border)',
105
+ borderTopColor: 'var(--accent)',
106
+ borderRadius: '50%',
107
+ animation: 'spin 1s linear infinite',
108
+ marginBottom: 20
109
+ }} />
110
+ <h2 style={{ color: 'var(--text-primary)', marginBottom: 8 }}>Updating Total Recall...</h2>
111
+ <p style={{ color: 'var(--text-muted)', fontSize: 14 }}>Applying git changes, installing npm modules, and restarting daemon.</p>
112
+ <p style={{ color: 'var(--accent)', fontSize: 13, marginTop: 10 }}>Please do not close this window.</p>
113
+ </div>
114
+ )}
115
+
116
+ {showConfirmModal && (
117
+ <div style={{
118
+ position: 'fixed',
119
+ inset: 0,
120
+ background: 'rgba(0, 0, 0, 0.7)',
121
+ zIndex: 900,
122
+ display: 'flex',
123
+ alignItems: 'center',
124
+ justifyContent: 'center',
125
+ backdropFilter: 'blur(4px)',
126
+ padding: 20
127
+ }}>
128
+ <div className="card" style={{
129
+ maxWidth: 450,
130
+ width: '100%',
131
+ backgroundColor: 'var(--bg-surface)',
132
+ border: '1px solid var(--border)',
133
+ borderRadius: 12,
134
+ padding: 24,
135
+ boxShadow: '0 20px 40px rgba(0, 0, 0, 0.5)'
136
+ }}>
137
+ <h3 style={{ marginBottom: 12, color: 'var(--text-primary)' }}>Confirm Core Self-Update</h3>
138
+ <p style={{ color: 'var(--text-muted)', marginBottom: 20, fontSize: 14, lineHeight: 1.5 }}>
139
+ This will pull the latest version of Total Recall ({updateInfo?.latestVersion}) from the git repository, reinstall dependencies, rebuild frontend bundles, and reboot the system kernel.
140
+ </p>
141
+ <div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
142
+ <button
143
+ className="btn btn-secondary"
144
+ onClick={() => setShowConfirmModal(false)}
145
+ style={{ padding: '8px 16px', borderRadius: 8 }}
146
+ >
147
+ Cancel
148
+ </button>
149
+ <button
150
+ className="btn btn-primary"
151
+ onClick={handleStartUpdate}
152
+ style={{ padding: '8px 16px', borderRadius: 8, background: 'var(--accent)' }}
153
+ >
154
+ Update Now
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )}
160
+
161
+ <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
162
+ <div>
163
+ <h1>System Health</h1>
164
+ <p>Live monitoring of your sovereign brain {lastPoll && <span>· Last poll: {lastPoll.toLocaleTimeString()}</span>}</p>
165
+ </div>
166
+ {updateInfo?.updateAvailable && (
167
+ <div className="badge badge-success" style={{
168
+ display: 'flex',
169
+ alignItems: 'center',
170
+ gap: 12,
171
+ padding: '12px 18px',
172
+ borderRadius: 12,
173
+ background: 'rgba(63, 185, 80, 0.1)',
174
+ border: '1px solid rgba(63, 185, 80, 0.3)',
175
+ animation: 'pulse 2s infinite'
176
+ }}>
177
+ <span style={{ fontSize: 14, color: 'var(--text-primary)', fontWeight: 600 }}>
178
+ New version available: {updateInfo.latestVersion}
179
+ </span>
180
+ <button
181
+ className="btn btn-primary"
182
+ onClick={() => setShowConfirmModal(true)}
183
+ style={{
184
+ fontSize: 12,
185
+ padding: '6px 12px',
186
+ borderRadius: 6,
187
+ backgroundColor: '#3fb950',
188
+ color: '#fff',
189
+ border: 'none',
190
+ cursor: 'pointer',
191
+ fontWeight: 600
192
+ }}
193
+ >
194
+ Update Now
195
+ </button>
196
+ </div>
197
+ )}
44
198
  </div>
45
199
 
46
200
  {error && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ {error}</div>}
201
+ {updateError && <div className="badge badge-error" style={{ marginBottom: 16 }}>⚠️ Update failed: {updateError}</div>}
47
202
 
48
203
  {health ? (
49
204
  <>
@@ -55,7 +210,9 @@ export default function HealthPage() {
55
210
  <div className="label">Status</div>
56
211
  </div>
57
212
  <div className="stat-card">
58
- <div className="value" style={{ color: 'var(--accent)' }}>{health.version}</div>
213
+ <div className="value" style={{ color: 'var(--accent)' }}>
214
+ {health.version} {updateInfo && !updateInfo.updateAvailable && <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>(Latest)</span>}
215
+ </div>
59
216
  <div className="label">Version</div>
60
217
  </div>
61
218
  <div className="stat-card">
@@ -76,6 +233,10 @@ export default function HealthPage() {
76
233
  <tr><th>Version</th><td>{health.version}</td></tr>
77
234
  <tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
78
235
  <tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
236
+ <tr><th>Daemon Status</th><td>{health.daemon || 'unknown'}</td></tr>
237
+ {health.cli_agents && (
238
+ <tr><th>CLI Agents</th><td>{health.cli_agents.join(', ')}</td></tr>
239
+ )}
79
240
  <tr><th>Polling</th><td>Every 5 seconds</td></tr>
80
241
  </tbody>
81
242
  </table>
@@ -86,6 +247,17 @@ export default function HealthPage() {
86
247
  {[1, 2, 3, 4].map(i => <div key={i} className="skeleton" style={{ height: 100 }} />)}
87
248
  </div>
88
249
  ) : null}
250
+
251
+ <style>{`
252
+ @keyframes spin {
253
+ 0% { transform: rotate(0deg); }
254
+ 100% { transform: rotate(360deg); }
255
+ }
256
+ @keyframes fadeIn {
257
+ from { opacity: 0; }
258
+ to { opacity: 1; }
259
+ }
260
+ `}</style>
89
261
  </div>
90
262
  )
91
263
  }
@@ -391,10 +391,12 @@ export default function IntegrationsPage() {
391
391
  )
392
392
 
393
393
  if (matchingKey) {
394
- if (matchingKey.hit_count > 0) {
394
+ if (matchingKey.hit_count > 0 || preset.id === 'antigravity') {
395
395
  const lastUsedStr = matchingKey.last_used_at
396
396
  ? ` (Active ${new Date(matchingKey.last_used_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })})`
397
- : ''
397
+ : preset.id === 'antigravity'
398
+ ? ' (Active Now)'
399
+ : ''
398
400
  statusBadge = (
399
401
  <span style={{
400
402
  display: 'inline-flex',