total-recall-brain 3.28.1 → 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 (136) 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/connect.mjs +5 -7
  48. package/src/cli/harness.mjs +1 -1
  49. package/src/cli/init.mjs +7 -5
  50. package/src/cli/mesh.mjs +118 -12
  51. package/src/cli/plugin/create.mjs +144 -25
  52. package/src/cli/plugin/index.mjs +83 -53
  53. package/src/cli/plugin/install.mjs +10 -127
  54. package/src/cli/plugin/list.mjs +44 -44
  55. package/src/cli/plugin/remove.mjs +23 -30
  56. package/src/cli/plugin/scaffold-cmd.mjs +30 -0
  57. package/src/cli/plugin/search.mjs +108 -23
  58. package/src/cli/rebuild.mjs +37 -1
  59. package/src/cli/recall.mjs +17 -2
  60. package/src/cli/research.mjs +4 -6
  61. package/src/cli/share.mjs +2 -0
  62. package/src/cli/skill-projection.mjs +13 -13
  63. package/src/cli/status.mjs +42 -31
  64. package/src/core/app-deploy/apply.mjs +386 -0
  65. package/src/core/app-deploy/design-tokens.mjs +217 -0
  66. package/src/core/app-deploy/plan.mjs +249 -0
  67. package/src/core/app-deploy/resolve.mjs +295 -0
  68. package/src/core/app-deploy/source.mjs +337 -0
  69. package/src/core/app-deploy/standalone.mjs +248 -0
  70. package/src/core/app-deploy/upgrade.mjs +256 -0
  71. package/src/core/app-deploy/verify.mjs +213 -0
  72. package/src/core/bound-hosts.mjs +1 -1
  73. package/src/core/brain-registry.mjs +146 -0
  74. package/src/core/brain-state.json +1 -0
  75. package/src/core/daemon-loop.mjs +30 -26
  76. package/src/core/dream.mjs +5 -17
  77. package/src/core/embeddings.mjs +14 -4
  78. package/src/core/fact-seeker.mjs +53 -368
  79. package/src/core/lan-discovery.mjs +3 -3
  80. package/src/core/mesh-access-sync.mjs +134 -0
  81. package/src/core/mesh-access.mjs +18 -3
  82. package/src/core/mesh-discover.mjs +173 -0
  83. package/src/core/mesh.mjs +131 -32
  84. package/src/core/optimizer.mjs +4 -41
  85. package/src/core/plugin-bundle.mjs +167 -0
  86. package/src/core/plugin-context.mjs +8 -13
  87. package/src/core/plugin-loader.mjs +272 -68
  88. package/src/core/plugin-peers.mjs +163 -0
  89. package/src/core/plugin-public.mjs +85 -0
  90. package/src/core/plugin-runner-child.mjs +24 -0
  91. package/src/core/plugin-runner.mjs +102 -0
  92. package/src/core/plugin-store.mjs +384 -0
  93. package/src/core/plugin-tasks.mjs +212 -0
  94. package/src/core/post-mortem.mjs +40 -9
  95. package/src/core/research-gate.mjs +182 -0
  96. package/src/core/research-queue.mjs +47 -7
  97. package/src/core/research-surface.mjs +211 -0
  98. package/src/core/research.mjs +25 -32
  99. package/src/core/scheduler.mjs +67 -153
  100. package/src/core/schema.mjs +129 -0
  101. package/src/core/search.mjs +12 -1
  102. package/src/core/secret-integration-research.mjs +12 -8
  103. package/src/core/secrets-store.mjs +32 -2
  104. package/src/core/session-watcher.mjs +6 -0
  105. package/src/core/surface.mjs +64 -3
  106. package/src/core/task-executors.mjs +8 -45
  107. package/src/core/vault-cache.mjs +70 -4
  108. package/src/server/api.mjs +21 -0
  109. package/src/server/rest.mjs +2 -0
  110. package/src/server/route-manifest.json +21 -5
  111. package/src/server/routes/collab.mjs +38 -11
  112. package/src/server/routes/extension.mjs +3 -2
  113. package/src/server/routes/memory.mjs +24 -2
  114. package/src/server/routes/mesh.mjs +7 -7
  115. package/src/server/routes/plugins-mesh.mjs +49 -0
  116. package/src/server/routes/plugins.mjs +91 -426
  117. package/src/server/routes/research.mjs +5 -2
  118. package/src/server/routes/share.mjs +2 -0
  119. package/src/server/tools.mjs +29 -2
  120. package/extension/icons/README.md +0 -27
  121. package/extension/lib/preconfigured.js +0 -5
  122. package/extension/popup/popup.css +0 -227
  123. package/extension/popup/popup.html +0 -50
  124. package/extension/popup/popup.js +0 -175
  125. package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
  126. package/scaffold/.agent/plugins/.gitkeep +0 -0
  127. package/scaffold/.agent/plugins/code-quality/README.md +0 -16
  128. package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
  129. package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
  130. package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
  131. package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
  132. package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
  133. package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
  134. package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
  135. package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
  136. package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
@@ -1,517 +1,292 @@
1
- // Total Recall — Content Script
2
- // Injected into every page. Queries the brain for related memories and shows
3
- // a floating pill in the bottom-right corner when matches are found.
4
- // Clicking the pill opens an interactive overlay listing the related memories
5
- // and exposing quick actions (Remember page, Research page).
6
- // Uses Shadow DOM to isolate styles from the host page.
1
+ // Total Recall — content script
2
+ //
3
+ // 1. Answers GET_PAGE_CONTEXT so the side panel and capture flows get the page's
4
+ // readable text, description and current selection.
5
+ // 2. Page recall (opt-in): asks the service worker for memories genuinely
6
+ // related to this page and, when there are any, shows a small pill that opens
7
+ // a card listing them.
8
+ // 3. Shows a brief toast when a capture is triggered from a shortcut or menu.
9
+ //
10
+ // All UI lives in a closed shadow root and is built with textContent only —
11
+ // memory text is user-captured web content and must never reach innerHTML.
7
12
 
8
13
  (function () {
9
14
  'use strict';
10
15
 
11
- // ---- Capture page context ----
12
- const pageContext = {
13
- url: location.href,
14
- title: document.title,
15
- description: document.querySelector('meta[name="description"]')?.content || ''
16
- };
17
-
18
- // ---- Skip non-content pages ----
19
- if (pageContext.url.startsWith('chrome://') ||
20
- pageContext.url.startsWith('chrome-extension://') ||
21
- pageContext.url.startsWith('about:')) {
22
- return;
23
- }
24
-
25
- let pillHost = null;
26
- let currentMemories = [];
27
-
28
- // ---- Create floating pill and interactive overlay with Shadow DOM ----
29
- function createPill(count) {
30
- if (pillHost) pillHost.remove();
31
-
32
- pillHost = document.createElement('div');
33
- pillHost.id = 'total-recall-pill-host';
16
+ if (window.__totalRecallContentScript) return;
17
+ window.__totalRecallContentScript = true;
34
18
 
35
- const shadow = pillHost.attachShadow({ mode: 'closed' });
19
+ const TEXT_LIMIT = 12000;
36
20
 
37
- const style = document.createElement('style');
38
- style.textContent = `
39
- :host {
40
- all: initial;
41
- position: fixed;
42
- bottom: 20px;
43
- right: 20px;
44
- z-index: 2147483647;
45
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
46
- }
47
-
48
- .container {
49
- position: relative;
50
- display: flex;
51
- flex-direction: column;
52
- align-items: flex-end;
53
- }
54
-
55
- .pill {
56
- display: flex;
57
- align-items: center;
58
- gap: 8px;
59
- padding: 10px 16px;
60
- background: #11111b;
61
- color: #cdd6f4;
62
- border: 1px solid #313244;
63
- border-radius: 999px;
64
- font-size: 13px;
65
- font-weight: 600;
66
- cursor: pointer;
67
- box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
68
- transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
69
- user-select: none;
70
- opacity: 0;
71
- transform: translateY(10px);
72
- animation: pill-enter 0.3s ease forwards;
73
- }
21
+ // ---- Page context -------------------------------------------------------
74
22
 
75
- .pill:hover {
76
- border-color: #89b4fa;
77
- transform: translateY(-2px);
78
- box-shadow: 0 6px 24px rgba(137, 180, 250, 0.2);
79
- background: #1e1e2e;
80
- }
81
-
82
- .pill.active {
83
- border-color: #89b4fa;
84
- background: #1e1e2e;
85
- }
86
-
87
- .pill .brain-icon {
88
- font-size: 15px;
89
- line-height: 1;
90
- }
91
-
92
- .pill .count {
93
- display: inline-flex;
94
- align-items: center;
95
- justify-content: center;
96
- min-width: 18px;
97
- height: 18px;
98
- padding: 0 5px;
99
- background: #89b4fa;
100
- color: #11111b;
101
- border-radius: 999px;
102
- font-size: 11px;
103
- font-weight: 700;
104
- }
105
-
106
- /* Interactive Overlay Card */
107
- .card {
108
- display: none;
109
- flex-direction: column;
110
- width: 320px;
111
- max-height: 420px;
112
- background: rgba(17, 17, 27, 0.95);
113
- backdrop-filter: blur(12px);
114
- -webkit-backdrop-filter: blur(12px);
115
- border: 1px solid #313244;
116
- border-radius: 16px;
117
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
118
- position: absolute;
119
- bottom: 50px;
120
- right: 0;
121
- z-index: 2147483647;
122
- overflow: hidden;
123
- transform: scale(0.95) translateY(10px);
124
- opacity: 0;
125
- transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
126
- pointer-events: none;
127
- }
128
-
129
- .card.visible {
130
- display: flex;
131
- transform: scale(1) translateY(0);
132
- opacity: 1;
133
- pointer-events: auto;
134
- }
135
-
136
- .card-header {
137
- display: flex;
138
- align-items: center;
139
- justify-content: space-between;
140
- padding: 14px 16px;
141
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
142
- }
143
-
144
- .card-header h3 {
145
- margin: 0;
146
- font-size: 14px;
147
- font-weight: 700;
148
- color: #cdd6f4;
149
- display: flex;
150
- align-items: center;
151
- gap: 6px;
152
- }
153
-
154
- .card-header .close-btn {
155
- background: none;
156
- border: none;
157
- color: #a6adc8;
158
- font-size: 18px;
159
- cursor: pointer;
160
- padding: 0;
161
- line-height: 1;
162
- display: flex;
163
- align-items: center;
164
- justify-content: center;
165
- width: 20px;
166
- height: 20px;
167
- border-radius: 50%;
168
- transition: background 0.2s;
169
- }
170
-
171
- .card-header .close-btn:hover {
172
- color: #f38ba8;
173
- background: rgba(255, 255, 255, 0.05);
174
- }
175
-
176
- .memory-list {
177
- flex: 1;
178
- overflow-y: auto;
179
- padding: 12px 16px;
180
- display: flex;
181
- flex-direction: column;
182
- gap: 10px;
183
- }
184
-
185
- .memory-item {
186
- background: rgba(255, 255, 255, 0.02);
187
- border: 1px solid rgba(255, 255, 255, 0.04);
188
- border-radius: 10px;
189
- padding: 12px;
190
- cursor: pointer;
191
- transition: all 0.2s ease;
192
- }
193
-
194
- .memory-item:hover {
195
- background: rgba(255, 255, 255, 0.05);
196
- border-color: #89b4fa;
197
- }
198
-
199
- .memory-title {
200
- font-size: 12px;
201
- font-weight: 600;
202
- color: #89b4fa;
203
- margin-bottom: 4px;
204
- line-height: 1.4;
205
- }
23
+ function metaContent(selector) {
24
+ const el = document.querySelector(selector);
25
+ return (el && el.getAttribute('content') || '').trim();
26
+ }
206
27
 
207
- .memory-excerpt {
208
- font-size: 11px;
209
- color: #bac2de;
210
- line-height: 1.45;
211
- display: -webkit-box;
212
- -webkit-line-clamp: 3;
213
- -webkit-box-orient: vertical;
214
- overflow: hidden;
215
- word-break: break-word;
216
- }
217
-
218
- .memory-excerpt.expanded {
219
- display: block;
220
- -webkit-line-clamp: unset;
221
- }
28
+ function readableRoot() {
29
+ const candidates = ['article', 'main', '[role="main"]', '#content', '.post', '.article'];
30
+ for (const sel of candidates) {
31
+ const el = document.querySelector(sel);
32
+ if (el && el.innerText && el.innerText.trim().length > 400) return el;
33
+ }
34
+ return document.body;
35
+ }
222
36
 
223
- .memory-meta {
224
- display: flex;
225
- justify-content: space-between;
226
- font-size: 9px;
227
- color: #6c7086;
228
- margin-top: 8px;
229
- border-top: 1px solid rgba(255, 255, 255, 0.03);
230
- padding-top: 6px;
231
- }
37
+ function pageContext() {
38
+ const root = readableRoot();
39
+ return {
40
+ url: location.href,
41
+ title: document.title || metaContent('meta[property="og:title"]'),
42
+ description: metaContent('meta[name="description"]') || metaContent('meta[property="og:description"]'),
43
+ selection: String(window.getSelection ? window.getSelection() : '').trim(),
44
+ text: root ? root.innerText.slice(0, TEXT_LIMIT) : '',
45
+ };
46
+ }
232
47
 
233
- .card-actions {
234
- display: flex;
235
- gap: 8px;
236
- padding: 12px 16px;
237
- border-top: 1px solid rgba(255, 255, 255, 0.05);
238
- background: rgba(0, 0, 0, 0.2);
239
- }
48
+ // ---- Shadow UI ----------------------------------------------------------
49
+
50
+ const TOKENS = `
51
+ --bg: #0c1220; --bg-2: #121a2b; --bg-3: #172033; --hover: #1c2740;
52
+ --accent: #3b82f6; --accent-hover: #60a5fa; --accent-muted: rgba(59,130,246,.14);
53
+ --text: #f1f5f9; --text-2: #94a3b8; --text-3: #64748b;
54
+ --border: rgba(148,163,184,.14); --border-accent: rgba(59,130,246,.35);
55
+ --success: #34d399; --error: #f87171;
56
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
57
+ `;
58
+
59
+ const STYLE = `
60
+ :host { all: initial; position: fixed; right: 20px; bottom: 20px; z-index: 2147483646; ${TOKENS} font-family: var(--font); }
61
+ *, *::before, *::after { box-sizing: border-box; }
62
+ /* Fonts are set here, not on :host: the page-level host reset (all: initial
63
+ !important) outranks :host rules and would leave the page's serif font. */
64
+ .stack { display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
65
+ font-family: var(--font); font-size: 13px; line-height: 1.4; color: var(--text); -webkit-font-smoothing: antialiased; }
66
+ button { font: inherit; color: inherit; cursor: pointer; border: 0; background: none; }
67
+ svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; flex: none; }
68
+
69
+ .pill { display: inline-flex; align-items: center; gap: 8px; height: 36px; padding: 0 8px 0 12px;
70
+ background: rgba(12,18,32,.92); color: var(--text); border: 1px solid var(--border); border-radius: 999px;
71
+ font-size: 13px; font-weight: 600; letter-spacing: -.005em; backdrop-filter: blur(16px) saturate(1.4);
72
+ box-shadow: 0 8px 24px rgba(0,0,0,.35); animation: rise .28s cubic-bezier(.23,1,.32,1) both;
73
+ transition: border-color .14s, transform .14s; }
74
+ .pill:hover, .pill[aria-expanded="true"] { border-color: var(--border-accent); transform: translateY(-1px); }
75
+ .pill .mark { color: var(--accent-hover); }
76
+ .pill .count { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 999px; display: inline-grid; place-items: center;
77
+ background: var(--accent); color: #fff; font-size: 11px; font-weight: 700; }
78
+ .pill .dismiss { width: 22px; height: 22px; border-radius: 999px; display: grid; place-items: center; color: var(--text-3); }
79
+ .pill .dismiss:hover { background: var(--hover); color: var(--text); }
80
+ .pill .dismiss svg { width: 12px; height: 12px; }
81
+
82
+ .card { width: 340px; max-height: min(460px, 70vh); display: flex; flex-direction: column; overflow: hidden;
83
+ background: rgba(12,18,32,.96); color: var(--text); border: 1px solid var(--border); border-radius: 18px;
84
+ backdrop-filter: blur(20px) saturate(1.4); box-shadow: 0 24px 64px rgba(0,0,0,.45), 0 0 40px rgba(59,130,246,.12);
85
+ animation: rise .22s cubic-bezier(.23,1,.32,1) both; }
86
+ .card[hidden] { display: none; }
87
+ .head { display: flex; align-items: center; gap: 8px; padding: 14px 16px 12px; border-bottom: 1px solid var(--border); }
88
+ .head h2 { margin: 0; flex: 1; font-size: 13px; font-weight: 600; }
89
+ .head .sub { font-size: 11px; color: var(--text-3); font-weight: 500; }
90
+ .icon-btn { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; color: var(--text-2); }
91
+ .icon-btn:hover { background: var(--hover); color: var(--text); }
92
+ .list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 4px; }
93
+ .item { display: block; width: 100%; text-align: left; padding: 10px 10px 11px; border-radius: 12px; border: 1px solid transparent; }
94
+ .item:hover { background: var(--bg-2); border-color: var(--border); }
95
+ .item .title { font-size: 13px; font-weight: 600; line-height: 1.35; color: var(--text); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
96
+ .item .excerpt { margin-top: 4px; font-size: 12px; line-height: 1.5; color: var(--text-2); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
97
+ .item .meta { margin-top: 6px; display: flex; gap: 8px; font-size: 11px; color: var(--text-3); }
98
+ .item .cat { text-transform: capitalize; color: var(--accent-hover); }
99
+ .foot { display: flex; gap: 8px; padding: 10px 12px 12px; border-top: 1px solid var(--border); }
100
+ .btn { flex: 1; height: 32px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; gap: 6px;
101
+ font-size: 12px; font-weight: 600; background: var(--bg-3); border: 1px solid var(--border); color: var(--text); }
102
+ .btn:hover { border-color: var(--border-accent); background: var(--hover); }
103
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
104
+ .btn.primary:hover { background: var(--accent-hover); }
105
+ .btn:disabled { opacity: .55; cursor: default; }
106
+ .btn svg { width: 14px; height: 14px; }
107
+
108
+ .toast { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 12px; font-size: 12.5px; font-weight: 600;
109
+ background: rgba(12,18,32,.96); color: var(--text); border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.35);
110
+ animation: rise .22s cubic-bezier(.23,1,.32,1) both; }
111
+ .toast .dot { width: 8px; height: 8px; border-radius: 99px; background: var(--success); box-shadow: 0 0 10px var(--success); }
112
+ .toast.error .dot { background: var(--error); box-shadow: 0 0 10px var(--error); }
113
+ .toast.leaving { opacity: 0; transform: translateY(6px); transition: opacity .2s, transform .2s; }
114
+
115
+ @keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } to { opacity: 1; transform: none; } }
116
+ @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
117
+ `;
118
+
119
+ const ICONS = {
120
+ mark: '<path d="M6 20V11a6 6 0 0 1 12 0v9"/><path d="M9.5 20v-8.5a2.5 2.5 0 0 1 5 0V20"/><path d="M4 20h16"/>',
121
+ close: '<path d="M18 6 6 18M6 6l12 12"/>',
122
+ pin: '<path d="M12 17v5"/><path d="M9 10.76V6h6v4.76l1.8 2.4A1 1 0 0 1 16 15H8a1 1 0 0 1-.8-1.6z"/><path d="M8 3h8"/>',
123
+ panel: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M15 4v16"/>',
124
+ };
240
125
 
241
- .action-btn {
242
- flex: 1;
243
- padding: 8px 12px;
244
- border: 1px solid rgba(255, 255, 255, 0.05);
245
- border-radius: 8px;
246
- background: rgba(255, 255, 255, 0.04);
247
- color: #cdd6f4;
248
- font-size: 11px;
249
- font-weight: 600;
250
- cursor: pointer;
251
- display: flex;
252
- align-items: center;
253
- justify-content: center;
254
- gap: 4px;
255
- transition: all 0.2s ease;
256
- }
126
+ function icon(name) {
127
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
128
+ svg.setAttribute('viewBox', '0 0 24 24');
129
+ svg.setAttribute('aria-hidden', 'true');
130
+ // Static, extension-authored markup only.
131
+ svg.innerHTML = ICONS[name];
132
+ return svg;
133
+ }
257
134
 
258
- .action-btn:hover {
259
- background: #89b4fa;
260
- color: #11111b;
261
- border-color: #89b4fa;
262
- }
135
+ function el(tag, className, text) {
136
+ const node = document.createElement(tag);
137
+ if (className) node.className = className;
138
+ if (text !== undefined) node.textContent = text;
139
+ return node;
140
+ }
263
141
 
264
- .action-btn:disabled {
265
- opacity: 0.5;
266
- cursor: not-allowed;
267
- background: rgba(255, 255, 255, 0.02) !important;
268
- color: #6c7086 !important;
269
- border-color: transparent !important;
270
- }
142
+ let host = null;
143
+ let shadow = null;
144
+ let stack = null;
271
145
 
272
- .toast {
273
- position: absolute;
274
- bottom: 60px;
275
- left: 50%;
276
- transform: translateX(-50%);
277
- background: #2ec471;
278
- color: #11111b;
279
- padding: 8px 16px;
280
- border-radius: 8px;
281
- font-size: 12px;
282
- font-weight: 700;
283
- z-index: 2147483647;
284
- opacity: 0;
285
- box-shadow: 0 4px 12px rgba(46, 196, 113, 0.3);
286
- transition: opacity 0.3s ease, transform 0.3s ease;
287
- pointer-events: none;
288
- }
146
+ function ensureHost() {
147
+ if (host && document.documentElement.contains(host)) return;
148
+ host = document.createElement('total-recall-overlay');
149
+ host.id = 'total-recall-overlay';
150
+ shadow = host.attachShadow({ mode: 'closed' });
151
+ const style = document.createElement('style');
152
+ style.textContent = STYLE;
153
+ stack = el('div', 'stack');
154
+ shadow.append(style, stack);
155
+ document.documentElement.appendChild(host);
156
+ }
289
157
 
290
- .toast.visible {
291
- opacity: 1;
292
- transform: translateX(-50%) translateY(-5px);
293
- }
158
+ // ---- Toast --------------------------------------------------------------
159
+
160
+ function showToast(text, tone) {
161
+ ensureHost();
162
+ const toast = el('div', `toast${tone === 'error' ? ' error' : ''}`);
163
+ toast.setAttribute('role', 'status');
164
+ toast.append(el('span', 'dot'), el('span', '', text));
165
+ stack.prepend(toast);
166
+ setTimeout(() => {
167
+ toast.classList.add('leaving');
168
+ setTimeout(() => toast.remove(), 220);
169
+ }, 2400);
170
+ }
294
171
 
295
- @keyframes pill-enter {
296
- to {
297
- opacity: 1;
298
- transform: translateY(0);
299
- }
300
- }
301
- `;
302
-
303
- const container = document.createElement('div');
304
- container.className = 'container';
305
-
306
- // The Floating Pill
307
- const pill = document.createElement('div');
308
- pill.className = 'pill';
309
- pill.innerHTML = `
310
- <span class="brain-icon">🧠</span>
311
- <span>Related Memories</span>
312
- <span class="count">${count}</span>
313
- `;
314
-
315
- // The Overlay Card
316
- const card = document.createElement('div');
317
- card.className = 'card';
318
-
319
- // Header
320
- const cardHeader = document.createElement('div');
321
- cardHeader.className = 'card-header';
322
- cardHeader.innerHTML = `
323
- <h3>🧠 Related Memories</h3>
324
- <button class="close-btn" title="Close Panel">×</button>
325
- `;
326
- card.appendChild(cardHeader);
327
-
328
- // List of Memories
329
- const memoryList = document.createElement('div');
330
- memoryList.className = 'memory-list';
331
-
332
- currentMemories.forEach(mem => {
333
- const item = document.createElement('div');
334
- item.className = 'memory-item';
335
-
336
- const relevancePercent = mem.confidence ? Math.round(mem.confidence * 100) : null;
337
- const relevanceStr = relevancePercent ? `${relevancePercent}% match` : 'Related';
338
-
339
- item.innerHTML = `
340
- <div class="memory-title">${mem.title}</div>
341
- <div class="memory-excerpt">${mem.content}</div>
342
- <div class="memory-meta">
343
- <span style="text-transform: capitalize; font-weight: 600;">${mem.category}</span>
344
- <span>${relevanceStr}</span>
345
- </div>
346
- `;
347
-
348
- // Click to toggle full content expansion
349
- item.addEventListener('click', () => {
350
- const excerpt = item.querySelector('.memory-excerpt');
351
- excerpt.classList.toggle('expanded');
352
- });
172
+ // ---- Related memories ---------------------------------------------------
173
+
174
+ function renderRelated(memories) {
175
+ ensureHost();
176
+ const card = el('section', 'card');
177
+ card.hidden = true;
178
+ card.setAttribute('aria-label', 'Related memories');
179
+
180
+ const head = el('header', 'head');
181
+ const titleWrap = el('div');
182
+ titleWrap.style.flex = '1';
183
+ const h2 = el('h2', '', 'You already know this');
184
+ const sub = el('div', 'sub', `${memories.length} related ${memories.length === 1 ? 'memory' : 'memories'} in your brain`);
185
+ titleWrap.append(h2, sub);
186
+ const closeBtn = el('button', 'icon-btn');
187
+ closeBtn.setAttribute('aria-label', 'Close');
188
+ closeBtn.append(icon('close'));
189
+ head.append(titleWrap, closeBtn);
190
+
191
+ const list = el('div', 'list');
192
+ for (const m of memories) {
193
+ const item = el('button', 'item');
194
+ item.title = 'Open in Total Recall';
195
+ item.append(el('div', 'title', m.title));
196
+ if (m.excerpt) item.append(el('div', 'excerpt', m.excerpt));
197
+ const meta = el('div', 'meta');
198
+ meta.append(el('span', 'cat', m.category));
199
+ if (typeof m.similarity === 'number') meta.append(el('span', '', `${Math.round(m.similarity * 100)}% similar`));
200
+ item.append(meta);
201
+ item.addEventListener('click', () => chrome.runtime.sendMessage({ type: 'OPEN_MEMORY', slug: m.slug }));
202
+ list.append(item);
203
+ }
353
204
 
354
- memoryList.appendChild(item);
355
- });
356
- card.appendChild(memoryList);
357
-
358
- // Quick Actions
359
- const cardActions = document.createElement('div');
360
- cardActions.className = 'card-actions';
361
-
362
- const rememberBtn = document.createElement('button');
363
- rememberBtn.className = 'action-btn';
364
- rememberBtn.innerHTML = '📌 Remember Page';
365
-
366
- const researchBtn = document.createElement('button');
367
- researchBtn.className = 'action-btn';
368
- researchBtn.innerHTML = '🔬 Research Page';
369
-
370
- cardActions.appendChild(rememberBtn);
371
- cardActions.appendChild(researchBtn);
372
- card.appendChild(cardActions);
373
-
374
- // Tiny Toast message indicator
375
- const toast = document.createElement('div');
376
- toast.className = 'toast';
377
- container.appendChild(toast);
378
-
379
- // Action button listeners
380
- const triggerAction = async (action, btn) => {
381
- btn.disabled = true;
382
- const originalText = btn.innerHTML;
383
- btn.innerHTML = '⏳ Processing...';
384
-
385
- chrome.runtime.sendMessage({
386
- type: 'SHARE',
387
- data: {
388
- url: pageContext.url,
389
- title: pageContext.title,
390
- action: action,
391
- source: 'chrome-extension-overlay'
392
- }
393
- }, (res) => {
394
- btn.disabled = false;
395
- btn.innerHTML = originalText;
396
- if (res && res.success) {
397
- toast.textContent = action === 'remember' ? '📌 Remembered page!' : '🔬 Research queued!';
398
- toast.classList.add('visible');
399
- setTimeout(() => toast.classList.remove('visible'), 2500);
400
- } else {
401
- let errText = '❌ Action failed';
402
- if (res && res.error) {
403
- if (res.error.includes('Authentication failed')) {
404
- errText = '❌ Auth Error - Check PAT';
405
- } else {
406
- errText = `❌ ${res.error}`;
407
- }
408
- }
409
- toast.textContent = errText;
410
- toast.style.background = '#e74c3c';
411
- toast.style.boxShadow = '0 4px 12px rgba(231, 76, 60, 0.3)';
412
- toast.classList.add('visible');
413
- setTimeout(() => {
414
- toast.classList.remove('visible');
415
- toast.style.background = '#2ec471';
416
- toast.style.boxShadow = '0 4px 12px rgba(46, 196, 113, 0.3)';
417
- }, 2500);
418
- }
419
- });
205
+ const foot = el('footer', 'foot');
206
+ const remember = el('button', 'btn primary');
207
+ remember.append(icon('pin'), el('span', '', 'Remember page'));
208
+ const open = el('button', 'btn');
209
+ open.append(icon('panel'), el('span', '', 'Open panel'));
210
+ foot.append(remember, open);
211
+ card.append(head, list, foot);
212
+
213
+ const pill = el('button', 'pill');
214
+ pill.setAttribute('aria-expanded', 'false');
215
+ pill.setAttribute('aria-label', `${memories.length} related memories`);
216
+ const mark = icon('mark');
217
+ mark.classList.add('mark');
218
+ const dismiss = el('span', 'dismiss');
219
+ dismiss.setAttribute('role', 'button');
220
+ dismiss.setAttribute('aria-label', 'Hide for this page');
221
+ dismiss.append(icon('close'));
222
+ pill.append(mark, el('span', '', 'Related'), el('span', 'count', String(memories.length)), dismiss);
223
+
224
+ const toggle = (open) => {
225
+ card.hidden = !open;
226
+ pill.setAttribute('aria-expanded', String(open));
420
227
  };
421
-
422
- rememberBtn.addEventListener('click', (e) => {
423
- e.stopPropagation();
424
- triggerAction('remember', rememberBtn);
425
- });
426
-
427
- researchBtn.addEventListener('click', (e) => {
228
+ pill.addEventListener('click', (e) => {
428
229
  e.stopPropagation();
429
- triggerAction('research', researchBtn);
430
- });
431
-
432
- // Toggle overlay visibility on pill click
433
- const toggleOverlay = (e) => {
434
- e.stopPropagation();
435
- const isVisible = card.classList.contains('visible');
436
- if (isVisible) {
437
- card.classList.remove('visible');
438
- pill.classList.remove('active');
439
- } else {
440
- card.classList.add('visible');
441
- pill.classList.add('active');
230
+ if (e.composedPath().includes(dismiss)) {
231
+ card.remove();
232
+ pill.remove();
233
+ return;
442
234
  }
443
- };
444
-
445
- pill.addEventListener('click', toggleOverlay);
446
- cardHeader.querySelector('.close-btn').addEventListener('click', toggleOverlay);
447
-
448
- // Prevent clicks inside card from closing it
235
+ toggle(card.hidden);
236
+ });
237
+ closeBtn.addEventListener('click', () => toggle(false));
449
238
  card.addEventListener('click', (e) => e.stopPropagation());
239
+ document.addEventListener('click', () => toggle(false));
240
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') toggle(false); });
450
241
 
451
- // Close overlay if clicking outside the host
452
- document.addEventListener('click', () => {
453
- card.classList.remove('visible');
454
- pill.classList.remove('active');
242
+ remember.addEventListener('click', () => {
243
+ remember.disabled = true;
244
+ chrome.runtime.sendMessage({ type: 'CAPTURE', options: { action: 'remember' } }, (res) => {
245
+ remember.disabled = false;
246
+ if (chrome.runtime.lastError) return;
247
+ showToast(res && res.success ? 'Saved to your brain' : (res && res.error) || 'Capture failed', res && res.success ? 'success' : 'error');
248
+ });
455
249
  });
250
+ open.addEventListener('click', () => chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL' }));
456
251
 
457
- container.appendChild(card);
458
- container.appendChild(pill);
459
-
460
- shadow.appendChild(style);
461
- shadow.appendChild(container);
462
- document.body.appendChild(pillHost);
252
+ stack.append(card, pill);
463
253
  }
464
254
 
465
- function isBlocked(blocklist) {
466
- if (!Array.isArray(blocklist) || blocklist.length === 0) return false;
467
- let host = '';
255
+ async function runPageRecall() {
256
+ let settings;
468
257
  try {
469
- host = new URL(pageContext.url).hostname.toLowerCase();
258
+ settings = await chrome.storage.sync.get({ pageRecall: false, showPill: true });
470
259
  } catch {
471
- return false;
260
+ return; // extension context invalidated (reloaded)
472
261
  }
473
- return blocklist.some((entry) => {
474
- const pattern = String(entry || '').trim().toLowerCase();
475
- return pattern && (host === pattern || host.endsWith(`.${pattern}`) || pageContext.url.toLowerCase().includes(pattern));
476
- });
477
- }
478
-
479
- // ---- Query brain after delay ----
480
- async function queryBrain() {
481
- const { passiveTracking = false, blocklist = [] } = await chrome.storage.sync.get(['passiveTracking', 'blocklist']);
482
- if (!passiveTracking || isBlocked(blocklist)) return;
483
-
484
- const query = pageContext.title || pageContext.url;
485
- if (!query) return;
486
-
262
+ if (!settings.pageRecall) return;
263
+ const ctx = pageContext();
487
264
  chrome.runtime.sendMessage(
488
- { type: 'QUERY_BRAIN', query, topK: 5 },
265
+ { type: 'QUERY_RELATED', page: { url: ctx.url, title: ctx.title, description: ctx.description } },
489
266
  (response) => {
490
267
  if (chrome.runtime.lastError) return;
491
- const memories = response?.memories || [];
492
- if (memories.length > 0) {
493
- currentMemories = memories;
494
- createPill(memories.length);
495
- }
496
- }
268
+ const memories = (response && response.memories) || [];
269
+ if (memories.length && settings.showPill) renderRelated(memories);
270
+ },
497
271
  );
498
272
  }
499
273
 
500
- // Wait 2 seconds after page load to avoid interfering with page rendering
501
- setTimeout(queryBrain, 2000);
274
+ // ---- Messages -----------------------------------------------------------
502
275
 
503
- // ---- Listen for page context requests ----
504
276
  chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
505
- if (msg.type === 'GET_PAGE_TEXT') {
506
- const selection = window.getSelection().toString().trim();
507
- const pageText = document.body.innerText.slice(0, 10000); // limit to 10k chars to avoid token blowout
508
- sendResponse({
509
- url: location.href,
510
- title: document.title,
511
- selection,
512
- pageText
513
- });
277
+ if (!msg) return false;
278
+ if (msg.type === 'GET_PAGE_CONTEXT') {
279
+ sendResponse(pageContext());
280
+ return false;
514
281
  }
515
- return true;
282
+ if (msg.type === 'TR_TOAST') {
283
+ showToast(String(msg.text || ''), msg.tone);
284
+ return false;
285
+ }
286
+ return false;
516
287
  });
288
+
289
+ // Let the page settle before querying; recall is never urgent.
290
+ if (document.readyState === 'complete') setTimeout(runPageRecall, 1500);
291
+ else window.addEventListener('load', () => setTimeout(runPageRecall, 1500), { once: true });
517
292
  })();