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.
- package/extension/background.js +1 -1
- package/extension/content-script.js +15 -0
- package/extension/lib/brain-client.js +81 -42
- package/extension/lib/preconfigured.js +5 -0
- package/extension/manifest.json +1 -1
- package/extension/options/options.html +1 -0
- package/extension/options/options.js +3 -2
- package/extension/popup/popup.css +6 -0
- package/extension/popup/popup.html +1 -0
- package/extension/popup/popup.js +47 -4
- package/extension/sidepanel/sidepanel.css +426 -29
- package/extension/sidepanel/sidepanel.html +119 -20
- package/extension/sidepanel/sidepanel.js +498 -29
- package/frontend/src/pages/IntegrationsPage.tsx +140 -1
- package/package.json +1 -1
- package/src/cli/init.mjs +30 -0
- package/src/cli/setup.mjs +10 -1
- package/src/server/rest.mjs +47 -9
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
//
|
|
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
|
|
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
|
|
428
|
+
researchList.innerHTML = '<div class="research-empty">No research projects found</div>';
|
|
170
429
|
return;
|
|
171
430
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
})();
|