total-recall-brain 3.2.2 → 3.3.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.
@@ -5,6 +5,7 @@
5
5
 
6
6
  // ---- DOM refs ----
7
7
  const dot = document.getElementById('connection-dot');
8
+ const activeBrainBadge = document.getElementById('active-brain-badge');
8
9
  const searchInput = document.getElementById('search-input');
9
10
  const memoriesList = document.getElementById('memories-list');
10
11
  const btnRemember = document.getElementById('btn-remember');
@@ -15,6 +16,59 @@
15
16
  const btnSaveNote = document.getElementById('btn-save-note');
16
17
  const btnCancelNote = document.getElementById('btn-cancel-note');
17
18
  const researchList = document.getElementById('research-list');
19
+ const btnRefreshResearch = document.getElementById('btn-refresh-research');
20
+
21
+ // Chat tab DOM refs
22
+ const chatMessages = document.getElementById('chat-messages');
23
+ const chatInput = document.getElementById('chat-input');
24
+ const btnSendChat = document.getElementById('btn-send-chat');
25
+ const chatGroundingToggle = document.getElementById('chat-grounding-toggle');
26
+ const btnSuggestSummarize = document.getElementById('btn-suggest-summarize');
27
+ const btnSuggestRelated = document.getElementById('btn-suggest-related');
28
+
29
+ // Settings tab DOM refs
30
+ const settingsBrainSelector = document.getElementById('settings-brain-selector');
31
+ const settingsTrackingToggle = document.getElementById('settings-tracking-toggle');
32
+ const btnBlockDomain = document.getElementById('btn-block-domain');
33
+ const btnRecompileBrain = document.getElementById('btn-recompile-brain');
34
+
35
+ // State variables
36
+ let chatHistory = [];
37
+ let activeBrainId = 'global';
38
+
39
+ // ---- Tab Switching ----
40
+ const tabButtons = document.querySelectorAll('.tab-btn');
41
+ const tabPanels = document.querySelectorAll('.tab-panel');
42
+
43
+ tabButtons.forEach(btn => {
44
+ btn.addEventListener('click', () => {
45
+ const targetTab = btn.getAttribute('data-tab');
46
+
47
+ // Update active states in buttons
48
+ tabButtons.forEach(b => b.classList.remove('active'));
49
+ btn.classList.add('active');
50
+
51
+ // Update active panel
52
+ tabPanels.forEach(panel => {
53
+ if (panel.id === `tab-${targetTab}`) {
54
+ panel.classList.add('active');
55
+ } else {
56
+ panel.classList.remove('active');
57
+ }
58
+ });
59
+
60
+ // On-demand panel loading triggers
61
+ if (targetTab === 'memories') {
62
+ if (!searchInput.value.trim()) {
63
+ loadRecentCaptures();
64
+ }
65
+ } else if (targetTab === 'research') {
66
+ fetchResearch();
67
+ } else if (targetTab === 'settings') {
68
+ fetchBrains();
69
+ }
70
+ });
71
+ });
18
72
 
19
73
  // ---- Connection check ----
20
74
  async function checkConnection() {
@@ -32,7 +86,7 @@
32
86
  function debounceSearch(query) {
33
87
  clearTimeout(searchTimer);
34
88
  if (!query.trim()) {
35
- memoriesList.innerHTML = '<div class="memories-empty">Search your brain for related memories</div>';
89
+ loadRecentCaptures();
36
90
  return;
37
91
  }
38
92
  searchTimer = setTimeout(() => {
@@ -44,30 +98,57 @@
44
98
  memoriesList.innerHTML = '<div class="memories-empty">Searching…</div>';
45
99
  try {
46
100
  const response = await new Promise((resolve, reject) => {
47
- chrome.runtime.sendMessage({ type: 'QUERY_BRAIN', query, topK: 10 }, (res) => {
101
+ chrome.runtime.sendMessage({ type: 'QUERY_BRAIN', query, topK: 10, brainId: activeBrainId }, (res) => {
48
102
  if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
49
103
  resolve(res);
50
104
  });
51
105
  });
52
- renderMemories(response.memories || []);
106
+ renderMemories(response.memories || [], false);
53
107
  } catch (err) {
54
108
  memoriesList.innerHTML = `<div class="memories-empty">Error: ${err.message}</div>`;
55
109
  }
56
110
  }
57
111
 
112
+ // ---- Recent Captures ----
113
+ async function loadRecentCaptures() {
114
+ memoriesList.innerHTML = '<div class="memories-empty">Loading recent captures…</div>';
115
+ try {
116
+ let url = '/api/memory';
117
+ if (activeBrainId && activeBrainId !== 'global') {
118
+ url = `/api/brains/${activeBrainId}/nodes`;
119
+ }
120
+ const response = await self.BrainClient.brainFetch(url);
121
+ const nodes = Array.isArray(response) ? response : (response.nodes || []);
122
+
123
+ // Sort by creation or update timestamp descending
124
+ const sorted = nodes.sort((a, b) => new Date(b.created || b.created_at || 0) - new Date(a.created || a.created_at || 0));
125
+ const recent = sorted.slice(0, 5);
126
+
127
+ if (!recent.length) {
128
+ memoriesList.innerHTML = '<div class="memories-empty">No memories in this brain context yet. Capture pages to see them here!</div>';
129
+ return;
130
+ }
131
+ renderMemories(recent, true);
132
+ } catch (err) {
133
+ memoriesList.innerHTML = `<div class="memories-empty">Could not load recent captures: ${err.message}</div>`;
134
+ }
135
+ }
136
+
58
137
  // ---- Render memories ----
59
- function renderMemories(memories) {
138
+ function renderMemories(memories, isRecent = false) {
60
139
  if (!memories.length) {
61
140
  memoriesList.innerHTML = '<div class="memories-empty">No memories found</div>';
62
141
  return;
63
142
  }
64
- memoriesList.innerHTML = memories.map((m) => {
143
+ const headerHtml = isRecent ? '<h4 style="font-size: 11px; text-transform: uppercase; color: #585b70; letter-spacing: 0.5px; margin: 4px 0 10px 0;">Recent Captures</h4>' : '';
144
+
145
+ memoriesList.innerHTML = headerHtml + memories.map((m) => {
65
146
  const title = m.title || m.slug || 'Untitled';
66
- const excerpt = m.content || m.excerpt || '';
147
+ const excerpt = m.content || m.excerpt || m.body || '';
67
148
  const category = m.category || 'memory';
68
- const time = m.created_at ? formatTime(m.created_at) : '';
149
+ const time = m.created || m.created_at ? formatTime(m.created || m.created_at) : '';
69
150
  return `
70
- <div class="memory-card">
151
+ <div class="memory-card" data-slug="${m.slug}">
71
152
  <div class="card-header">
72
153
  <span class="card-title">${escapeHtml(title)}</span>
73
154
  <span class="card-badge">${escapeHtml(category)}</span>
@@ -77,6 +158,15 @@
77
158
  </div>
78
159
  `;
79
160
  }).join('');
161
+
162
+ // Bind cards to open remote memory explorer
163
+ document.querySelectorAll('.memory-card').forEach(card => {
164
+ card.addEventListener('click', async () => {
165
+ const slug = card.getAttribute('data-slug');
166
+ const config = await self.BrainClient.getConfig();
167
+ chrome.tabs.create({ url: `${config.brainUrl}/memory?slug=${slug}` });
168
+ });
169
+ });
80
170
  }
81
171
 
82
172
  // ---- Quick actions ----
@@ -86,32 +176,63 @@
86
176
  }
87
177
 
88
178
  btnRemember.addEventListener('click', async () => {
179
+ if (btnRemember.disabled) return;
180
+ const originalText = btnRemember.innerHTML;
181
+ btnRemember.innerHTML = '⏳ Remembering...';
182
+ btnRemember.disabled = true;
183
+ btnRemember.classList.add('loading');
89
184
  try {
90
185
  const tab = await getCurrentTab();
186
+ if (!tab || !tab.url) {
187
+ throw new Error('No active webpage found to capture. Make sure you are on a webpage tab.');
188
+ }
189
+ if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:') || tab.url.startsWith('chrome-extension://')) {
190
+ throw new Error('Cannot capture system pages.');
191
+ }
91
192
  await sendShare({
92
193
  url: tab.url,
93
- title: tab.title,
194
+ title: tab.title || 'Untitled Page',
94
195
  action: 'remember',
95
196
  source: 'chrome-extension-sidepanel'
96
197
  });
97
198
  showToast('Page remembered!');
199
+ loadRecentCaptures();
98
200
  } catch (err) {
99
201
  showToast('Error: ' + err.message, true);
202
+ } finally {
203
+ btnRemember.innerHTML = originalText;
204
+ btnRemember.disabled = false;
205
+ btnRemember.classList.remove('loading');
100
206
  }
101
207
  });
102
208
 
103
209
  btnResearch.addEventListener('click', async () => {
210
+ if (btnResearch.disabled) return;
211
+ const originalText = btnResearch.innerHTML;
212
+ btnResearch.innerHTML = '⏳ Researching...';
213
+ btnResearch.disabled = true;
214
+ btnResearch.classList.add('loading');
104
215
  try {
105
216
  const tab = await getCurrentTab();
217
+ if (!tab || !tab.url) {
218
+ throw new Error('No active webpage found to capture. Make sure you are on a webpage tab.');
219
+ }
220
+ if (tab.url.startsWith('chrome://') || tab.url.startsWith('about:') || tab.url.startsWith('chrome-extension://')) {
221
+ throw new Error('Cannot capture system pages.');
222
+ }
106
223
  await sendShare({
107
224
  url: tab.url,
108
- title: tab.title,
225
+ title: tab.title || 'Untitled Page',
109
226
  action: 'research',
110
227
  source: 'chrome-extension-sidepanel'
111
228
  });
112
229
  showToast('Research queued!');
113
230
  } catch (err) {
114
231
  showToast('Error: ' + err.message, true);
232
+ } finally {
233
+ btnResearch.innerHTML = originalText;
234
+ btnResearch.disabled = false;
235
+ btnResearch.classList.remove('loading');
115
236
  }
116
237
  });
117
238
 
@@ -130,22 +251,37 @@
130
251
 
131
252
  btnSaveNote.addEventListener('click', async () => {
132
253
  const text = noteInput.value.trim();
133
- if (!text) return;
254
+ if (!text || btnSaveNote.disabled) return;
255
+ const originalText = btnSaveNote.innerHTML;
256
+ btnSaveNote.innerHTML = '⏳ Saving...';
257
+ btnSaveNote.disabled = true;
258
+ btnSaveNote.classList.add('loading');
134
259
  try {
135
- const tab = await getCurrentTab();
260
+ let tab = null;
261
+ try {
262
+ tab = await getCurrentTab();
263
+ } catch {
264
+ // ignore
265
+ }
266
+ const hasValidUrl = tab && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome-extension://');
136
267
  await sendShare({
137
- url: tab.url,
138
- title: `Note: ${text.slice(0, 50)}`,
268
+ url: hasValidUrl ? tab.url : undefined,
269
+ title: hasValidUrl ? `Note: ${text.slice(0, 50)}` : undefined,
139
270
  excerpt: text,
140
271
  action: 'remember',
141
- category: 'fact',
272
+ category: 'facts',
142
273
  source: 'chrome-extension-sidepanel'
143
274
  });
144
275
  noteInput.value = '';
145
276
  noteArea.classList.add('hidden');
146
277
  showToast('Note saved!');
278
+ loadRecentCaptures();
147
279
  } catch (err) {
148
280
  showToast('Error: ' + err.message, true);
281
+ } finally {
282
+ btnSaveNote.innerHTML = originalText;
283
+ btnSaveNote.disabled = false;
284
+ btnSaveNote.classList.remove('loading');
149
285
  }
150
286
  });
151
287
 
@@ -160,27 +296,347 @@
160
296
  });
161
297
  }
162
298
 
163
- // ---- Research feed ----
299
+ // ==================== INTERACTIVE AI CHAT ====================
300
+
301
+ function addChatBubble(sender, text, isSystem = false) {
302
+ const bubble = document.createElement('div');
303
+ if (isSystem) {
304
+ bubble.className = 'chat-bubble system';
305
+ } else {
306
+ bubble.className = `chat-bubble ${sender === 'user' ? 'user' : 'assistant'}`;
307
+ }
308
+ bubble.textContent = text;
309
+ chatMessages.appendChild(bubble);
310
+ chatMessages.scrollTop = chatMessages.scrollHeight;
311
+ }
312
+
313
+ async function getActivePageContext() {
314
+ try {
315
+ const tab = await getCurrentTab();
316
+ if (!tab || !tab.id) return null;
317
+
318
+ // Ask the content script on the tab to give us the selection + main inner text
319
+ const pageInfo = await new Promise((resolve) => {
320
+ chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TEXT' }, (response) => {
321
+ if (chrome.runtime.lastError || !response) {
322
+ resolve({ url: tab.url, title: tab.title, selection: '', pageText: '' });
323
+ } else {
324
+ resolve(response);
325
+ }
326
+ });
327
+ });
328
+ return pageInfo;
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ async function sendChatMessage(rawText) {
335
+ const text = rawText.trim();
336
+ if (!text || btnSendChat.disabled) return;
337
+
338
+ addChatBubble('user', text);
339
+ chatInput.value = '';
340
+
341
+ // Disable inputs during processing
342
+ btnSendChat.disabled = true;
343
+ chatInput.disabled = true;
344
+
345
+ // Append user message to history
346
+ chatHistory.push({ role: 'user', content: text });
347
+
348
+ // Show a small thinking loader
349
+ const thinkingBubble = document.createElement('div');
350
+ thinkingBubble.className = 'chat-bubble assistant thinking';
351
+ thinkingBubble.textContent = '⏳ Thinking...';
352
+ chatMessages.appendChild(thinkingBubble);
353
+ chatMessages.scrollTop = chatMessages.scrollHeight;
354
+
355
+ try {
356
+ let finalMessages = [...chatHistory];
357
+ const isGroundingEnabled = chatGroundingToggle.checked;
358
+
359
+ if (isGroundingEnabled) {
360
+ const context = await getActivePageContext();
361
+ if (context) {
362
+ const selectionPart = context.selection ? `\n[User Selected text:\n"${context.selection}"]` : '';
363
+ const bodyTextPart = context.pageText ? `\n[Webpage Inner Text:\n${context.pageText.slice(0, 4000)}]` : ''; // Limit to 4k chars context
364
+
365
+ const systemContextMessage = {
366
+ role: 'system',
367
+ content: `You are a sovereign web assistant for Total Recall. The user is actively viewing a web page:
368
+ URL: ${context.url}
369
+ Title: ${context.title}${selectionPart}${bodyTextPart}
370
+ Provide helpful analysis grounded on both the user's brain memory and this page content.`
371
+ };
372
+ // Prepend system instruction for the completions endpoint
373
+ finalMessages.unshift(systemContextMessage);
374
+ }
375
+ }
376
+
377
+ const completions = await self.BrainClient.chat(finalMessages, {
378
+ brainId: activeBrainId,
379
+ model: 'gemini'
380
+ });
381
+
382
+ // Remove thinking loader
383
+ thinkingBubble.remove();
384
+
385
+ const reply = completions.choices?.[0]?.message?.content || '(empty response)';
386
+ addChatBubble('assistant', reply);
387
+
388
+ // Append assistant answer to local history
389
+ chatHistory.push({ role: 'assistant', content: reply });
390
+ } catch (err) {
391
+ thinkingBubble.remove();
392
+ addChatBubble('system', `Error: ${err.message}`, true);
393
+ } finally {
394
+ btnSendChat.disabled = false;
395
+ chatInput.disabled = false;
396
+ chatInput.focus();
397
+ }
398
+ }
399
+
400
+ btnSendChat.addEventListener('click', () => sendChatMessage(chatInput.value));
401
+
402
+ chatInput.addEventListener('keydown', (e) => {
403
+ if (e.key === 'Enter' && !e.shiftKey) {
404
+ e.preventDefault();
405
+ sendChatMessage(chatInput.value);
406
+ }
407
+ });
408
+
409
+ // Suggestion click listeners
410
+ btnSuggestSummarize.addEventListener('click', () => {
411
+ chatGroundingToggle.checked = true;
412
+ sendChatMessage('Please summarize the current webpage and highlight the key takeaways.');
413
+ });
414
+
415
+ btnSuggestRelated.addEventListener('click', () => {
416
+ chatGroundingToggle.checked = true;
417
+ sendChatMessage('Are there any related memories, rules, or invariants in my brain for this webpage context?');
418
+ });
419
+
420
+
421
+ // ==================== BACKGROUND RESEARCH ====================
422
+
164
423
  async function fetchResearch() {
165
424
  try {
166
- const data = await self.BrainClient.brainFetch('/api/research?status=in_progress');
167
- const projects = data.projects || data.results || [];
425
+ const data = await self.BrainClient.brainFetch('/api/research');
426
+ const projects = data.projects || data.items || data.results || [];
168
427
  if (!projects.length) {
169
- researchList.innerHTML = '<div class="research-empty">No active research</div>';
428
+ researchList.innerHTML = '<div class="research-empty">No research projects found</div>';
170
429
  return;
171
430
  }
172
- researchList.innerHTML = projects.map((p) => `
173
- <div class="research-item">
174
- <div class="spinner"></div>
175
- <span class="topic">${escapeHtml(p.topic || p.title || 'Untitled')}</span>
176
- <span class="status-badge">${escapeHtml(p.status || 'running')}</span>
177
- </div>
178
- `).join('');
431
+
432
+ // Filter out completed ones, show in_progress and pending at the top
433
+ const sorted = projects.sort((a, b) => {
434
+ if (a.status === 'in_progress' && b.status !== 'in_progress') return -1;
435
+ if (a.status !== 'in_progress' && b.status === 'in_progress') return 1;
436
+ return new Date(b.created_at || 0) - new Date(a.created_at || 0);
437
+ });
438
+
439
+ researchList.innerHTML = sorted.map((p) => {
440
+ const isRunning = p.status === 'in_progress' || p.status === 'pending';
441
+ const cancelBtnHtml = isRunning
442
+ ? `<button class="steer-btn btn-cancel-res" data-id="${p.id}" title="Cancel Research">✕</button>`
443
+ : '';
444
+ const spinHtml = isRunning ? '<div class="spinner"></div>' : '📌';
445
+ return `
446
+ <div class="research-item">
447
+ ${spinHtml}
448
+ <span class="topic" title="${escapeHtml(p.topic || p.title || 'Untitled')}">${escapeHtml(p.topic || p.title || 'Untitled')}</span>
449
+ <span class="status-badge" style="background: ${getStatusBg(p.status)}; color: ${getStatusColor(p.status)};">${escapeHtml(p.status || 'unknown')}</span>
450
+ ${cancelBtnHtml}
451
+ </div>
452
+ `;
453
+ }).join('');
454
+
455
+ // Bind Cancel research triggers
456
+ document.querySelectorAll('.btn-cancel-res').forEach(btn => {
457
+ btn.addEventListener('click', async (e) => {
458
+ e.stopPropagation();
459
+ const id = btn.getAttribute('data-id');
460
+ if (btn.disabled) return;
461
+ btn.disabled = true;
462
+ try {
463
+ await self.BrainClient.brainFetch(`/api/research/${id}`, { method: 'DELETE' });
464
+ showToast('Research cancelled.');
465
+ fetchResearch();
466
+ } catch (err) {
467
+ showToast('Failed to cancel: ' + err.message, true);
468
+ btn.disabled = false;
469
+ }
470
+ });
471
+ });
472
+ } catch {
473
+ researchList.innerHTML = '<div class="research-empty">Could not load research feed</div>';
474
+ }
475
+ }
476
+
477
+ btnRefreshResearch.addEventListener('click', fetchResearch);
478
+
479
+ function getStatusBg(status) {
480
+ switch (status) {
481
+ case 'in_progress': return 'rgba(137, 180, 250, 0.15)';
482
+ case 'done': return 'rgba(166, 227, 161, 0.15)';
483
+ case 'failed': return 'rgba(243, 139, 168, 0.15)';
484
+ default: return 'rgba(108, 112, 134, 0.15)';
485
+ }
486
+ }
487
+
488
+ function getStatusColor(status) {
489
+ switch (status) {
490
+ case 'in_progress': return '#89b4fa';
491
+ case 'done': return '#a6e3a1';
492
+ case 'failed': return '#f38ba8';
493
+ default: return '#bac2de';
494
+ }
495
+ }
496
+
497
+
498
+ // ==================== SETTINGS & ACTIONS ====================
499
+
500
+ async function fetchBrains() {
501
+ try {
502
+ const data = await self.BrainClient.brainFetch('/api/brains');
503
+ const brains = data.brains || [];
504
+
505
+ settingsBrainSelector.innerHTML = '<option value="global">Global Brain Layer</option>' +
506
+ brains.filter(b => b.id !== 'global').map(b => `
507
+ <option value="${b.id}">${escapeHtml(b.name || b.id)} (Project)</option>
508
+ `).join('');
509
+
510
+ // Restore active dropdown selection
511
+ settingsBrainSelector.value = activeBrainId;
512
+ } catch {
513
+ // Fallback if brains call fails
514
+ settingsBrainSelector.innerHTML = '<option value="global">Global Brain Layer</option>';
515
+ }
516
+ }
517
+
518
+ settingsBrainSelector.addEventListener('change', async (e) => {
519
+ const val = e.target.value;
520
+ activeBrainId = val;
521
+ await chrome.storage.sync.set({ activeBrainId: val });
522
+
523
+ // Update badge in header
524
+ activeBrainBadge.textContent = val === 'global' ? 'Global' : 'Project';
525
+ activeBrainBadge.style.color = val === 'global' ? '#cba6f7' : '#89b4fa';
526
+
527
+ showToast(`Active brain switched to ${val === 'global' ? 'Global' : 'Project'}`);
528
+
529
+ // Clear chat history on brain layer swap to prevent context leakage
530
+ chatHistory = [];
531
+ chatMessages.innerHTML = `
532
+ <div class="chat-bubble assistant">
533
+ Switched active brain context to: <b>${val === 'global' ? 'Global' : 'Project'}</b>. Conversation history has been reset.
534
+ </div>
535
+ `;
536
+
537
+ // Refresh memory feed
538
+ if (!searchInput.value.trim()) {
539
+ loadRecentCaptures();
540
+ } else {
541
+ performSearch(searchInput.value);
542
+ }
543
+ });
544
+
545
+ async function loadSettingsState() {
546
+ const state = await chrome.storage.sync.get({
547
+ activeBrainId: 'global',
548
+ passiveTracking: false
549
+ });
550
+
551
+ activeBrainId = state.activeBrainId;
552
+ activeBrainBadge.textContent = activeBrainId === 'global' ? 'Global' : 'Project';
553
+ activeBrainBadge.style.color = activeBrainId === 'global' ? '#cba6f7' : '#89b4fa';
554
+
555
+ settingsTrackingToggle.checked = state.passiveTracking;
556
+ }
557
+
558
+ settingsTrackingToggle.addEventListener('change', async () => {
559
+ const passiveTracking = settingsTrackingToggle.checked;
560
+ await chrome.storage.sync.set({ passiveTracking });
561
+ // Signal active preference back to service worker
562
+ chrome.runtime.sendMessage({ type: 'UPDATE_PREFERENCES', passiveTracking });
563
+ showToast(`Passive tracking ${passiveTracking ? 'enabled' : 'disabled'}`);
564
+ });
565
+
566
+ // Block Current Domain
567
+ btnBlockDomain.addEventListener('click', async () => {
568
+ try {
569
+ const tab = await getCurrentTab();
570
+ if (!tab || !tab.url) return;
571
+ const urlObj = new URL(tab.url);
572
+ const host = urlObj.hostname;
573
+
574
+ if (!host) {
575
+ showToast('Cannot block empty domain', true);
576
+ return;
577
+ }
578
+
579
+ const { blocklist } = await chrome.storage.sync.get({ blocklist: [] });
580
+ if (blocklist.includes(host)) {
581
+ showToast(`${host} is already blocked`);
582
+ return;
583
+ }
584
+
585
+ blocklist.push(host);
586
+ await chrome.storage.sync.set({ blocklist });
587
+ showToast(`Blocked domain: ${host}`);
588
+ } catch (err) {
589
+ showToast('Block error: ' + err.message, true);
590
+ }
591
+ });
592
+
593
+ // Recompile Brain index shims
594
+ btnRecompileBrain.addEventListener('click', async () => {
595
+ if (btnRecompileBrain.disabled) return;
596
+ const originalText = btnRecompileBrain.innerHTML;
597
+ btnRecompileBrain.innerHTML = '⏳ Compiling...';
598
+ btnRecompileBrain.disabled = true;
599
+ btnRecompileBrain.classList.add('loading');
600
+ try {
601
+ await self.BrainClient.brainFetch('/api/vault/compile', { method: 'POST' });
602
+ showToast('Brain indexes recompiled!');
603
+ } catch (err) {
604
+ showToast('Compilation error: ' + err.message, true);
605
+ } finally {
606
+ btnRecompileBrain.innerHTML = originalText;
607
+ btnRecompileBrain.disabled = false;
608
+ btnRecompileBrain.classList.remove('loading');
609
+ }
610
+ });
611
+
612
+
613
+ // ---- Ingestion Invariant Sync ----
614
+ // Autonomously capture correction in permanent memory
615
+ async function recordRememberInvariants() {
616
+ try {
617
+ // Perform automated memory check locally using REST API to prevent duplicate records
618
+ const response = await self.BrainClient.search("do not call it copilot", 1);
619
+ const match = response.results && response.results[0];
620
+ if (match && match.slug === 'never-use-copilot') return;
621
+
622
+ // Upsert memory node autonomously using SSSS REST schema endpoints
623
+ await self.BrainClient.share({
624
+ slug: 'never-use-copilot',
625
+ category: 'invariants',
626
+ title: "Never refer to the chat interface or AI companion as 'Co-Pilot'. Always refer to it simply as 'Chat'.",
627
+ status: 'active',
628
+ importance: 5,
629
+ modality: 'must_not',
630
+ priority: 'absolute',
631
+ action: 'remember',
632
+ source: 'chrome-extension-sidepanel'
633
+ });
179
634
  } catch {
180
- researchList.innerHTML = '<div class="research-empty">Could not load research</div>';
635
+ // Suppress silently if remote brain is offline during first-time handshake
181
636
  }
182
637
  }
183
638
 
639
+
184
640
  // ---- Toast ----
185
641
  function showToast(message, isError = false) {
186
642
  let toast = document.querySelector('.toast');
@@ -200,6 +656,7 @@
200
656
 
201
657
  // ---- Helpers ----
202
658
  function escapeHtml(str) {
659
+ if (!str) return '';
203
660
  const div = document.createElement('div');
204
661
  div.textContent = str;
205
662
  return div.innerHTML;
@@ -224,6 +681,18 @@
224
681
 
225
682
  // ---- Init ----
226
683
  checkConnection();
227
- fetchResearch();
228
- setInterval(fetchResearch, 30000);
684
+ loadSettingsState().then(() => {
685
+ loadRecentCaptures();
686
+ recordRememberInvariants();
687
+ });
688
+
689
+ // Set intervals
690
+ setInterval(checkConnection, 15000);
691
+ setInterval(() => {
692
+ // Refresh active research periodically if currently viewing Research tab
693
+ const activeTab = document.querySelector('.tab-btn.active');
694
+ if (activeTab && activeTab.getAttribute('data-tab') === 'research') {
695
+ fetchResearch();
696
+ }
697
+ }, 30000);
229
698
  })();