A minimal, local-first context & entry-recall engine for Chrome (MV3). It quietly indexes the pages you visit and lets you get back to "that page I saw" from a fast command palette — by keyword and by meaning. Your data stays 100% on-device; zero network at runtime.
Design & architecture: SpotRecall-dev-plan.md · Privacy: PRIVACY.md
On Chrome Store: https://chromewebstore.google.com/detail/spotrecall/lfdjmifmmmimonedmclkpckjebmcabcg
npm install
npm run build # -> dist/ (~22MB: code + ORT WASM, model NOT bundled)
npm test # pure-logic contract tests (39)In Chrome open chrome://extensions → enable Developer mode → Load
unpacked → select the dist/ folder.
- Popup (dropdown palette): click the toolbar icon or press
Cmd/Ctrl + Shift + K→ a search panel anchored under the icon. Click a result to open it in a new tab. - Panel (new tab): the "Open panel" button in the popup → full panel (semantic model download, dashboard, blocklist, backup, privacy).
- Recent pages / searches: with an empty query the popup shows recent searches (deletable) and recently-visited pages grouped by time; typing switches to relevance ranking.
- Cross-lingual search: find a page even when your query is in a different language — the query is translated on-device before searching (Chrome 138+; silently degrades when unavailable).
- i18n: English / 日本語 / 简体中文, auto-detected with a manual switcher in the popup and panel.
- Keyword (BM25) search works offline from install. Semantic search downloads a multilingual model (~135MB) once to a local cache; once it finishes, semantic search is active and works fully offline afterwards.
To ship the model inside the package so the extension never needs the network:
npm run download-model # bake the model into public/models (one-time, online)
npm run build # -> dist/ with the model (~155MB)The same code supports both local-bundled and runtime-download paths — whether you
run download-model decides which is used.
| Context | Responsibility |
|---|---|
Content capture |
Capture metadata/text of the top page (and readable same-origin sub-frames); watch interactions & SPA navigations; privacy filtering. Top frame only — ad/captcha iframes are excluded. |
| Popup (palette page) | The action-popup search UI (Preact): keyword + semantic results, recent searches/pages, i18n. |
| Panel (options page) | Dashboard, one-time model download with progress, blocklist, ZIP backup/restore, privacy controls. |
| Service Worker | Message routing + documentId staleness guard + capture lifecycle state machine + page-instance table + sole IndexedDB writer + GC + telemetry + offscreen management. |
| Offscreen document | Persistent model inference (multilingual-e5-small, int8; WebGPU→WASM), in-memory int8 vector store + brute-force search, on-device query translation, tasks replayed from IndexedDB. |
Search: BM25 (MiniSearch, CJK-aware via Intl.Segmenter) instant, plus on-device
vector search (async), cross-lingual query translation, weighted RRF fusion, and
relevance-first ordering (time buckets only in the empty-query browse mode).
npm test— URL normalization/dedup, weighted RRF + multi-list fusion, int8 quantization scoring (Recall@10 within 2% of the float32 baseline, MRR drop < 0.02), timeline bucketing, CJK BM25 tokenization, the page-instance state machine (refCount GC /documentIdstaleness / SPA / reload re-commit), DAO lifecycle + atomic transactions + LRU retention, and idempotent backup re-import.npm run build— clean typecheck, packaged extension, ORT WASM served from the extension origin (offline).
The code is wired; confirm these locally (see the dev plan §11):
- Zero-network / privacy: BM25 works offline from install; when semantic search downloads, use DevTools Network to confirm requests hit only the model host and carry no user data; semantic search still works offline once cached; WebGPU and WASM fallback both work.
- Main flow & SPA: browse → capture → recall → open; navigating between GitHub issues yields a distinct record per URL.
- Lifecycle races: the same URL in two tabs; closing one must not delete the shared record; no stale-message overwrites under fast open/close & redirects.
- Privacy: incognito records nothing; blocklisted domains record nothing; password/credit-card fields never enter the captured text.
- Cross-lingual: an English query finds a Japanese page (and vice versa) after the language pack is available.
- Performance benchmark: on a fixed machine/version, produce P50/P95 for BM25 / query embedding / dot-product / end-to-end.
- Recall quality: produce Recall@K / MRR / first-hit on a fixed labeled set
(
npm run eval:recall,npm run eval:bench— see eval/).
Page text is stored in plaintext in local IndexedDB — the same exposure as browser history (other local processes, backup files, DevTools can read it). Not adding static encryption is a deliberate, threat-model-based decision. Incognito windows and blocklisted sites are never recorded; form values / passwords / credit-card fields never enter the captured text. The panel offers global pause, a per-site blocklist, one-click wipe, and backup export/import.
Full privacy policy (en/zh/ja): PRIVACY.md.
Submission kit (Chrome Web Store + Microsoft Edge Add-ons): see
STORE_LISTING.md. Screenshots and promo tiles are in
release-screenshots/ and reproducible with
npm run make-screenshots.
AGPL-3.0-only