total-recall-brain 3.28.2 → 3.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/bin/total-recall.mjs +24 -1
  2. package/docs/ARCHITECTURE.md +26 -18
  3. package/extension/background.js +259 -84
  4. package/extension/content-script.css +7 -13
  5. package/extension/content-script.js +252 -477
  6. package/extension/icons/icon-128.png +0 -0
  7. package/extension/icons/icon-16.png +0 -0
  8. package/extension/icons/icon-32.png +0 -0
  9. package/extension/icons/icon-48.png +0 -0
  10. package/extension/icons/mark.svg +6 -0
  11. package/extension/lib/brain-client.js +160 -73
  12. package/extension/lib/tokens.css +187 -0
  13. package/extension/lib/ui-helpers.js +229 -0
  14. package/extension/lib/ui-helpers.spec.mjs +94 -0
  15. package/extension/manifest.json +20 -5
  16. package/extension/options/options.css +64 -218
  17. package/extension/options/options.html +91 -43
  18. package/extension/options/options.js +208 -85
  19. package/extension/sidepanel/sidepanel.css +268 -970
  20. package/extension/sidepanel/sidepanel.html +200 -166
  21. package/extension/sidepanel/sidepanel.js +788 -942
  22. package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
  23. package/frontend/dist/index.html +1 -1
  24. package/frontend/src/api/plugins.ts +124 -92
  25. package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
  26. package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
  27. package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
  28. package/frontend/src/pages/PluginsPage.tsx +454 -1218
  29. package/package.json +2 -1
  30. package/plugins/code-quality/README.md +25 -0
  31. package/plugins/code-quality/cli.mjs +25 -0
  32. package/plugins/code-quality/plugin.json +17 -0
  33. package/plugins/git-sentinel/README.md +27 -0
  34. package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
  35. package/plugins/git-sentinel/plugin.json +18 -0
  36. package/plugins/system-monitor/README.md +26 -0
  37. package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
  38. package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
  39. package/plugins/system-monitor/plugin.json +29 -0
  40. package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
  41. package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
  42. package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
  43. package/src/cli/agent.mjs +1 -1
  44. package/src/cli/app/index.mjs +493 -0
  45. package/src/cli/brain.mjs +33 -3
  46. package/src/cli/command.mjs +80 -41
  47. package/src/cli/harness.mjs +1 -1
  48. package/src/cli/init.mjs +3 -1
  49. package/src/cli/mesh.mjs +118 -12
  50. package/src/cli/plugin/create.mjs +144 -25
  51. package/src/cli/plugin/index.mjs +83 -53
  52. package/src/cli/plugin/install.mjs +10 -127
  53. package/src/cli/plugin/list.mjs +44 -44
  54. package/src/cli/plugin/remove.mjs +23 -30
  55. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  56. package/src/cli/plugin/search.mjs +108 -23
  57. package/src/cli/rebuild.mjs +37 -1
  58. package/src/cli/recall.mjs +17 -2
  59. package/src/cli/research.mjs +4 -6
  60. package/src/cli/share.mjs +2 -0
  61. package/src/core/app-deploy/apply.mjs +386 -0
  62. package/src/core/app-deploy/design-tokens.mjs +217 -0
  63. package/src/core/app-deploy/plan.mjs +249 -0
  64. package/src/core/app-deploy/resolve.mjs +295 -0
  65. package/src/core/app-deploy/source.mjs +337 -0
  66. package/src/core/app-deploy/standalone.mjs +248 -0
  67. package/src/core/app-deploy/upgrade.mjs +256 -0
  68. package/src/core/app-deploy/verify.mjs +213 -0
  69. package/src/core/bound-hosts.mjs +1 -1
  70. package/src/core/brain-registry.mjs +146 -0
  71. package/src/core/brain-state.json +1 -0
  72. package/src/core/daemon-loop.mjs +30 -26
  73. package/src/core/dream.mjs +5 -17
  74. package/src/core/fact-seeker.mjs +53 -368
  75. package/src/core/lan-discovery.mjs +3 -3
  76. package/src/core/mesh-access-sync.mjs +134 -0
  77. package/src/core/mesh-access.mjs +18 -3
  78. package/src/core/mesh-discover.mjs +173 -0
  79. package/src/core/mesh.mjs +131 -32
  80. package/src/core/optimizer.mjs +4 -41
  81. package/src/core/plugin-bundle.mjs +167 -0
  82. package/src/core/plugin-context.mjs +8 -13
  83. package/src/core/plugin-loader.mjs +272 -68
  84. package/src/core/plugin-peers.mjs +163 -0
  85. package/src/core/plugin-public.mjs +85 -0
  86. package/src/core/plugin-runner-child.mjs +24 -0
  87. package/src/core/plugin-runner.mjs +102 -0
  88. package/src/core/plugin-store.mjs +384 -0
  89. package/src/core/plugin-tasks.mjs +212 -0
  90. package/src/core/post-mortem.mjs +40 -9
  91. package/src/core/research-gate.mjs +182 -0
  92. package/src/core/research-queue.mjs +47 -7
  93. package/src/core/research-surface.mjs +211 -0
  94. package/src/core/research.mjs +25 -32
  95. package/src/core/scheduler.mjs +67 -153
  96. package/src/core/schema.mjs +129 -0
  97. package/src/core/search.mjs +12 -1
  98. package/src/core/secret-integration-research.mjs +12 -8
  99. package/src/core/secrets-store.mjs +32 -2
  100. package/src/core/session-watcher.mjs +6 -0
  101. package/src/core/surface.mjs +64 -3
  102. package/src/core/task-executors.mjs +8 -45
  103. package/src/core/vault-cache.mjs +70 -4
  104. package/src/server/api.mjs +21 -0
  105. package/src/server/rest.mjs +2 -0
  106. package/src/server/route-manifest.json +21 -5
  107. package/src/server/routes/collab.mjs +38 -11
  108. package/src/server/routes/extension.mjs +3 -2
  109. package/src/server/routes/memory.mjs +24 -2
  110. package/src/server/routes/mesh.mjs +7 -7
  111. package/src/server/routes/plugins-mesh.mjs +49 -0
  112. package/src/server/routes/plugins.mjs +91 -426
  113. package/src/server/routes/research.mjs +5 -2
  114. package/src/server/routes/share.mjs +2 -0
  115. package/src/server/tools.mjs +29 -2
  116. package/extension/icons/README.md +0 -27
  117. package/extension/lib/preconfigured.js +0 -5
  118. package/extension/popup/popup.css +0 -227
  119. package/extension/popup/popup.html +0 -50
  120. package/extension/popup/popup.js +0 -175
  121. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  122. package/scaffold/.agent/plugins/.gitkeep +0 -0
  123. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  124. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  125. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  126. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  127. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  128. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  129. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  130. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  131. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  132. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
@@ -2,72 +2,120 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <title>Total Recall — Settings</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Total Recall — Extension settings</title>
7
+ <link rel="stylesheet" href="../lib/tokens.css">
6
8
  <link rel="stylesheet" href="options.css">
7
9
  </head>
8
10
  <body>
9
- <div class="options-page">
10
- <header class="options-header">
11
- <h1>🧠 Total Recall — Settings</h1>
11
+ <svg width="0" height="0" style="position:absolute" aria-hidden="true">
12
+ <symbol id="i-plug" viewBox="0 0 24 24"><path d="M9 3v5M15 3v5"/><path d="M6 8h12v3a6 6 0 0 1-12 0z"/><path d="M12 17v4"/></symbol>
13
+ <symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 4.5 6v5.5c0 4.5 3.2 8.2 7.5 9.5 4.3-1.3 7.5-5 7.5-9.5V6z"/><path d="m9 12 2 2 4-4"/></symbol>
14
+ <symbol id="i-keyboard" viewBox="0 0 24 24"><rect x="2.5" y="6" width="19" height="12" rx="2"/><path d="M6.5 10h.01M10 10h.01M14 10h.01M17.5 10h.01M7 14h10"/></symbol>
15
+ <symbol id="i-eye" viewBox="0 0 24 24"><path d="M2.5 12S6 5 12 5s9.5 7 9.5 7-3.5 7-9.5 7-9.5-7-9.5-7z"/><circle cx="12" cy="12" r="3"/></symbol>
16
+ <symbol id="i-external" viewBox="0 0 24 24"><path d="M14 4h6v6"/><path d="M20 4 11 13"/><path d="M19 14v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4"/></symbol>
17
+ </svg>
18
+
19
+ <div class="page">
20
+ <header class="hero">
21
+ <img src="../icons/icon-128.png" alt="" width="44" height="44" class="hero-mark">
22
+ <div>
23
+ <h1>Total Recall for Chrome</h1>
24
+ <p>Capture what you read, recall what you know, and chat with your brain beside any tab.</p>
25
+ </div>
12
26
  </header>
13
27
 
14
- <section class="section">
15
- <h2>Connection</h2>
28
+ <section class="card" aria-labelledby="h-connection">
29
+ <div class="card-head">
30
+ <span class="card-icon"><svg class="icon"><use href="#i-plug"/></svg></span>
31
+ <div>
32
+ <h2 id="h-connection">Connection</h2>
33
+ <p>Where your brain runs and the token the extension uses to reach it.</p>
34
+ </div>
35
+ <span class="chip" id="connection-chip">Not tested</span>
36
+ </div>
16
37
 
17
- <div class="field-group">
18
- <label for="brain-url">Brain URL</label>
19
- <input type="text" id="brain-url" placeholder="http://127.0.0.1:3000" />
38
+ <div class="field">
39
+ <label for="brain-url">Brain address</label>
40
+ <input class="input" id="brain-url" type="url" placeholder="http://127.0.0.1:3000" spellcheck="false" autocomplete="off">
41
+ <p class="help">Usually <code>http://127.0.0.1:3000</code>. For a brain on another machine, Chrome asks once for permission to reach it.</p>
20
42
  </div>
21
43
 
22
- <div class="field-group">
23
- <label for="pat-token">PAT Token</label>
24
- <input type="password" id="pat-token" placeholder="Enter your personal access token" />
44
+ <div class="field">
45
+ <label for="pat-token">Access token</label>
46
+ <div class="input-row">
47
+ <input class="input mono" id="pat-token" type="password" placeholder="tr_…" spellcheck="false" autocomplete="off">
48
+ <button class="icon-btn" id="btn-reveal" type="button" aria-label="Show token" aria-pressed="false" title="Show token"><svg class="icon"><use href="#i-eye"/></svg></button>
49
+ </div>
50
+ <p class="help">Create one in the dashboard under <a href="#" id="link-integrations">Integrations<svg class="icon sm"><use href="#i-external"/></svg></a>. It stays on this device and is never synced.</p>
25
51
  </div>
26
52
 
27
- <div class="connection-row">
28
- <button id="btn-test" class="btn secondary">Test Connection</button>
29
- <div id="connection-status" class="connection-status"></div>
53
+ <div class="result" id="connection-result" hidden></div>
54
+
55
+ <div class="actions">
56
+ <button class="btn" id="btn-test" type="button">Test connection</button>
57
+ <button class="btn primary" id="btn-save-connection" type="button">Save connection</button>
30
58
  </div>
31
59
  </section>
32
60
 
33
- <section class="section">
34
- <h2>Privacy</h2>
61
+ <section class="card" aria-labelledby="h-privacy">
62
+ <div class="card-head">
63
+ <span class="card-icon"><svg class="icon"><use href="#i-shield"/></svg></span>
64
+ <div>
65
+ <h2 id="h-privacy">Page recall &amp; privacy</h2>
66
+ <p>Nothing leaves this browser unless you capture it — or you turn on related memories below.</p>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="setting">
71
+ <div>
72
+ <div class="setting-title">Related memories on pages</div>
73
+ <p class="help">When a page loads, send its title to your brain and badge the toolbar icon if you already know something related. Off by default.</p>
74
+ </div>
75
+ <label class="switch"><input type="checkbox" id="opt-page-recall"><span class="track"></span></label>
76
+ </div>
77
+ <div class="setting sub" id="pill-setting">
78
+ <div>
79
+ <div class="setting-title">Floating card on the page</div>
80
+ <p class="help">Also show a small “Related” pill in the corner of the page.</p>
81
+ </div>
82
+ <label class="switch"><input type="checkbox" id="opt-show-pill"><span class="track"></span></label>
83
+ </div>
35
84
 
36
- <div class="field-group">
37
- <label for="blocklist">Domain Blocklist <span class="hint">(one per line)</span></label>
38
- <textarea id="blocklist" rows="5" placeholder="example.com&#10;private.internal.io"></textarea>
85
+ <div class="field">
86
+ <label for="blocklist">Never check these sites <span class="muted">— one domain per line, subdomains included</span></label>
87
+ <textarea class="textarea mono" id="blocklist" rows="5" placeholder="mail.google.com&#10;bank.example.com"></textarea>
39
88
  </div>
40
89
 
41
- <div class="field-group">
42
- <label>Capture Granularity</label>
43
- <div class="radio-group">
44
- <label class="radio-label">
45
- <input type="radio" name="granularity" value="off" />
46
- <span class="radio-text">Off</span>
47
- <span class="radio-desc">No passive tracking</span>
48
- </label>
49
- <label class="radio-label">
50
- <input type="radio" name="granularity" value="minimal" />
51
- <span class="radio-text">Minimal</span>
52
- <span class="radio-desc">URL and title only</span>
53
- </label>
54
- <label class="radio-label">
55
- <input type="radio" name="granularity" value="full" />
56
- <span class="radio-text">Full</span>
57
- <span class="radio-desc">URL, title, and page metadata</span>
58
- </label>
90
+ <div class="actions">
91
+ <span class="saved" id="privacy-saved" hidden>Saved</span>
92
+ <button class="btn primary" id="btn-save-privacy" type="button">Save privacy settings</button>
93
+ </div>
94
+ </section>
95
+
96
+ <section class="card" aria-labelledby="h-shortcuts">
97
+ <div class="card-head">
98
+ <span class="card-icon"><svg class="icon"><use href="#i-keyboard"/></svg></span>
99
+ <div>
100
+ <h2 id="h-shortcuts">Shortcuts</h2>
101
+ <p>Capture without reaching for the mouse.</p>
59
102
  </div>
60
103
  </div>
104
+ <dl class="shortcuts" id="shortcut-list"></dl>
105
+ <div class="tips">
106
+ <div><span class="kbd">tr</span> then <span class="kbd">Space</span> in the address bar searches your brain.</div>
107
+ <div>Right-click a selection to remember it, research it, or ask your brain about it.</div>
108
+ </div>
109
+ <div class="actions">
110
+ <button class="btn" id="btn-shortcuts" type="button">Change shortcuts<svg class="icon sm"><use href="#i-external"/></svg></button>
111
+ </div>
61
112
  </section>
62
113
 
63
- <div class="actions">
64
- <button id="btn-save" class="btn primary">Save Settings</button>
65
- <span id="save-status" class="save-status"></span>
66
- </div>
114
+ <footer class="foot" id="foot"></footer>
67
115
  </div>
68
116
 
69
- <script src="../lib/preconfigured.js"></script>
70
117
  <script src="../lib/brain-client.js"></script>
118
+ <script src="../lib/ui-helpers.js"></script>
71
119
  <script src="options.js"></script>
72
120
  </body>
73
121
  </html>
@@ -1,111 +1,234 @@
1
- // Total Recall — Options Page Logic
1
+ // Total Recall — options page
2
2
 
3
3
  (function () {
4
4
  'use strict';
5
5
 
6
- const brainUrlInput = document.getElementById('brain-url');
7
- const patInput = document.getElementById('pat-token');
8
- const btnTest = document.getElementById('btn-test');
9
- const connectionStatus = document.getElementById('connection-status');
10
- const blocklistTextarea = document.getElementById('blocklist');
11
- const btnSave = document.getElementById('btn-save');
12
- const saveStatus = document.getElementById('save-status');
13
-
14
- // ---- Load saved settings ----
15
- async function loadSettings() {
16
- const [localSettings, syncSettings] = await Promise.all([
17
- chrome.storage.local.get(['brainUrl', 'pat']),
18
- chrome.storage.sync.get([
19
- 'blocklist',
20
- 'captureGranularity',
21
- 'passiveTracking'
22
- ])
23
- ]);
24
- const settings = { ...syncSettings, ...localSettings };
6
+ const Brain = self.BrainClient;
7
+ const $ = (id) => document.getElementById(id);
8
+
9
+ function el(tag, className, text) {
10
+ const node = document.createElement(tag);
11
+ if (className) node.className = className;
12
+ if (text !== undefined) node.textContent = text;
13
+ return node;
14
+ }
15
+
16
+ function setBusy(button, busy) {
17
+ button.disabled = busy;
18
+ button.classList.toggle('is-busy', busy);
19
+ }
20
+
21
+ function isLocal(url) {
22
+ try {
23
+ const host = new URL(url).hostname;
24
+ return host === '127.0.0.1' || host === 'localhost';
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function originPattern(url) {
31
+ const u = new URL(url);
32
+ return `${u.protocol}//${u.hostname}/*`;
33
+ }
34
+
35
+ /** Remote brains need a runtime host grant; must be requested from a click. */
36
+ async function ensureHostPermission(url) {
37
+ if (isLocal(url)) return true;
38
+ const origins = [originPattern(url)];
39
+ if (await chrome.permissions.contains({ origins })) return true;
40
+ return chrome.permissions.request({ origins });
41
+ }
42
+
43
+ function readConnectionForm() {
44
+ const raw = $('brain-url').value.trim() || Brain.DEFAULT_BRAIN_URL;
45
+ let brainUrl;
46
+ try {
47
+ const u = new URL(raw);
48
+ if (!/^https?:$/.test(u.protocol)) throw new Error();
49
+ brainUrl = Brain.normalizeUrl(u.origin + u.pathname);
50
+ } catch {
51
+ throw new Error('Enter a full address, like http://127.0.0.1:3000');
52
+ }
53
+ return { brainUrl, pat: $('pat-token').value.trim() };
54
+ }
55
+
56
+ function setChip(text, tone) {
57
+ const chip = $('connection-chip');
58
+ chip.textContent = text;
59
+ chip.className = `chip ${tone || ''}`;
60
+ }
25
61
 
26
- const pre = self.PreConfigured || {};
27
- brainUrlInput.value = settings.brainUrl || pre.brainUrl || 'http://127.0.0.1:3000';
28
- patInput.value = settings.pat || pre.pat || '';
62
+ function showResult(ok, rows, message) {
63
+ const box = $('connection-result');
64
+ box.className = `result ${ok ? 'ok' : 'fail'}`;
65
+ if (ok) {
66
+ box.replaceChildren(...rows.flatMap(([k, v]) => [el('dt', '', k), el('dd', '', v)]));
67
+ } else {
68
+ box.replaceChildren(el('div', '', message));
69
+ }
70
+ box.hidden = false;
71
+ }
29
72
 
30
- // Blocklist: stored as array, displayed one per line
31
- if (Array.isArray(settings.blocklist)) {
32
- blocklistTextarea.value = settings.blocklist.join('\n');
73
+ async function testConnection() {
74
+ const { brainUrl, pat } = readConnectionForm();
75
+ if (!(await ensureHostPermission(brainUrl))) {
76
+ throw new Error(`Chrome permission to reach ${new URL(brainUrl).host} was declined.`);
77
+ }
78
+ const config = { brainUrl, pat, activeBrainId: 'global' };
79
+ const health = await Brain.request('/health', { config, brainScoped: false });
80
+ if (!pat) {
81
+ const err = new Error('The brain is reachable. Add an access token to finish connecting.');
82
+ err.code = 'unconfigured';
83
+ err.health = health;
84
+ throw err;
33
85
  }
86
+ const list = await Brain.request('/api/memory?limit=1', { config });
87
+ let brains = null;
88
+ try { brains = await Brain.request('/api/brains', { config, brainScoped: false }); } catch { /* optional scope */ }
89
+ return { health, total: list && list.total, brains: brains && brains.brains };
90
+ }
34
91
 
35
- // Granularity radio
36
- let granularity = settings.captureGranularity || 'minimal';
37
- if (settings.passiveTracking === false) {
38
- granularity = 'off';
92
+ function describeTest(result) {
93
+ const rows = [
94
+ ['Brain', `v${result.health.version || '?'} · daemon ${result.health.daemon || 'unknown'}`],
95
+ ['Memories', typeof result.total === 'number' ? result.total.toLocaleString() : '—'],
96
+ ];
97
+ if (Array.isArray(result.brains) && result.brains.length > 1) {
98
+ rows.push(['Project brains', String(result.brains.length - 1)]);
39
99
  }
40
- const radio = document.querySelector(`input[name="granularity"][value="${granularity}"]`);
41
- if (radio) radio.checked = true;
100
+ return rows;
42
101
  }
43
102
 
44
- // ---- Save settings ----
45
- btnSave.addEventListener('click', async () => {
103
+ $('btn-test').addEventListener('click', async () => {
104
+ const btn = $('btn-test');
105
+ setBusy(btn, true);
106
+ setChip('Testing…');
46
107
  try {
47
- // Parse blocklist textarea into array
48
- const blocklistRaw = blocklistTextarea.value.trim();
49
- const blocklist = blocklistRaw
50
- ? blocklistRaw.split('\n').map((d) => d.trim()).filter(Boolean)
51
- : [];
52
-
53
- // Get selected granularity
54
- const granularityRadio = document.querySelector('input[name="granularity"]:checked');
55
- const captureGranularity = granularityRadio ? granularityRadio.value : 'minimal';
56
- const passiveTracking = captureGranularity !== 'off';
57
-
58
- await chrome.storage.local.set({
59
- brainUrl: brainUrlInput.value.trim() || 'http://127.0.0.1:3000',
60
- pat: patInput.value.trim(),
61
- });
62
- await chrome.storage.sync.remove('pat');
63
- await chrome.storage.sync.set({
64
- blocklist,
65
- captureGranularity,
66
- passiveTracking
67
- });
68
-
69
- showSaveStatus('Settings saved!', false);
108
+ const result = await testConnection();
109
+ setChip('Connected', 'success');
110
+ showResult(true, describeTest(result));
70
111
  } catch (err) {
71
- showSaveStatus('Error: ' + err.message, true);
112
+ setChip(err.code === 'unconfigured' ? 'Needs token' : 'Not connected', err.code === 'unconfigured' ? 'warning' : 'error');
113
+ showResult(false, null, err.message);
114
+ } finally {
115
+ setBusy(btn, false);
72
116
  }
73
117
  });
74
118
 
75
- // ---- Test connection ----
76
- btnTest.addEventListener('click', async () => {
77
- connectionStatus.textContent = 'Testing…';
78
- connectionStatus.className = 'connection-status';
79
-
119
+ $('btn-save-connection').addEventListener('click', async () => {
120
+ const btn = $('btn-save-connection');
121
+ setBusy(btn, true);
80
122
  try {
81
- // Temporarily apply the input values for the test
82
- const testUrl = brainUrlInput.value.trim() || 'http://127.0.0.1:3000';
83
- const testPat = patInput.value.trim();
123
+ const { brainUrl, pat } = readConnectionForm();
124
+ if (!(await ensureHostPermission(brainUrl))) {
125
+ throw new Error(`Without permission to reach ${new URL(brainUrl).host}, the extension can't use that brain.`);
126
+ }
127
+ await chrome.storage.local.set({ brainUrl, pat });
128
+ await chrome.storage.sync.remove(['pat', 'brainUrl']);
129
+ $('brain-url').value = brainUrl;
130
+ try {
131
+ const result = await testConnection();
132
+ setChip('Connected', 'success');
133
+ showResult(true, [['Saved', 'Connection works'], ...describeTest(result)]);
134
+ } catch (err) {
135
+ setChip('Saved · not connected', 'warning');
136
+ showResult(false, null, `Saved, but the test failed: ${err.message}`);
137
+ }
138
+ } catch (err) {
139
+ showResult(false, null, err.message);
140
+ } finally {
141
+ setBusy(btn, false);
142
+ }
143
+ });
144
+
145
+ $('btn-reveal').addEventListener('click', () => {
146
+ const input = $('pat-token');
147
+ const show = input.type === 'password';
148
+ input.type = show ? 'text' : 'password';
149
+ $('btn-reveal').setAttribute('aria-pressed', String(show));
150
+ $('btn-reveal').setAttribute('aria-label', show ? 'Hide token' : 'Show token');
151
+ });
84
152
 
85
- const headers = { 'Content-Type': 'application/json' };
86
- if (testPat) headers['Authorization'] = `Bearer ${testPat}`;
153
+ $('link-integrations').addEventListener('click', async (e) => {
154
+ e.preventDefault();
155
+ let base;
156
+ try { base = readConnectionForm().brainUrl; } catch { base = Brain.DEFAULT_BRAIN_URL; }
157
+ chrome.tabs.create({ url: `${base}/integrations` });
158
+ });
87
159
 
88
- const res = await fetch(`${testUrl}/health`, { headers });
89
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
160
+ // ---- Privacy ----
90
161
 
91
- connectionStatus.textContent = '✓ Connected';
92
- connectionStatus.className = 'connection-status ok';
93
- } catch (err) {
94
- connectionStatus.textContent = `✗ ${err.message}`;
95
- connectionStatus.className = 'connection-status fail';
96
- }
162
+ function syncPillSetting() {
163
+ const on = $('opt-page-recall').checked;
164
+ $('opt-show-pill').disabled = !on;
165
+ $('pill-setting').classList.toggle('is-disabled', !on);
166
+ }
167
+ $('opt-page-recall').addEventListener('change', syncPillSetting);
168
+
169
+ $('btn-save-privacy').addEventListener('click', async () => {
170
+ const blocklist = $('blocklist').value
171
+ .split('\n')
172
+ .map((line) => line.trim().toLowerCase().replace(/^[a-z]+:\/\//, '').replace(/\/.*$/, ''))
173
+ .filter(Boolean);
174
+ const unique = [...new Set(blocklist)];
175
+ await chrome.storage.sync.set({
176
+ pageRecall: $('opt-page-recall').checked,
177
+ showPill: $('opt-show-pill').checked,
178
+ blocklist: unique,
179
+ });
180
+ $('blocklist').value = unique.join('\n');
181
+ const saved = $('privacy-saved');
182
+ saved.hidden = false;
183
+ setTimeout(() => { saved.hidden = true; }, 2200);
97
184
  });
98
185
 
99
- // ---- Save status indicator ----
100
- function showSaveStatus(message, isError) {
101
- saveStatus.textContent = message;
102
- saveStatus.className = `save-status ${isError ? 'error' : 'saved'}`;
103
- setTimeout(() => {
104
- saveStatus.textContent = '';
105
- saveStatus.className = 'save-status';
106
- }, 3000);
186
+ // ---- Shortcuts ----
187
+
188
+ async function renderShortcuts() {
189
+ const labels = {
190
+ _execute_action: 'Open the side panel',
191
+ 'remember-page': 'Remember page (or selection)',
192
+ };
193
+ const commands = await chrome.commands.getAll();
194
+ const list = $('shortcut-list');
195
+ list.replaceChildren(...commands.filter((c) => labels[c.name]).map((c) => {
196
+ const row = el('div');
197
+ row.append(el('dt', '', labels[c.name]));
198
+ const dd = el('dd');
199
+ if (c.shortcut) {
200
+ for (const key of c.shortcut.split(/\+/)) dd.append(el('span', 'kbd', key));
201
+ } else {
202
+ dd.append(el('span', 'unset', 'Not set'));
203
+ }
204
+ row.append(dd);
205
+ return row;
206
+ }));
107
207
  }
108
208
 
209
+ $('btn-shortcuts').addEventListener('click', () => chrome.tabs.create({ url: 'chrome://extensions/shortcuts' }));
210
+
109
211
  // ---- Init ----
110
- loadSettings();
212
+
213
+ async function init() {
214
+ const [local, sync] = await Promise.all([
215
+ chrome.storage.local.get(['brainUrl', 'pat']),
216
+ chrome.storage.sync.get({ pageRecall: false, showPill: true, blocklist: [] }),
217
+ ]);
218
+ const config = await Brain.getConfig();
219
+ $('brain-url').value = local.brainUrl || config.brainUrl;
220
+ $('pat-token').value = config.pat || '';
221
+ $('opt-page-recall').checked = !!sync.pageRecall;
222
+ $('opt-show-pill').checked = sync.showPill !== false;
223
+ $('blocklist').value = (sync.blocklist || []).join('\n');
224
+ syncPillSetting();
225
+ renderShortcuts();
226
+
227
+ const foot = $('foot');
228
+ foot.replaceChildren(el('span', '', `Total Recall extension v${chrome.runtime.getManifest().version}`), el('span', '', 'Your data stays on your brain.'));
229
+
230
+ if (config.pat) $('btn-test').click();
231
+ }
232
+
233
+ init();
111
234
  })();