total-recall-brain 3.28.2 → 3.30.1
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/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +22 -6
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/connect.mjs +10 -1
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +76 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
|
@@ -1,1106 +1,952 @@
|
|
|
1
|
-
// Total Recall —
|
|
1
|
+
// Total Recall — side panel
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const searchInput = document.getElementById('search-input');
|
|
10
|
-
const memoriesList = document.getElementById('memories-list');
|
|
11
|
-
const btnRemember = document.getElementById('btn-remember');
|
|
12
|
-
const btnResearch = document.getElementById('btn-research');
|
|
13
|
-
const btnNote = document.getElementById('btn-note');
|
|
14
|
-
const noteArea = document.getElementById('note-area');
|
|
15
|
-
const noteInput = document.getElementById('note-input');
|
|
16
|
-
const btnSaveNote = document.getElementById('btn-save-note');
|
|
17
|
-
const btnCancelNote = document.getElementById('btn-cancel-note');
|
|
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
|
-
});
|
|
6
|
+
const Brain = self.BrainClient;
|
|
7
|
+
const UI = self.TRUi;
|
|
8
|
+
const $ = (id) => document.getElementById(id);
|
|
59
9
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
} else if (targetTab === 'research') {
|
|
66
|
-
fetchResearch();
|
|
67
|
-
} else if (targetTab === 'settings') {
|
|
68
|
-
fetchBrains();
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
});
|
|
10
|
+
const TABS = ['recall', 'chat', 'research'];
|
|
11
|
+
const CHAT_HISTORY_LIMIT = 40;
|
|
12
|
+
const CHAT_CONTEXT_TURNS = 12;
|
|
13
|
+
const PAGE_TEXT_FOR_CHAT = 6000;
|
|
72
14
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
15
|
+
const state = {
|
|
16
|
+
tab: 'recall',
|
|
17
|
+
page: null, // chrome.tabs.Tab for the active tab
|
|
18
|
+
activeBrainId: 'global',
|
|
19
|
+
brains: [],
|
|
20
|
+
connection: 'checking',
|
|
21
|
+
chat: [],
|
|
22
|
+
chatBusy: false,
|
|
23
|
+
research: { items: [], counts: {} },
|
|
24
|
+
settings: { pageRecall: false, showPill: true, blocklist: [] },
|
|
25
|
+
health: null,
|
|
26
|
+
};
|
|
82
27
|
|
|
83
|
-
//
|
|
84
|
-
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Small utilities
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
85
31
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}, 300);
|
|
32
|
+
function svg(name, cls = 'icon sm') {
|
|
33
|
+
const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
34
|
+
el.setAttribute('class', cls);
|
|
35
|
+
el.setAttribute('aria-hidden', 'true');
|
|
36
|
+
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
37
|
+
use.setAttribute('href', `#i-${name}`);
|
|
38
|
+
el.appendChild(use);
|
|
39
|
+
return el;
|
|
95
40
|
}
|
|
96
41
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
|
|
103
|
-
resolve(res);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
renderMemories(response.memories || [], false);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
memoriesList.innerHTML = `<div class="memories-empty">Error: ${err.message}</div>`;
|
|
109
|
-
}
|
|
42
|
+
function el(tag, className, text) {
|
|
43
|
+
const node = document.createElement(tag);
|
|
44
|
+
if (className) node.className = className;
|
|
45
|
+
if (text !== undefined && text !== null) node.textContent = text;
|
|
46
|
+
return node;
|
|
110
47
|
}
|
|
111
48
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
}
|
|
49
|
+
function setBusy(button, busy) {
|
|
50
|
+
button.disabled = busy;
|
|
51
|
+
button.classList.toggle('is-busy', busy);
|
|
135
52
|
}
|
|
136
53
|
|
|
137
|
-
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const category = m.category || 'memory';
|
|
149
|
-
const time = m.created || m.created_at ? formatTime(m.created || m.created_at) : '';
|
|
150
|
-
return `
|
|
151
|
-
<div class="memory-card" data-slug="${m.slug}">
|
|
152
|
-
<div class="card-header">
|
|
153
|
-
<span class="card-title">${escapeHtml(title)}</span>
|
|
154
|
-
<span class="card-badge">${escapeHtml(category)}</span>
|
|
155
|
-
</div>
|
|
156
|
-
<div class="card-excerpt">${escapeHtml(excerpt)}</div>
|
|
157
|
-
${time ? `<div class="card-time">${time}</div>` : ''}
|
|
158
|
-
</div>
|
|
159
|
-
`;
|
|
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
|
-
});
|
|
54
|
+
let toastTimer = null;
|
|
55
|
+
function toast(message, isError = false) {
|
|
56
|
+
const t = $('toast');
|
|
57
|
+
t.textContent = message;
|
|
58
|
+
t.classList.toggle('error', isError);
|
|
59
|
+
t.hidden = false;
|
|
60
|
+
t.style.animation = 'none';
|
|
61
|
+
void t.offsetWidth;
|
|
62
|
+
t.style.animation = '';
|
|
63
|
+
clearTimeout(toastTimer);
|
|
64
|
+
toastTimer = setTimeout(() => { t.hidden = true; }, isError ? 4200 : 2600);
|
|
170
65
|
}
|
|
171
66
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
175
|
-
return tab;
|
|
67
|
+
function describeError(err) {
|
|
68
|
+
return (err && err.message) || 'Something went wrong.';
|
|
176
69
|
}
|
|
177
70
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
btnRemember.innerHTML = '⏳ Remembering...';
|
|
182
|
-
btnRemember.disabled = true;
|
|
183
|
-
btnRemember.classList.add('loading');
|
|
184
|
-
try {
|
|
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
|
-
}
|
|
192
|
-
await sendShare({
|
|
193
|
-
url: tab.url,
|
|
194
|
-
title: tab.title || 'Untitled Page',
|
|
195
|
-
action: 'remember',
|
|
196
|
-
source: 'chrome-extension-sidepanel'
|
|
197
|
-
});
|
|
198
|
-
showToast('Page remembered!');
|
|
199
|
-
loadRecentCaptures();
|
|
200
|
-
} catch (err) {
|
|
201
|
-
showToast('Error: ' + err.message, true);
|
|
202
|
-
} finally {
|
|
203
|
-
btnRemember.innerHTML = originalText;
|
|
204
|
-
btnRemember.disabled = false;
|
|
205
|
-
btnRemember.classList.remove('loading');
|
|
206
|
-
}
|
|
207
|
-
});
|
|
71
|
+
function isAuthOrOffline(err) {
|
|
72
|
+
return err && ['offline', 'unauthorized', 'unconfigured', 'timeout'].includes(err.code);
|
|
73
|
+
}
|
|
208
74
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
btnResearch.disabled = true;
|
|
214
|
-
btnResearch.classList.add('loading');
|
|
215
|
-
try {
|
|
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
|
-
}
|
|
223
|
-
await sendShare({
|
|
224
|
-
url: tab.url,
|
|
225
|
-
title: tab.title || 'Untitled Page',
|
|
226
|
-
action: 'research',
|
|
227
|
-
source: 'chrome-extension-sidepanel'
|
|
228
|
-
});
|
|
229
|
-
showToast('Research queued!');
|
|
230
|
-
} catch (err) {
|
|
231
|
-
showToast('Error: ' + err.message, true);
|
|
232
|
-
} finally {
|
|
233
|
-
btnResearch.innerHTML = originalText;
|
|
234
|
-
btnResearch.disabled = false;
|
|
235
|
-
btnResearch.classList.remove('loading');
|
|
236
|
-
}
|
|
237
|
-
});
|
|
75
|
+
async function openMemory(slug) {
|
|
76
|
+
const url = await Brain.dashboardUrl(`/memory?slug=${encodeURIComponent(slug)}`);
|
|
77
|
+
chrome.tabs.create({ url });
|
|
78
|
+
}
|
|
238
79
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (!noteArea.classList.contains('hidden')) {
|
|
243
|
-
noteInput.focus();
|
|
244
|
-
}
|
|
245
|
-
});
|
|
80
|
+
function skeleton(container, rows = 3) {
|
|
81
|
+
container.replaceChildren(...Array.from({ length: rows }, () => el('div', 'skeleton skeleton-row')));
|
|
82
|
+
}
|
|
246
83
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Connection
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
251
87
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
btnSaveNote.classList.add('loading');
|
|
259
|
-
try {
|
|
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://');
|
|
267
|
-
await sendShare({
|
|
268
|
-
url: hasValidUrl ? tab.url : undefined,
|
|
269
|
-
title: hasValidUrl ? `Note: ${text.slice(0, 50)}` : undefined,
|
|
270
|
-
excerpt: text,
|
|
271
|
-
action: 'remember',
|
|
272
|
-
category: 'facts',
|
|
273
|
-
source: 'chrome-extension-sidepanel'
|
|
274
|
-
});
|
|
275
|
-
noteInput.value = '';
|
|
276
|
-
noteArea.classList.add('hidden');
|
|
277
|
-
showToast('Note saved!');
|
|
278
|
-
loadRecentCaptures();
|
|
279
|
-
} catch (err) {
|
|
280
|
-
showToast('Error: ' + err.message, true);
|
|
281
|
-
} finally {
|
|
282
|
-
btnSaveNote.innerHTML = originalText;
|
|
283
|
-
btnSaveNote.disabled = false;
|
|
284
|
-
btnSaveNote.classList.remove('loading');
|
|
285
|
-
}
|
|
286
|
-
});
|
|
88
|
+
const GATE_COPY = {
|
|
89
|
+
offline: ['Brain unreachable', 'Total Recall isn’t answering at the configured address. Make sure it’s running, or change the address in connection settings.'],
|
|
90
|
+
timeout: ['Brain unreachable', 'The brain took too long to answer. It may be busy; retry in a moment.'],
|
|
91
|
+
unconfigured: ['Connect your brain', 'Add an access token so the extension can read and write your memories.'],
|
|
92
|
+
unauthorized: ['Access token rejected', 'The brain didn’t accept the saved token. It may have been revoked or expired; paste a new one in connection settings.'],
|
|
93
|
+
};
|
|
287
94
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
|
|
293
|
-
if (res && res.success) return resolve(res);
|
|
294
|
-
reject(new Error(res?.error || 'Unknown error'));
|
|
295
|
-
});
|
|
296
|
-
});
|
|
95
|
+
function setStatusDot(kind, title) {
|
|
96
|
+
const dot = $('status-dot');
|
|
97
|
+
dot.className = `status-dot ${kind}`;
|
|
98
|
+
$('brain-chip').title = title || 'Active brain — change in settings';
|
|
297
99
|
}
|
|
298
100
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
bubble.className = `chat-bubble ${sender === 'user' ? 'user' : 'assistant'}`;
|
|
307
|
-
}
|
|
308
|
-
bubble.textContent = text;
|
|
309
|
-
chatMessages.appendChild(bubble);
|
|
310
|
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
101
|
+
function showGate(err) {
|
|
102
|
+
const [title, message] = GATE_COPY[err.code] || GATE_COPY.offline;
|
|
103
|
+
$('gate-title').textContent = title;
|
|
104
|
+
$('gate-message').textContent = message;
|
|
105
|
+
$('gate').hidden = false;
|
|
106
|
+
$('app').classList.add('is-gated');
|
|
107
|
+
setStatusDot('down', `${title}. ${err.message || ''}`.trim());
|
|
311
108
|
}
|
|
312
109
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
}
|
|
110
|
+
function hideGate() {
|
|
111
|
+
$('gate').hidden = true;
|
|
112
|
+
$('app').classList.remove('is-gated');
|
|
332
113
|
}
|
|
333
114
|
|
|
334
|
-
async function
|
|
335
|
-
|
|
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
|
-
|
|
115
|
+
async function checkConnection({ quiet = false } = {}) {
|
|
116
|
+
if (!quiet) setStatusDot('checking');
|
|
355
117
|
try {
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 });
|
|
118
|
+
// /health is public; an authenticated read proves the token works too.
|
|
119
|
+
const [health] = await Promise.all([Brain.health(), Brain.request('/api/memory?limit=1')]);
|
|
120
|
+
state.health = health;
|
|
121
|
+
const wasGated = state.connection !== 'ok';
|
|
122
|
+
state.connection = 'ok';
|
|
123
|
+
hideGate();
|
|
124
|
+
setStatusDot(health && health.daemon && health.daemon !== 'running' ? 'warn' : 'ok',
|
|
125
|
+
`Connected · brain v${(health && health.version) || '?'}${health && health.daemon !== 'running' ? ' · daemon stopped' : ''}`);
|
|
126
|
+
renderSheetFooter();
|
|
127
|
+
return wasGated;
|
|
390
128
|
} catch (err) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
129
|
+
if (isAuthOrOffline(err)) {
|
|
130
|
+
state.connection = err.code;
|
|
131
|
+
showGate(err);
|
|
132
|
+
} else {
|
|
133
|
+
// Reachable but something narrower failed (e.g. scope); keep the UI usable.
|
|
134
|
+
state.connection = 'ok';
|
|
135
|
+
hideGate();
|
|
136
|
+
setStatusDot('warn', err.message);
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Tabs
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
function switchTab(name, { focus = false } = {}) {
|
|
147
|
+
if (!TABS.includes(name)) return;
|
|
148
|
+
state.tab = name;
|
|
149
|
+
TABS.forEach((t, i) => {
|
|
150
|
+
const btn = $(`tab-btn-${t}`);
|
|
151
|
+
const active = t === name;
|
|
152
|
+
btn.classList.toggle('is-active', active);
|
|
153
|
+
btn.setAttribute('aria-selected', String(active));
|
|
154
|
+
btn.tabIndex = active ? 0 : -1;
|
|
155
|
+
const pane = $(`tab-${t}`);
|
|
156
|
+
pane.hidden = !active;
|
|
157
|
+
pane.classList.toggle('is-active', active);
|
|
158
|
+
if (active) document.querySelector('.tab-indicator').style.transform = `translateX(${i * 100}%)`;
|
|
159
|
+
});
|
|
160
|
+
if (focus) $(`tab-btn-${name}`).focus();
|
|
161
|
+
if (name === 'research') loadResearch();
|
|
162
|
+
if (name === 'chat') {
|
|
163
|
+
scrollChatToEnd();
|
|
164
|
+
setTimeout(() => $('chat-input').focus(), 50);
|
|
397
165
|
}
|
|
166
|
+
chrome.storage.session.set({ lastTab: name }).catch(() => {});
|
|
398
167
|
}
|
|
399
168
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
e.
|
|
405
|
-
|
|
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?');
|
|
169
|
+
document.querySelectorAll('.tab').forEach((btn) => {
|
|
170
|
+
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
|
|
171
|
+
btn.addEventListener('keydown', (e) => {
|
|
172
|
+
const i = TABS.indexOf(btn.dataset.tab);
|
|
173
|
+
if (e.key === 'ArrowRight') switchTab(TABS[(i + 1) % TABS.length], { focus: true });
|
|
174
|
+
if (e.key === 'ArrowLeft') switchTab(TABS[(i + TABS.length - 1) % TABS.length], { focus: true });
|
|
175
|
+
});
|
|
418
176
|
});
|
|
419
177
|
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Recall: current page
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
420
181
|
|
|
421
|
-
|
|
182
|
+
let pageToken = 0;
|
|
422
183
|
|
|
423
|
-
async function
|
|
184
|
+
async function refreshPage() {
|
|
185
|
+
const token = ++pageToken;
|
|
186
|
+
let tab = null;
|
|
424
187
|
try {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
188
|
+
[tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
189
|
+
} catch { /* ignore */ }
|
|
190
|
+
if (token !== pageToken) return;
|
|
191
|
+
state.page = tab || null;
|
|
192
|
+
renderPageCard();
|
|
193
|
+
if (state.connection !== 'ok') return;
|
|
194
|
+
loadPageSavedState(token);
|
|
195
|
+
loadRelated(token);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function renderPageCard() {
|
|
199
|
+
const tab = state.page;
|
|
200
|
+
const capturable = tab && UI.isCapturableUrl(tab.url);
|
|
201
|
+
$('page-title').textContent = (tab && tab.title) || (tab ? tab.url : 'No active tab');
|
|
202
|
+
$('page-host').textContent = capturable ? UI.hostnameOf(tab.url) : (tab && tab.url ? tab.url.split(':')[0] + ' page' : '');
|
|
203
|
+
$('page-saved').hidden = true;
|
|
204
|
+
$('btn-remember').disabled = !capturable;
|
|
205
|
+
$('btn-research-page').disabled = !capturable;
|
|
206
|
+
$('page-disabled').hidden = !!capturable;
|
|
207
|
+
|
|
208
|
+
const fav = $('page-favicon');
|
|
209
|
+
fav.replaceChildren(svg('globe'));
|
|
210
|
+
if (tab && tab.favIconUrl && /^(https?:|data:image\/)/.test(tab.favIconUrl)) {
|
|
211
|
+
const img = new Image();
|
|
212
|
+
img.alt = '';
|
|
213
|
+
img.referrerPolicy = 'no-referrer';
|
|
214
|
+
img.onload = () => fav.replaceChildren(img);
|
|
215
|
+
img.src = tab.favIconUrl;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
renderBlockButton();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function loadPageSavedState(token) {
|
|
222
|
+
const tab = state.page;
|
|
223
|
+
if (!tab || !UI.isCapturableUrl(tab.url)) return;
|
|
224
|
+
try {
|
|
225
|
+
const matches = await Brain.findByUrl(tab.url);
|
|
226
|
+
if (token !== pageToken) return;
|
|
227
|
+
const newest = matches
|
|
228
|
+
.map((n) => Date.parse(n.created) || 0)
|
|
229
|
+
.sort((a, b) => b - a)[0];
|
|
230
|
+
if (matches.length) {
|
|
231
|
+
$('page-saved-label').textContent = newest ? `Saved ${UI.timeAgo(newest)}` : 'Saved';
|
|
232
|
+
$('page-saved').hidden = false;
|
|
430
233
|
}
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
});
|
|
234
|
+
} catch { /* non-critical */ }
|
|
235
|
+
}
|
|
438
236
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
});
|
|
237
|
+
async function loadRelated(token) {
|
|
238
|
+
const tab = state.page;
|
|
239
|
+
const section = $('related-section');
|
|
240
|
+
if (!tab || !UI.isCapturableUrl(tab.url) || !tab.title) {
|
|
241
|
+
section.hidden = true;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const results = await Brain.search(tab.title, { topK: 10 });
|
|
246
|
+
if (token !== pageToken) return;
|
|
247
|
+
const related = UI.filterRelated(results, { excludeUrl: tab.url, limit: 4 });
|
|
248
|
+
section.hidden = related.length === 0;
|
|
249
|
+
renderRows($('related-list'), related, { showSimilarity: true });
|
|
472
250
|
} catch {
|
|
473
|
-
|
|
251
|
+
if (token === pageToken) section.hidden = true;
|
|
474
252
|
}
|
|
475
253
|
}
|
|
476
254
|
|
|
477
|
-
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Recall: rows, search, recent
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
function toRow(node) {
|
|
260
|
+
return {
|
|
261
|
+
slug: node.slug,
|
|
262
|
+
title: node.title || node.slug,
|
|
263
|
+
excerpt: node.excerpt !== undefined ? node.excerpt : UI.clip(UI.stripMarkdown(node.content || node.body || ''), 200),
|
|
264
|
+
category: node.category,
|
|
265
|
+
created: node.created,
|
|
266
|
+
similarity: node.similarity,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
478
269
|
|
|
479
|
-
function
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
case 'failed': return 'rgba(243, 139, 168, 0.15)';
|
|
484
|
-
default: return 'rgba(108, 112, 134, 0.15)';
|
|
270
|
+
function renderRows(container, items, { showSimilarity = false, empty } = {}) {
|
|
271
|
+
if (!items.length) {
|
|
272
|
+
container.replaceChildren(empty ? el('div', 'empty', empty) : document.createDocumentFragment());
|
|
273
|
+
return;
|
|
485
274
|
}
|
|
275
|
+
container.replaceChildren(...items.map((item) => {
|
|
276
|
+
const row = el('button', 'row');
|
|
277
|
+
row.type = 'button';
|
|
278
|
+
row.title = 'Open in dashboard';
|
|
279
|
+
row.append(el('span', 'row-title', item.title));
|
|
280
|
+
if (item.excerpt) row.append(el('span', 'row-excerpt', item.excerpt));
|
|
281
|
+
const meta = el('span', 'row-meta');
|
|
282
|
+
if (item.category) meta.append(el('span', 'cat', String(item.category).replace(/[-_]/g, ' ')));
|
|
283
|
+
const time = item.created ? UI.timeAgo(item.created) : '';
|
|
284
|
+
if (time) { meta.append(el('span', 'sep')); meta.append(el('span', '', time)); }
|
|
285
|
+
if (showSimilarity && typeof item.similarity === 'number') {
|
|
286
|
+
meta.append(el('span', 'sep'));
|
|
287
|
+
meta.append(el('span', '', `${Math.round(item.similarity * 100)}% similar`));
|
|
288
|
+
}
|
|
289
|
+
row.append(meta);
|
|
290
|
+
row.addEventListener('click', () => openMemory(item.slug));
|
|
291
|
+
return row;
|
|
292
|
+
}));
|
|
486
293
|
}
|
|
487
294
|
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
295
|
+
async function loadRecent() {
|
|
296
|
+
const list = $('recent-list');
|
|
297
|
+
if (!list.childElementCount) skeleton(list, 3);
|
|
298
|
+
try {
|
|
299
|
+
const nodes = await Brain.listRecent(6);
|
|
300
|
+
renderRows(list, nodes.map(toRow), { empty: 'Nothing here yet. Remember a page or save a note and it shows up here.' });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (isAuthOrOffline(err)) return checkConnection();
|
|
303
|
+
list.replaceChildren(el('div', 'error-note', `Couldn’t load recent memories: ${describeError(err)}`));
|
|
494
304
|
}
|
|
495
305
|
}
|
|
496
306
|
|
|
307
|
+
let searchTimer = null;
|
|
308
|
+
let searchToken = 0;
|
|
497
309
|
|
|
498
|
-
|
|
310
|
+
function onSearchInput() {
|
|
311
|
+
const query = $('search-input').value.trim();
|
|
312
|
+
clearTimeout(searchTimer);
|
|
313
|
+
if (!query) {
|
|
314
|
+
searchToken++;
|
|
315
|
+
$('search-results').hidden = true;
|
|
316
|
+
$('browse').hidden = false;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
$('search-results').hidden = false;
|
|
320
|
+
$('browse').hidden = true;
|
|
321
|
+
$('search-heading').textContent = 'Searching…';
|
|
322
|
+
skeleton($('search-list'), 3);
|
|
323
|
+
searchTimer = setTimeout(() => runSearch(query), 250);
|
|
324
|
+
}
|
|
499
325
|
|
|
500
|
-
async function
|
|
326
|
+
async function runSearch(query) {
|
|
327
|
+
const token = ++searchToken;
|
|
501
328
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
settingsBrainSelector.value = activeBrainId;
|
|
512
|
-
} catch {
|
|
513
|
-
// Fallback if brains call fails
|
|
514
|
-
settingsBrainSelector.innerHTML = '<option value="global">Global Brain Layer</option>';
|
|
329
|
+
const results = await Brain.search(query, { topK: 12 });
|
|
330
|
+
if (token !== searchToken) return;
|
|
331
|
+
const rows = results.filter((r) => r.type !== 'session' && r.slug).map(toRow);
|
|
332
|
+
$('search-heading').textContent = rows.length ? `${rows.length} result${rows.length === 1 ? '' : 's'}` : 'Results';
|
|
333
|
+
renderRows($('search-list'), rows, { showSimilarity: true, empty: `No memories match “${query}”.` });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (token !== searchToken) return;
|
|
336
|
+
$('search-heading').textContent = 'Results';
|
|
337
|
+
$('search-list').replaceChildren(el('div', 'error-note', describeError(err)));
|
|
515
338
|
}
|
|
516
339
|
}
|
|
517
340
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
}
|
|
341
|
+
$('search-input').addEventListener('input', onSearchInput);
|
|
342
|
+
$('search-input').addEventListener('keydown', (e) => {
|
|
343
|
+
if (e.key === 'Escape') { $('search-input').value = ''; onSearchInput(); }
|
|
543
344
|
});
|
|
345
|
+
$('btn-refresh-recent').addEventListener('click', loadRecent);
|
|
544
346
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// Capture
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
function sendCapture(options) {
|
|
352
|
+
return new Promise((resolve, reject) => {
|
|
353
|
+
chrome.runtime.sendMessage({ type: 'CAPTURE', tabId: state.page && state.page.id, options }, (res) => {
|
|
354
|
+
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
|
|
355
|
+
if (res && res.success) return resolve(res.result);
|
|
356
|
+
const err = new Error((res && res.error) || 'Capture failed');
|
|
357
|
+
err.code = res && res.code;
|
|
358
|
+
reject(err);
|
|
359
|
+
});
|
|
549
360
|
});
|
|
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
361
|
}
|
|
557
362
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
363
|
+
async function afterCapture(action) {
|
|
364
|
+
if (action === 'research') {
|
|
365
|
+
loadResearch();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const tab = state.page;
|
|
369
|
+
if (tab && UI.isCapturableUrl(tab.url)) {
|
|
370
|
+
$('page-saved-label').textContent = 'Saved just now';
|
|
371
|
+
$('page-saved').hidden = false;
|
|
372
|
+
}
|
|
373
|
+
loadRecent();
|
|
374
|
+
}
|
|
563
375
|
|
|
564
|
-
|
|
565
|
-
|
|
376
|
+
$('btn-remember').addEventListener('click', async () => {
|
|
377
|
+
const btn = $('btn-remember');
|
|
378
|
+
setBusy(btn, true);
|
|
566
379
|
try {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const host = urlObj.hostname;
|
|
571
|
-
|
|
572
|
-
if (!host) {
|
|
573
|
-
showToast('Cannot block empty domain', true);
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const { blocklist } = await chrome.storage.sync.get({ blocklist: [] });
|
|
578
|
-
if (blocklist.includes(host)) {
|
|
579
|
-
showToast(`${host} is already blocked`);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
blocklist.push(host);
|
|
584
|
-
await chrome.storage.sync.set({ blocklist });
|
|
585
|
-
showToast(`Blocked domain: ${host}`);
|
|
380
|
+
await sendCapture({ action: 'remember' });
|
|
381
|
+
toast('Saved to your brain');
|
|
382
|
+
afterCapture('remember');
|
|
586
383
|
} catch (err) {
|
|
587
|
-
|
|
384
|
+
toast(describeError(err), true);
|
|
385
|
+
} finally {
|
|
386
|
+
setBusy(btn, false);
|
|
588
387
|
}
|
|
589
388
|
});
|
|
590
389
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const originalText = btnRecompileBrain.innerHTML;
|
|
595
|
-
btnRecompileBrain.innerHTML = '⏳ Compiling...';
|
|
596
|
-
btnRecompileBrain.disabled = true;
|
|
597
|
-
btnRecompileBrain.classList.add('loading');
|
|
390
|
+
$('btn-research-page').addEventListener('click', async () => {
|
|
391
|
+
const btn = $('btn-research-page');
|
|
392
|
+
setBusy(btn, true);
|
|
598
393
|
try {
|
|
599
|
-
await
|
|
600
|
-
|
|
394
|
+
await sendCapture({ action: 'research' });
|
|
395
|
+
toast('Research queued');
|
|
396
|
+
afterCapture('research');
|
|
601
397
|
} catch (err) {
|
|
602
|
-
|
|
398
|
+
toast(describeError(err), true);
|
|
603
399
|
} finally {
|
|
604
|
-
|
|
605
|
-
btnRecompileBrain.disabled = false;
|
|
606
|
-
btnRecompileBrain.classList.remove('loading');
|
|
400
|
+
setBusy(btn, false);
|
|
607
401
|
}
|
|
608
402
|
});
|
|
609
403
|
|
|
404
|
+
function toggleNote(open) {
|
|
405
|
+
const area = $('note-area');
|
|
406
|
+
area.hidden = !open;
|
|
407
|
+
$('btn-note').setAttribute('aria-expanded', String(open));
|
|
408
|
+
if (open) $('note-input').focus();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
$('btn-note').addEventListener('click', () => toggleNote($('note-area').hidden));
|
|
412
|
+
$('btn-cancel-note').addEventListener('click', () => { $('note-input').value = ''; toggleNote(false); });
|
|
610
413
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
414
|
+
async function saveNote() {
|
|
415
|
+
const note = $('note-input').value.trim();
|
|
416
|
+
const btn = $('btn-save-note');
|
|
417
|
+
if (!note || btn.disabled) return;
|
|
418
|
+
setBusy(btn, true);
|
|
614
419
|
try {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
action: 'remember',
|
|
630
|
-
source: 'chrome-extension-sidepanel'
|
|
631
|
-
});
|
|
632
|
-
} catch {
|
|
633
|
-
// Suppress silently if remote brain is offline during first-time handshake
|
|
420
|
+
const tab = state.page;
|
|
421
|
+
if (tab && UI.isCapturableUrl(tab.url)) {
|
|
422
|
+
await sendCapture({ action: 'remember', note, selection: '' });
|
|
423
|
+
} else {
|
|
424
|
+
await Brain.share({ excerpt: note, title: UI.clip(note.split('\n')[0], 80), action: 'remember', source: 'chrome-extension', tags: ['note'] });
|
|
425
|
+
}
|
|
426
|
+
$('note-input').value = '';
|
|
427
|
+
toggleNote(false);
|
|
428
|
+
toast('Note saved');
|
|
429
|
+
afterCapture('remember');
|
|
430
|
+
} catch (err) {
|
|
431
|
+
toast(describeError(err), true);
|
|
432
|
+
} finally {
|
|
433
|
+
setBusy(btn, false);
|
|
634
434
|
}
|
|
635
435
|
}
|
|
636
436
|
|
|
437
|
+
$('btn-save-note').addEventListener('click', saveNote);
|
|
438
|
+
$('note-input').addEventListener('keydown', (e) => {
|
|
439
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); saveNote(); }
|
|
440
|
+
if (e.key === 'Escape') toggleNote(false);
|
|
441
|
+
});
|
|
637
442
|
|
|
638
|
-
//
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (!toast) {
|
|
642
|
-
toast = document.createElement('div');
|
|
643
|
-
toast.className = 'toast';
|
|
644
|
-
document.body.appendChild(toast);
|
|
645
|
-
}
|
|
646
|
-
toast.textContent = message;
|
|
647
|
-
toast.classList.toggle('error', isError);
|
|
648
|
-
// Force reflow to restart animation
|
|
649
|
-
toast.classList.remove('show');
|
|
650
|
-
void toast.offsetWidth;
|
|
651
|
-
toast.classList.add('show');
|
|
652
|
-
setTimeout(() => toast.classList.remove('show'), 2500);
|
|
653
|
-
}
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
// Chat
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
654
446
|
|
|
655
|
-
|
|
656
|
-
function escapeHtml(str) {
|
|
657
|
-
if (!str) return '';
|
|
658
|
-
const div = document.createElement('div');
|
|
659
|
-
div.textContent = str;
|
|
660
|
-
return div.innerHTML;
|
|
661
|
-
}
|
|
447
|
+
const chatKey = () => `chat:${state.activeBrainId}`;
|
|
662
448
|
|
|
663
|
-
function
|
|
449
|
+
async function loadChat() {
|
|
664
450
|
try {
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
const diff = now - d;
|
|
668
|
-
if (diff < 60000) return 'just now';
|
|
669
|
-
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
|
670
|
-
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
|
671
|
-
return d.toLocaleDateString();
|
|
451
|
+
const data = await chrome.storage.session.get(chatKey());
|
|
452
|
+
state.chat = data[chatKey()] || [];
|
|
672
453
|
} catch {
|
|
673
|
-
|
|
454
|
+
state.chat = [];
|
|
674
455
|
}
|
|
456
|
+
renderChat();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function persistChat() {
|
|
460
|
+
chrome.storage.session.set({ [chatKey()]: state.chat.slice(-CHAT_HISTORY_LIMIT) }).catch(() => {});
|
|
675
461
|
}
|
|
676
462
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const collabCreateName = document.getElementById('collab-create-name');
|
|
697
|
-
const btnCollabSubmitCreate = document.getElementById('btn-collab-submit-create');
|
|
698
|
-
|
|
699
|
-
const collabAnnotationsList = document.getElementById('collab-annotations-list');
|
|
700
|
-
const collabNoteInput = document.getElementById('collab-note-input');
|
|
701
|
-
const btnCollabSaveNote = document.getElementById('btn-collab-save-note');
|
|
702
|
-
|
|
703
|
-
const collabPresenceCount = document.getElementById('collab-presence-count');
|
|
704
|
-
const collabChatMessages = document.getElementById('collab-chat-messages');
|
|
705
|
-
const collabChatInput = document.getElementById('collab-chat-input');
|
|
706
|
-
const btnCollabSendChat = document.getElementById('btn-collab-send-chat');
|
|
707
|
-
|
|
708
|
-
let collabToken = '';
|
|
709
|
-
let collabUsername = '';
|
|
710
|
-
let collabActiveUrl = '';
|
|
711
|
-
let collabSocket = null;
|
|
712
|
-
let collabGroups = [];
|
|
713
|
-
let collabSelectedGroup = null;
|
|
714
|
-
|
|
715
|
-
async function initCollab() {
|
|
716
|
-
const data = await chrome.storage.local.get(['collabToken', 'collabUsername']);
|
|
717
|
-
if (data.collabToken && data.collabUsername) {
|
|
718
|
-
collabToken = data.collabToken;
|
|
719
|
-
collabUsername = data.collabUsername;
|
|
720
|
-
showCollabMain();
|
|
463
|
+
function scrollChatToEnd() {
|
|
464
|
+
const s = $('chat-scroll');
|
|
465
|
+
requestAnimationFrame(() => { s.scrollTop = s.scrollHeight; });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function renderMessage(m) {
|
|
469
|
+
const wrap = el('div', `msg ${m.role === 'user' ? 'user' : m.error ? 'assistant error' : 'assistant'}`);
|
|
470
|
+
const bubble = el('div', 'bubble');
|
|
471
|
+
if (m.role === 'user') {
|
|
472
|
+
bubble.textContent = m.content;
|
|
473
|
+
wrap.append(bubble);
|
|
474
|
+
if (m.context && m.context.title) {
|
|
475
|
+
const ctx = el('span', 'msg-context');
|
|
476
|
+
ctx.append(svg('globe'), el('span', '', UI.clip(m.context.title, 60)));
|
|
477
|
+
wrap.append(ctx);
|
|
478
|
+
}
|
|
479
|
+
} else if (m.error) {
|
|
480
|
+
bubble.textContent = m.content;
|
|
481
|
+
wrap.append(bubble);
|
|
721
482
|
} else {
|
|
722
|
-
|
|
483
|
+
// renderMarkdown escapes all input before adding its whitelisted tags.
|
|
484
|
+
bubble.innerHTML = UI.renderMarkdown(m.content);
|
|
485
|
+
wrap.append(bubble);
|
|
486
|
+
if (m.model) wrap.append(el('span', 'msg-meta', m.model));
|
|
723
487
|
}
|
|
488
|
+
return wrap;
|
|
724
489
|
}
|
|
725
490
|
|
|
726
|
-
function
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
491
|
+
function renderChat() {
|
|
492
|
+
const list = $('chat-messages');
|
|
493
|
+
list.replaceChildren(...state.chat.map(renderMessage));
|
|
494
|
+
if (state.chatBusy) {
|
|
495
|
+
const typing = el('div', 'msg assistant');
|
|
496
|
+
const dots = el('div', 'typing');
|
|
497
|
+
dots.append(el('span'), el('span'), el('span'));
|
|
498
|
+
typing.append(dots);
|
|
499
|
+
list.append(typing);
|
|
500
|
+
}
|
|
501
|
+
$('chat-empty').hidden = state.chat.length > 0 || state.chatBusy;
|
|
502
|
+
$('btn-clear-chat').hidden = state.chat.length === 0;
|
|
503
|
+
updateSendState();
|
|
504
|
+
scrollChatToEnd();
|
|
730
505
|
}
|
|
731
506
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
await fetchCollabGroups();
|
|
737
|
-
updateCollabActiveUrl();
|
|
507
|
+
function updateSendState() {
|
|
508
|
+
$('btn-send-chat').disabled = state.chatBusy || !$('chat-input').value.trim();
|
|
509
|
+
const capturable = state.page && UI.isCapturableUrl(state.page.url);
|
|
510
|
+
$('chat-grounding').disabled = !capturable;
|
|
738
511
|
}
|
|
739
512
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
513
|
+
function getPageContext(tabId) {
|
|
514
|
+
return new Promise((resolve) => {
|
|
515
|
+
chrome.tabs.sendMessage(tabId, { type: 'GET_PAGE_CONTEXT' }, (res) => {
|
|
516
|
+
if (chrome.runtime.lastError) return resolve(null);
|
|
517
|
+
resolve(res || null);
|
|
745
518
|
});
|
|
746
|
-
|
|
747
|
-
collabGroups = await res.json();
|
|
748
|
-
renderCollabGroupsDropdown();
|
|
749
|
-
} else if (res.status === 401) {
|
|
750
|
-
logoutCollab();
|
|
751
|
-
}
|
|
752
|
-
} catch (err) {
|
|
753
|
-
console.error('Failed to fetch collab groups:', err);
|
|
754
|
-
}
|
|
519
|
+
});
|
|
755
520
|
}
|
|
756
521
|
|
|
757
|
-
function
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
522
|
+
async function sendChat(text, { grounded = $('chat-grounding').checked, extraContext = '' } = {}) {
|
|
523
|
+
const content = text.trim();
|
|
524
|
+
if (!content || state.chatBusy) return;
|
|
525
|
+
const tab = state.page;
|
|
526
|
+
const useGrounding = grounded && tab && UI.isCapturableUrl(tab.url);
|
|
527
|
+
|
|
528
|
+
state.chat.push({ role: 'user', content, context: useGrounding ? { title: tab.title, url: tab.url } : null });
|
|
529
|
+
state.chatBusy = true;
|
|
530
|
+
$('chat-input').value = '';
|
|
531
|
+
autosize();
|
|
532
|
+
renderChat();
|
|
533
|
+
persistChat();
|
|
768
534
|
|
|
769
|
-
async function updateCollabActiveUrl() {
|
|
770
535
|
try {
|
|
771
|
-
const
|
|
772
|
-
if (
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}
|
|
536
|
+
const messages = [];
|
|
537
|
+
if (useGrounding || extraContext) {
|
|
538
|
+
const ctx = useGrounding ? await getPageContext(tab.id) : null;
|
|
539
|
+
const lines = ['The user is asking from the Total Recall browser extension. Ground your answer in their memories first.'];
|
|
540
|
+
if (useGrounding) {
|
|
541
|
+
lines.push(`They are viewing: ${(ctx && ctx.title) || tab.title}\nURL: ${tab.url}`);
|
|
542
|
+
if (ctx && ctx.description) lines.push(`Description: ${ctx.description}`);
|
|
543
|
+
if (ctx && ctx.selection) lines.push(`Selected text:\n"""${UI.clip(ctx.selection, 2000)}"""`);
|
|
544
|
+
if (ctx && ctx.text) lines.push(`Page text (truncated):\n"""${UI.clip(ctx.text, PAGE_TEXT_FOR_CHAT)}"""`);
|
|
780
545
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
collabAnnotationsList.innerHTML = '<div class="memories-empty">No active web tab</div>';
|
|
784
|
-
closeCollabSocket();
|
|
546
|
+
if (extraContext) lines.push(extraContext);
|
|
547
|
+
messages.push({ role: 'system', content: lines.join('\n\n') });
|
|
785
548
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
async function fetchCollabAnnotations() {
|
|
792
|
-
if (!collabActiveUrl || !collabToken) return;
|
|
793
|
-
try {
|
|
794
|
-
const config = await self.BrainClient.getConfig();
|
|
795
|
-
const res = await fetch(`${config.brainUrl}/api/collab/annotations?url=${encodeURIComponent(collabActiveUrl)}`, {
|
|
796
|
-
headers: { 'Authorization': `Bearer ${collabToken}` }
|
|
797
|
-
});
|
|
798
|
-
if (res.ok) {
|
|
799
|
-
const annotations = await res.json();
|
|
800
|
-
renderCollabAnnotations(annotations);
|
|
549
|
+
for (const m of state.chat.slice(-CHAT_CONTEXT_TURNS)) {
|
|
550
|
+
if (!m.error) messages.push({ role: m.role, content: m.content });
|
|
801
551
|
}
|
|
552
|
+
const reply = await Brain.chat(messages);
|
|
553
|
+
state.chat.push({ role: 'assistant', content: reply.content || '(The brain returned an empty answer.)', model: reply.model });
|
|
802
554
|
} catch (err) {
|
|
803
|
-
|
|
555
|
+
state.chat.push({ role: 'assistant', error: true, content: describeError(err) });
|
|
556
|
+
if (isAuthOrOffline(err)) checkConnection();
|
|
557
|
+
} finally {
|
|
558
|
+
state.chatBusy = false;
|
|
559
|
+
renderChat();
|
|
560
|
+
persistChat();
|
|
804
561
|
}
|
|
805
562
|
}
|
|
806
563
|
|
|
807
|
-
function
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
}
|
|
812
|
-
collabAnnotationsList.innerHTML = list.map(a => `
|
|
813
|
-
<div class="collab-annotation-item">
|
|
814
|
-
<div class="author">👤 ${escapeHtml(a.author)}</div>
|
|
815
|
-
${a.excerpt ? `<div class="excerpt">${escapeHtml(a.excerpt)}</div>` : ''}
|
|
816
|
-
<div class="text">${escapeHtml(a.text)}</div>
|
|
817
|
-
<div class="time">${formatTime(a.created_at)}</div>
|
|
818
|
-
</div>
|
|
819
|
-
`).join('');
|
|
564
|
+
function autosize() {
|
|
565
|
+
const ta = $('chat-input');
|
|
566
|
+
ta.style.height = 'auto';
|
|
567
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 160)}px`;
|
|
820
568
|
}
|
|
821
569
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
570
|
+
$('chat-input').addEventListener('input', () => { autosize(); updateSendState(); });
|
|
571
|
+
$('chat-input').addEventListener('keydown', (e) => {
|
|
572
|
+
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
|
|
573
|
+
e.preventDefault();
|
|
574
|
+
sendChat($('chat-input').value);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
$('btn-send-chat').addEventListener('click', () => sendChat($('chat-input').value));
|
|
578
|
+
$('btn-clear-chat').addEventListener('click', () => {
|
|
579
|
+
state.chat = [];
|
|
580
|
+
persistChat();
|
|
581
|
+
renderChat();
|
|
582
|
+
$('chat-input').focus();
|
|
583
|
+
});
|
|
584
|
+
document.querySelectorAll('.suggestion').forEach((btn) => {
|
|
585
|
+
btn.addEventListener('click', () => sendChat(btn.dataset.prompt, { grounded: true }));
|
|
586
|
+
});
|
|
833
587
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
};
|
|
588
|
+
async function consumePendingAsk() {
|
|
589
|
+
let pending;
|
|
590
|
+
try {
|
|
591
|
+
({ pendingAsk: pending } = await chrome.storage.session.get('pendingAsk'));
|
|
592
|
+
} catch { return; }
|
|
593
|
+
if (!pending || Date.now() - pending.at > 60000) return;
|
|
594
|
+
await chrome.storage.session.remove('pendingAsk');
|
|
595
|
+
switchTab('chat');
|
|
596
|
+
const quote = UI.clip(pending.text, 600);
|
|
597
|
+
sendChat(`What do I know about this?\n\n“${quote}”`, {
|
|
598
|
+
grounded: false,
|
|
599
|
+
extraContext: pending.title ? `The selection comes from “${pending.title}” (${pending.url}).` : '',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
838
602
|
|
|
839
|
-
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Research
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
let researchPoll = null;
|
|
608
|
+
|
|
609
|
+
function researchRow(item, active) {
|
|
610
|
+
const row = el('div', 'research-row');
|
|
611
|
+
const status = el('div', 'r-status');
|
|
612
|
+
const meta = UI.researchStatus(item.status);
|
|
613
|
+
status.append(item.status === 'in_progress' ? el('div', 'spin') : el('div', `dot ${meta.tone}`));
|
|
614
|
+
status.title = meta.label;
|
|
615
|
+
const main = el('div', 'r-main');
|
|
616
|
+
main.append(el('div', 'r-topic', item.topic || item.title || 'Untitled'));
|
|
617
|
+
const when = UI.timeAgo(item.updated_at || item.created_at);
|
|
618
|
+
// Who wanted this: the user, or the AI's background (System 2) research for a project.
|
|
619
|
+
const why = item.origin === 'autonomous'
|
|
620
|
+
? (item.project ? `Auto · ${item.project}` : 'Auto')
|
|
621
|
+
: item.origin === 'user' ? 'You asked' : '';
|
|
622
|
+
main.append(el('div', 'r-meta', [meta.label, why, when].filter(Boolean).join(' · ')));
|
|
623
|
+
if (item.origin === 'autonomous' && item.rationale) main.title = item.rationale;
|
|
624
|
+
row.append(status, main);
|
|
625
|
+
if (active) {
|
|
626
|
+
const cancel = el('button', 'icon-btn small');
|
|
627
|
+
cancel.type = 'button';
|
|
628
|
+
cancel.setAttribute('aria-label', `Cancel research: ${item.topic}`);
|
|
629
|
+
cancel.title = 'Cancel';
|
|
630
|
+
cancel.append(svg('close'));
|
|
631
|
+
cancel.addEventListener('click', async () => {
|
|
632
|
+
cancel.disabled = true;
|
|
840
633
|
try {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
} else if (data.type === 'USER_JOINED') {
|
|
845
|
-
appendCollabChatBubble('System', `${data.username} joined this page`, new Date().toISOString(), true);
|
|
846
|
-
} else if (data.type === 'USER_LEFT') {
|
|
847
|
-
appendCollabChatBubble('System', `${data.username} left`, new Date().toISOString(), true);
|
|
848
|
-
} else if (data.type === 'ANNOTATION_ADDED') {
|
|
849
|
-
fetchCollabAnnotations();
|
|
850
|
-
}
|
|
634
|
+
await Brain.cancelResearch(item.id);
|
|
635
|
+
toast('Research cancelled');
|
|
636
|
+
loadResearch();
|
|
851
637
|
} catch (err) {
|
|
852
|
-
|
|
638
|
+
cancel.disabled = false;
|
|
639
|
+
toast(describeError(err), true);
|
|
853
640
|
}
|
|
854
|
-
};
|
|
641
|
+
});
|
|
642
|
+
row.append(cancel);
|
|
643
|
+
}
|
|
644
|
+
return row;
|
|
645
|
+
}
|
|
855
646
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
647
|
+
function renderResearch() {
|
|
648
|
+
const { items, counts } = state.research;
|
|
649
|
+
const running = items.filter((i) => i.status === 'in_progress');
|
|
650
|
+
const queued = items.filter((i) => i.status === 'pending');
|
|
651
|
+
const active = [...running, ...queued];
|
|
652
|
+
const finished = items.filter((i) => i.status === 'done' || i.status === 'failed').slice(0, 12);
|
|
653
|
+
const QUEUED_SHOWN = 8;
|
|
859
654
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
655
|
+
const countsEl = $('research-counts');
|
|
656
|
+
countsEl.replaceChildren();
|
|
657
|
+
const chips = [['in_progress', 'accent'], ['pending', ''], ['done', 'success'], ['failed', 'error']];
|
|
658
|
+
for (const [key, tone] of chips) {
|
|
659
|
+
if (!counts[key]) continue;
|
|
660
|
+
countsEl.append(el('span', `chip ${tone}`, `${counts[key]} ${UI.researchStatus(key).label.toLowerCase()}`));
|
|
661
|
+
}
|
|
865
662
|
|
|
866
|
-
|
|
867
|
-
if (
|
|
868
|
-
|
|
869
|
-
collabSocket.close();
|
|
870
|
-
} catch {}
|
|
871
|
-
collabSocket = null;
|
|
663
|
+
const activeRows = [...running, ...queued.slice(0, QUEUED_SHOWN)].map((i) => researchRow(i, true));
|
|
664
|
+
if (queued.length > QUEUED_SHOWN) {
|
|
665
|
+
activeRows.push(el('div', 'more-note', `+ ${queued.length - QUEUED_SHOWN} more queued`));
|
|
872
666
|
}
|
|
667
|
+
renderListOrEmpty($('research-active'), activeRows, 'Nothing running. Queue a topic above, or use Research on any page.');
|
|
668
|
+
renderListOrEmpty($('research-done'), finished.map((i) => researchRow(i, false)), 'Finished research lands in your brain as a report.');
|
|
669
|
+
|
|
670
|
+
// The list is capped; the server's counts are the real totals.
|
|
671
|
+
const activeTotal = (counts.pending || 0) + (counts.in_progress || 0) || active.length;
|
|
672
|
+
const badge = $('research-active-count');
|
|
673
|
+
badge.hidden = activeTotal === 0;
|
|
674
|
+
badge.textContent = activeTotal > 99 ? '99+' : String(activeTotal);
|
|
675
|
+
|
|
676
|
+
clearInterval(researchPoll);
|
|
677
|
+
if (active.length) researchPoll = setInterval(loadResearch, 15000);
|
|
873
678
|
}
|
|
874
679
|
|
|
875
|
-
function
|
|
876
|
-
|
|
877
|
-
if (isSystem) {
|
|
878
|
-
bubble.className = 'chat-bubble system';
|
|
879
|
-
bubble.textContent = text;
|
|
880
|
-
} else {
|
|
881
|
-
const isMe = sender.toLowerCase() === collabUsername.toLowerCase();
|
|
882
|
-
bubble.className = `chat-bubble ${isMe ? 'user' : 'assistant'}`;
|
|
883
|
-
bubble.innerHTML = `<span style="font-size: 10px; opacity: 0.8; font-weight: bold; display: block; margin-bottom: 2px;">${escapeHtml(sender)}</span>${escapeHtml(text)}`;
|
|
884
|
-
}
|
|
885
|
-
collabChatMessages.appendChild(bubble);
|
|
886
|
-
collabChatMessages.scrollTop = collabChatMessages.scrollHeight;
|
|
680
|
+
function renderListOrEmpty(container, rows, emptyText) {
|
|
681
|
+
container.replaceChildren(...(rows.length ? rows : [el('div', 'empty', emptyText)]));
|
|
887
682
|
}
|
|
888
683
|
|
|
889
|
-
async function
|
|
684
|
+
async function loadResearch() {
|
|
685
|
+
if (state.connection !== 'ok') return;
|
|
686
|
+
if (!state.research.items.length && state.tab === 'research') skeleton($('research-active'), 2);
|
|
890
687
|
try {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
method: 'POST',
|
|
894
|
-
headers: { 'Content-Type': 'application/json' },
|
|
895
|
-
body: JSON.stringify({ username, password })
|
|
896
|
-
});
|
|
897
|
-
const data = await res.json();
|
|
898
|
-
if (res.ok) {
|
|
899
|
-
collabToken = data.token;
|
|
900
|
-
collabUsername = data.username;
|
|
901
|
-
await chrome.storage.local.set({ collabToken: data.token, collabUsername: data.username });
|
|
902
|
-
showToast('Registered successfully!');
|
|
903
|
-
showCollabMain();
|
|
904
|
-
} else {
|
|
905
|
-
showToast('Registration failed: ' + (data.error || 'Unknown error'), true);
|
|
906
|
-
}
|
|
688
|
+
state.research = await Brain.listResearch();
|
|
689
|
+
renderResearch();
|
|
907
690
|
} catch (err) {
|
|
908
|
-
|
|
691
|
+
if (isAuthOrOffline(err)) return checkConnection();
|
|
692
|
+
$('research-active').replaceChildren(el('div', 'error-note', `Couldn’t load research: ${describeError(err)}`));
|
|
909
693
|
}
|
|
910
694
|
}
|
|
911
695
|
|
|
912
|
-
async
|
|
696
|
+
$('research-form').addEventListener('submit', async (e) => {
|
|
697
|
+
e.preventDefault();
|
|
698
|
+
const input = $('research-input');
|
|
699
|
+
const topic = input.value.trim();
|
|
700
|
+
if (!topic) return input.focus();
|
|
701
|
+
const btn = $('btn-queue-research');
|
|
702
|
+
setBusy(btn, true);
|
|
913
703
|
try {
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
body: JSON.stringify({ username, password })
|
|
919
|
-
});
|
|
920
|
-
const data = await res.json();
|
|
921
|
-
if (res.ok) {
|
|
922
|
-
collabToken = data.token;
|
|
923
|
-
collabUsername = data.username;
|
|
924
|
-
await chrome.storage.local.set({ collabToken: data.token, collabUsername: data.username });
|
|
925
|
-
showToast('Logged in successfully!');
|
|
926
|
-
showCollabMain();
|
|
927
|
-
} else {
|
|
928
|
-
showToast('Login failed: ' + (data.error || 'Unknown error'), true);
|
|
929
|
-
}
|
|
704
|
+
await Brain.queueResearch(topic);
|
|
705
|
+
input.value = '';
|
|
706
|
+
toast('Research queued');
|
|
707
|
+
loadResearch();
|
|
930
708
|
} catch (err) {
|
|
931
|
-
|
|
709
|
+
toast(describeError(err), true);
|
|
710
|
+
} finally {
|
|
711
|
+
setBusy(btn, false);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
$('btn-refresh-research').addEventListener('click', loadResearch);
|
|
715
|
+
|
|
716
|
+
// ---------------------------------------------------------------------------
|
|
717
|
+
// Settings sheet
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
|
|
720
|
+
function openSheet(open) {
|
|
721
|
+
const sheet = $('sheet');
|
|
722
|
+
sheet.classList.toggle('is-open', open);
|
|
723
|
+
sheet.toggleAttribute('inert', !open);
|
|
724
|
+
sheet.setAttribute('aria-hidden', String(!open));
|
|
725
|
+
$('scrim').hidden = !open;
|
|
726
|
+
if (open) {
|
|
727
|
+
loadBrains();
|
|
728
|
+
renderShortcut();
|
|
729
|
+
setTimeout(() => $('btn-close-settings').focus(), 60);
|
|
730
|
+
} else {
|
|
731
|
+
$('btn-open-settings').focus();
|
|
932
732
|
}
|
|
933
733
|
}
|
|
934
734
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
735
|
+
$('btn-open-settings').addEventListener('click', () => openSheet(true));
|
|
736
|
+
$('brain-chip').addEventListener('click', () => openSheet(true));
|
|
737
|
+
$('btn-close-settings').addEventListener('click', () => openSheet(false));
|
|
738
|
+
$('scrim').addEventListener('click', () => openSheet(false));
|
|
739
|
+
|
|
740
|
+
function brainLabel(id) {
|
|
741
|
+
if (!id || id === 'global') return 'Global';
|
|
742
|
+
const brain = state.brains.find((b) => b.id === id);
|
|
743
|
+
return (brain && brain.name) || id.replace(/^(project|tenant):/, '');
|
|
941
744
|
}
|
|
942
745
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
const p = collabPasswordInput.value.trim();
|
|
947
|
-
if (u && p) loginCollab(u, p);
|
|
948
|
-
});
|
|
746
|
+
function renderBrainChip() {
|
|
747
|
+
$('brain-chip-label').textContent = brainLabel(state.activeBrainId);
|
|
748
|
+
}
|
|
949
749
|
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
750
|
+
async function loadBrains() {
|
|
751
|
+
const select = $('brain-select');
|
|
752
|
+
try {
|
|
753
|
+
const data = await Brain.listBrains();
|
|
754
|
+
state.brains = (data && data.brains) || [];
|
|
755
|
+
} catch { /* keep whatever we had */ }
|
|
756
|
+
const options = state.brains.length ? state.brains : [{ id: 'global', name: 'Global Brain', node_count: null }];
|
|
757
|
+
select.replaceChildren(...options.map((b) => {
|
|
758
|
+
const opt = document.createElement('option');
|
|
759
|
+
opt.value = b.id;
|
|
760
|
+
const count = typeof b.node_count === 'number' ? ` · ${b.node_count.toLocaleString()} memories` : '';
|
|
761
|
+
opt.textContent = `${b.id === 'global' ? 'Global brain' : b.name}${count}`;
|
|
762
|
+
return opt;
|
|
763
|
+
}));
|
|
764
|
+
if (!options.some((b) => b.id === state.activeBrainId)) {
|
|
765
|
+
const opt = document.createElement('option');
|
|
766
|
+
opt.value = state.activeBrainId;
|
|
767
|
+
opt.textContent = brainLabel(state.activeBrainId);
|
|
768
|
+
select.append(opt);
|
|
769
|
+
}
|
|
770
|
+
select.value = state.activeBrainId;
|
|
771
|
+
renderBrainChip();
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
$('brain-select').addEventListener('change', async (e) => {
|
|
775
|
+
state.activeBrainId = e.target.value || 'global';
|
|
776
|
+
await chrome.storage.sync.set({ activeBrainId: state.activeBrainId });
|
|
777
|
+
renderBrainChip();
|
|
778
|
+
toast(`Using ${brainLabel(state.activeBrainId)} brain`);
|
|
779
|
+
loadChat();
|
|
780
|
+
loadRecent();
|
|
781
|
+
refreshPage();
|
|
782
|
+
if ($('search-input').value.trim()) onSearchInput();
|
|
954
783
|
});
|
|
955
784
|
|
|
956
|
-
|
|
785
|
+
function renderSettingsToggles() {
|
|
786
|
+
$('toggle-page-recall').checked = !!state.settings.pageRecall;
|
|
787
|
+
$('toggle-pill').checked = !!state.settings.showPill;
|
|
788
|
+
$('toggle-pill').disabled = !state.settings.pageRecall;
|
|
789
|
+
document.querySelector('.setting-row.sub').classList.toggle('is-disabled', !state.settings.pageRecall);
|
|
790
|
+
}
|
|
957
791
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
792
|
+
$('toggle-page-recall').addEventListener('change', async (e) => {
|
|
793
|
+
state.settings.pageRecall = e.target.checked;
|
|
794
|
+
await chrome.storage.sync.set({ pageRecall: state.settings.pageRecall });
|
|
795
|
+
renderSettingsToggles();
|
|
796
|
+
toast(state.settings.pageRecall ? 'Related memories on — takes effect on the next page load' : 'Related memories off');
|
|
797
|
+
});
|
|
798
|
+
$('toggle-pill').addEventListener('change', async (e) => {
|
|
799
|
+
state.settings.showPill = e.target.checked;
|
|
800
|
+
await chrome.storage.sync.set({ showPill: state.settings.showPill });
|
|
961
801
|
});
|
|
962
802
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
803
|
+
function currentHost() {
|
|
804
|
+
const tab = state.page;
|
|
805
|
+
return tab && UI.isCapturableUrl(tab.url) ? new URL(tab.url).hostname.replace(/^www\./, '') : '';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function renderBlockButton() {
|
|
809
|
+
const host = currentHost();
|
|
810
|
+
const btn = $('btn-block-site');
|
|
811
|
+
btn.disabled = !host;
|
|
812
|
+
const blocked = host && UI.isBlocked(state.page.url, state.settings.blocklist);
|
|
813
|
+
btn.classList.toggle('danger', !blocked);
|
|
814
|
+
btn.replaceChildren(document.createTextNode(blocked ? 'Allow ' : 'Never check '), el('span', '', host || 'this site'));
|
|
815
|
+
if (blocked) btn.append(document.createTextNode(' again'));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
$('btn-block-site').addEventListener('click', async () => {
|
|
819
|
+
const host = currentHost();
|
|
820
|
+
if (!host) return;
|
|
821
|
+
const list = Array.isArray(state.settings.blocklist) ? state.settings.blocklist.slice() : [];
|
|
822
|
+
const blocked = UI.isBlocked(state.page.url, list);
|
|
823
|
+
const next = blocked
|
|
824
|
+
? list.filter((entry) => !UI.isBlocked(state.page.url, [entry]))
|
|
825
|
+
: [...list, host];
|
|
826
|
+
state.settings.blocklist = next;
|
|
827
|
+
await chrome.storage.sync.set({ blocklist: next });
|
|
828
|
+
renderBlockButton();
|
|
829
|
+
toast(blocked ? `${host} will be checked again` : `${host} won’t be checked for related memories`);
|
|
966
830
|
});
|
|
967
831
|
|
|
968
|
-
|
|
969
|
-
const
|
|
970
|
-
|
|
832
|
+
$('btn-recompile').addEventListener('click', async () => {
|
|
833
|
+
const btn = $('btn-recompile');
|
|
834
|
+
setBusy(btn, true);
|
|
971
835
|
try {
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
method: 'POST',
|
|
975
|
-
headers: {
|
|
976
|
-
'Content-Type': 'application/json',
|
|
977
|
-
'Authorization': `Bearer ${collabToken}`
|
|
978
|
-
},
|
|
979
|
-
body: JSON.stringify({ code })
|
|
980
|
-
});
|
|
981
|
-
const data = await res.json();
|
|
982
|
-
if (res.ok) {
|
|
983
|
-
showToast('Joined group!');
|
|
984
|
-
collabJoinCode.value = '';
|
|
985
|
-
collabJoinGroupRow.classList.add('hidden');
|
|
986
|
-
await fetchCollabGroups();
|
|
987
|
-
} else {
|
|
988
|
-
showToast('Join error: ' + data.error, true);
|
|
989
|
-
}
|
|
836
|
+
await Brain.compile();
|
|
837
|
+
toast('Search index rebuilt');
|
|
990
838
|
} catch (err) {
|
|
991
|
-
|
|
839
|
+
toast(describeError(err), true);
|
|
840
|
+
} finally {
|
|
841
|
+
setBusy(btn, false);
|
|
992
842
|
}
|
|
993
843
|
});
|
|
994
844
|
|
|
995
|
-
|
|
996
|
-
const name = collabCreateName.value.trim();
|
|
997
|
-
if (!name) return;
|
|
845
|
+
async function renderShortcut() {
|
|
998
846
|
try {
|
|
999
|
-
const
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
847
|
+
const commands = await chrome.commands.getAll();
|
|
848
|
+
const open = commands.find((c) => c.name === '_execute_action');
|
|
849
|
+
const remember = commands.find((c) => c.name === 'remember-page');
|
|
850
|
+
const parts = [open && open.shortcut, remember && remember.shortcut].filter(Boolean);
|
|
851
|
+
$('shortcut-summary').textContent = parts.length ? parts.join(' · ') : 'Not set';
|
|
852
|
+
} catch { /* ignore */ }
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function renderSheetFooter() {
|
|
856
|
+
const foot = $('sheet-foot');
|
|
857
|
+
const ext = `Extension v${chrome.runtime.getManifest().version}`;
|
|
858
|
+
const brain = state.health && state.health.version ? `Brain v${state.health.version}` : '';
|
|
859
|
+
foot.replaceChildren(el('span', '', ext), el('span', '', brain));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
$('link-options').addEventListener('click', () => chrome.runtime.openOptionsPage());
|
|
863
|
+
$('link-shortcuts').addEventListener('click', () => chrome.tabs.create({ url: 'chrome://extensions/shortcuts' }));
|
|
864
|
+
$('link-dashboard').addEventListener('click', async () => chrome.tabs.create({ url: await Brain.dashboardUrl('/') }));
|
|
865
|
+
|
|
866
|
+
// ---------------------------------------------------------------------------
|
|
867
|
+
// Gate buttons
|
|
868
|
+
// ---------------------------------------------------------------------------
|
|
869
|
+
|
|
870
|
+
$('gate-settings').addEventListener('click', () => chrome.runtime.openOptionsPage());
|
|
871
|
+
$('gate-retry').addEventListener('click', async () => {
|
|
872
|
+
const btn = $('gate-retry');
|
|
873
|
+
setBusy(btn, true);
|
|
874
|
+
const recovered = await checkConnection();
|
|
875
|
+
setBusy(btn, false);
|
|
876
|
+
if (recovered) loadAll();
|
|
1020
877
|
});
|
|
1021
878
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
fetchCollabAnnotations();
|
|
1026
|
-
});
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
// Global events
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
1027
882
|
|
|
1028
|
-
|
|
1029
|
-
const
|
|
1030
|
-
if (
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
method: 'POST',
|
|
1035
|
-
headers: {
|
|
1036
|
-
'Content-Type': 'application/json',
|
|
1037
|
-
'Authorization': `Bearer ${collabToken}`
|
|
1038
|
-
},
|
|
1039
|
-
body: JSON.stringify({
|
|
1040
|
-
url: collabActiveUrl,
|
|
1041
|
-
groupCode: collabSelectedGroup.code,
|
|
1042
|
-
text
|
|
1043
|
-
})
|
|
1044
|
-
});
|
|
1045
|
-
if (res.ok) {
|
|
1046
|
-
showToast('Note pinned!');
|
|
1047
|
-
collabNoteInput.value = '';
|
|
1048
|
-
fetchCollabAnnotations();
|
|
1049
|
-
} else {
|
|
1050
|
-
const errData = await res.json();
|
|
1051
|
-
showToast('Pin note error: ' + errData.error, true);
|
|
1052
|
-
}
|
|
1053
|
-
} catch (err) {
|
|
1054
|
-
showToast('Failed to pin note: ' + err.message, true);
|
|
883
|
+
document.addEventListener('keydown', (e) => {
|
|
884
|
+
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement && document.activeElement.tagName);
|
|
885
|
+
if (e.key === 'Escape' && $('sheet').classList.contains('is-open')) return openSheet(false);
|
|
886
|
+
if (e.key === '/' && !typing && state.tab === 'recall') {
|
|
887
|
+
e.preventDefault();
|
|
888
|
+
$('search-input').focus();
|
|
1055
889
|
}
|
|
1056
890
|
});
|
|
1057
891
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
});
|
|
892
|
+
let pageTimer = null;
|
|
893
|
+
function schedulePageRefresh() {
|
|
894
|
+
clearTimeout(pageTimer);
|
|
895
|
+
pageTimer = setTimeout(() => { refreshPage(); updateSendState(); }, 150);
|
|
896
|
+
}
|
|
1065
897
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
898
|
+
chrome.tabs.onActivated.addListener(schedulePageRefresh);
|
|
899
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
|
900
|
+
if (!state.page || tabId !== state.page.id) return;
|
|
901
|
+
if (changeInfo.url || changeInfo.title || changeInfo.status === 'complete' || changeInfo.favIconUrl) schedulePageRefresh();
|
|
1071
902
|
});
|
|
903
|
+
chrome.windows?.onFocusChanged?.addListener(schedulePageRefresh);
|
|
1072
904
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
updateCollabActiveUrl();
|
|
905
|
+
chrome.runtime.onMessage.addListener((msg) => {
|
|
906
|
+
if (msg && msg.type === 'CAPTURED') afterCapture(msg.action);
|
|
1076
907
|
});
|
|
1077
908
|
|
|
1078
|
-
chrome.
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
909
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
910
|
+
if (area === 'session' && changes.pendingAsk && changes.pendingAsk.newValue) consumePendingAsk();
|
|
911
|
+
if (area === 'sync') {
|
|
912
|
+
if (changes.blocklist) { state.settings.blocklist = changes.blocklist.newValue || []; renderBlockButton(); }
|
|
913
|
+
if (changes.pageRecall) { state.settings.pageRecall = !!changes.pageRecall.newValue; renderSettingsToggles(); }
|
|
914
|
+
}
|
|
915
|
+
if (area === 'local' && (changes.brainUrl || changes.pat)) {
|
|
916
|
+
checkConnection().then((recovered) => { if (recovered) loadAll(); });
|
|
1081
917
|
}
|
|
1082
918
|
});
|
|
1083
919
|
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
920
|
+
// ---------------------------------------------------------------------------
|
|
921
|
+
// Init
|
|
922
|
+
// ---------------------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
function loadAll() {
|
|
925
|
+
refreshPage();
|
|
926
|
+
loadRecent();
|
|
927
|
+
loadResearch();
|
|
928
|
+
loadBrains();
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
async function init() {
|
|
932
|
+
const [sync, session] = await Promise.all([
|
|
933
|
+
chrome.storage.sync.get({ activeBrainId: 'global', pageRecall: false, showPill: true, blocklist: [] }),
|
|
934
|
+
chrome.storage.session.get('lastTab').catch(() => ({})),
|
|
935
|
+
]);
|
|
936
|
+
state.activeBrainId = sync.activeBrainId || 'global';
|
|
937
|
+
state.settings = { pageRecall: sync.pageRecall, showPill: sync.showPill, blocklist: sync.blocklist || [] };
|
|
938
|
+
renderBrainChip();
|
|
939
|
+
renderSettingsToggles();
|
|
940
|
+
renderSheetFooter();
|
|
941
|
+
switchTab(TABS.includes(session.lastTab) ? session.lastTab : 'recall');
|
|
942
|
+
await loadChat();
|
|
943
|
+
await refreshPage();
|
|
944
|
+
|
|
945
|
+
await checkConnection();
|
|
946
|
+
if (state.connection === 'ok') loadAll();
|
|
947
|
+
consumePendingAsk();
|
|
948
|
+
setInterval(() => checkConnection({ quiet: true }).then((recovered) => { if (recovered) loadAll(); }), 30000);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
init();
|
|
1104
952
|
})();
|
|
1105
|
-
|
|
1106
|
-
|