LLM-generated summary cards anchored to verbatim quotes on any web page. A URL-keyed Chrome side panel for slow, evidence-linked reading.
English · 简体中文
- Why
- Features
- Quick Start
- Side Panel UX
- Permissions & Unsupported Pages
- Provider Settings
- Credential Boundary and Release Decision
- Development
- Project Structure
- E2E Contract
- Anchor Smoke Test
- Regression Gate
- Roadmap
- Contributing
- License
Most LLM "summarize this page" tools produce a paragraph of generated prose that you cannot trace back to the original article. Parallel Reader takes the opposite approach: every card is a verbatim quote from the page plus a short LLM gist, and the quote anchors back to the exact location in the live DOM so you can jump to it and verify in context. The side panel stores card state by URL, so a page can recover its cards after browser restart.
- Anchored summary cards — each card carries the literal quote and a click jumps to that exact text range in the page.
- Three-way anchor validation — every anchor is checked against raw page text, Mozilla Readability article text, and a live DOM Range, so you see honestly which cards are still locatable.
- Persistent per-URL cache with governance — switch pages, recreate tabs, or restart Chrome without losing cards. Each entry carries a content fingerprint and a configurable TTL (default 7 days), so when the page changes the side panel shows a "rerun" banner instead of silently restoring stale cards. Settings exposes one-click Clear cards for this page and Clear all cached pages.
- History / library view — list every URL you have analyzed with title, timestamp, and card count; reopen, delete individually, or bulk-export the whole library as Markdown / JSON.
- Copy quote / copy summary — works even when the live DOM no longer matches the cached anchor (DOM-miss cards are still useful for note-taking).
- Bilingual UI (English / Simplified Chinese) — the side panel,
manifest, and shortcut description all flow through
chrome.i18n. A Settings dropdown lets you override the interface language independently of the LLM-output language; auto follows the browser locale. New keys land in_locales/en/messages.jsonfirst and a parity test guards that_locales/zh_CN/messages.jsonstays in sync. - Configurable density and language — concise / normal / detailed bullet budget; Chinese or English summaries.
- Configurable card count — request between 4 and 10 cards per analysis.
- OpenAI-compatible BYOK — works with DeepSeek, DashScope, OpenAI-shaped endpoints; key is stored in the local Chrome profile only.
- Accessible side panel —
aria-livestatus, keyboard-navigable card context menu (Arrow / Home / End / Esc / Tab), persistent active-card highlight, visible:focus-visibleoutlines. - Anchor smoke CLI — batch-validate a list of URLs end-to-end against the real provider with a regression gate on DOM hit rate and readable text size.
npm install
npm run buildLoad the unpacked extension in Chrome:
- Open
chrome://extensions. - Enable Developer mode.
- Click Load unpacked.
- Select the project's
./distdirectory. - Click the extension toolbar icon to open the side panel.
- Save provider settings (see Provider Settings).
- Click Analyze current page.
Requires Chrome 114 or newer (the side panel API and MV3 service worker features used here are not available in older builds).
Each analyzed page reports the raw text length, the Readability article length, the version sent to the LLM, and the actual selected text length. A non-blocking quality line tells you what to expect:
| Tag | Meaning |
|---|---|
抽取正常 |
Selected text is long enough for normal card generation. |
可读文本偏短 |
Page may still be loading, gated by login, or not an article body. |
使用 Raw 文本 |
Readability could not extract a stable article body, so raw page text was used. |
These warnings do not stop analysis — they make partial-page state visible before you judge anchor hit rate or card quality.
Results are cached per tab and URL. When the current page already has saved
cards, the main action label changes from 分析当前页 to 重新分析当前页.
Rerun explicitly extracts the page again and replaces the cached result
only after a successful analysis. Existing cards stay visible while the
new run is in progress, so a failed provider call or a partially loaded page
does not erase the previous saved result.
Each card can copy its verbatim quote or a compact summary directly from the side panel. Copy actions remain available even when the card cannot be highlighted in the live DOM, so DOM-miss cards still help with note-taking and follow-up reading.
- The card context menu opens with right-click and is fully keyboard-driven:
↑/↓cycles non-disabled menu items.Home/Endjumps to the first / last item.Esccloses the menu.Tableaves the menu and closes it.
- The most recently activated card receives a persistent
card-activeaccent so you can scroll back without losing your reading position. - Status messages live in an
aria-live="polite"region; settings errors surface inline via arole="alert"paragraph. - All interactive elements (cards, primary action, icon buttons, debug
toggle) expose
:focus-visibleoutlines.
The prototype declares <all_urls> because the validation target is
arbitrary English-media articles, not a fixed allowlist. The content script
needs the page text and live DOM ranges from whichever article the reader
opens. activeTab and scripting are also used so the side panel can retry
content-script injection for the current active page after extension reloads.
Unsupported or restricted pages are handled before analysis:
- Browser internal pages and extension pages (e.g.
chrome://extensions,chrome-extension://...) cannot receive extension content scripts. - Browser PDF viewer pages are not supported by this prototype; use an article page or another copyable text view instead.
file://pages require enabling Allow access to file URLs for the unpacked extension inchrome://extensions.- Other non-HTTP protocols may fail because Chrome does not expose a normal page DOM to content scripts.
Settings are stored in Chrome local storage under parallel-reader-settings.
This is a local BYOK prototype: the API key lives on the current Chrome
profile and is used by the extension background worker.
Default settings:
| Setting | Default |
|---|---|
| Base URL | https://api.deepseek.com/v1 |
| Model | deepseek-chat |
| Card count | 4 to 10 |
| Summary language | Chinese (zh-CN) |
| Card density | Normal (normal) |
| Max document chars | 20000 |
Summary language can be switched to English when reading English media. Card density can be set to concise, normal, or detailed; it changes the prompt-level bullet count and detail budget without changing the anchor requirement.
For DashScope or another OpenAI-compatible endpoint, set the side-panel fields to the same API key, base URL, and model the extension should call. The smoke CLI also accepts these environment variables:
export DASHSCOPE_CODING_SK=...
export DASHSCOPE_CODING_BASE_URL=...
export DASHSCOPE_CODING_MODEL=qwen3-coder-plus # optional; defaults to qwen3-coder-plusDecision (2026-04-29): this project remains a local, developer-operated BYOK tool. It is acceptable for private unpacked-extension testing where the reader knowingly stores their own provider key in their own Chrome profile.
This is not an acceptable production credential model for public distribution. Before any Chrome Web Store release, provider calls must move behind one of the following:
- A backend service that owns provider credentials and applies auth, rate limits, request logging, and abuse controls.
- An ephemeral-token proxy that issues short-lived scoped credentials instead of persisting a long-lived provider key in extension storage.
Release implication: the current storage + background-worker provider call
path is a prototype boundary, not a launch boundary. Product work can
continue locally, but public release is blocked until the credential model
is replaced and reviewed.
npm run dev # watch build into dist/
npm run build # production-style local build
npm run check # lint + check:no-cjk + test + typecheck + build + audit
npm run check:no-cjk # fail when src/ contains hard-coded CJK outside the allow-list
npm run e2e # project-local .e2e contract gate (Playwright)
npm run e2e:linux # same gate inside a Linux container (mirrors CI Chromium-for-Testing)
npm run lint # Biome lint baseline for src/, scripts/, tests/, build config
npm test # node --test test suite
npm run typecheck # TypeScript only
npm audit # dependency auditThe build output in dist/ is what Chrome loads. After editing source
files, rebuild and reload the unpacked extension from chrome://extensions.
src/
background.ts MV3 service worker, provider call orchestration
content.ts page extraction + DOM Range location
sidepanel.ts side-panel entry; wires modules below
sidepanel.html / .css side-panel UI
shared/ pure helpers (no chrome.* calls)
anchor.ts anchor matching algorithms
anchor-repair.ts fuzzy fallback for anchor location
dom-anchor.ts DOM Range builder for the content script
extraction-quality.ts raw / readable / selected text classification
i18n.ts typed t() wrapper + applyI18n DOM bootstrap + locale override
json-extract.ts defensive JSON parsing for provider responses
logger.ts debug-flag-gated console.warn shim
page-support.ts URL/protocol allow-list for the side panel
prompt.ts prompt construction
provider.ts provider request/response shape
types.ts shared TypeScript types
sidepanel/ extracted side-panel modules
card-view.ts DOM-safe card rendering (no innerHTML)
clipboard.ts clipboard helpers + fallback
concurrency.ts runWithConcurrency, debounce
dom.ts $, escapeHtml, errorMessage helpers
history.ts per-URL history index + Markdown / JSON export
history-view.ts history panel rendering
menu.ts card context menu (keyboard navigable)
page-identity.ts URL normalization for cache key
settings-form.ts settings panel binding + inline error
tests/ node --test specs (97 tests)
scripts/ anchor smoke CLI + e2e-linux container helper + check-no-cjk guard
tools/e2e/ vendored e2e_contract_validator (Python, gate-only)
.e2e/ project-local E2E contract (gate.sh + Playwright)
_locales/ chrome.i18n message bundles (en + zh_CN)
The project-local .e2e gate runs an extension-level smoke test against a
local article fixture:
npm run e2eThe gate builds the unpacked extension, launches Chrome, opens the
side-panel page, verifies the first-run provider setup guard, and checks
that the content script can extract and locate fixture article text without
provider credentials. Generated CTRF evidence is written to
.e2e/artifact.json.
UI strings live in _locales/<locale>/messages.json and flow through
chrome.i18n.getMessage via the typed t() helper in
src/shared/i18n.ts. The same module bundles the English and Chinese
message tables so a Settings dropdown can override the locale at
runtime, independent of the browser UI language.
To add a string:
- Add a new key to
_locales/en/messages.jsonwith a stable description and (when needed)$1/$2placeholders. - Mirror the key in
_locales/zh_CN/messages.jsonwith the same placeholders. The parity test intests/i18n.test.mjswill fail if you forget. - Append the key to the
LocaleKeyunion insrc/shared/i18n.ts. - Replace the literal in code with
t('keyName')(ort('keyName', [arg1, arg2])for substitutions). For HTML, usedata-i18n="keyName"for text content ordata-i18n-attr="aria-label:keyName1;title:keyName2"for attributes;applyI18n()walks the DOM at boot.
To add a new locale, drop a new _locales/<locale>/messages.json next
to the existing ones (Chrome's locale codes use underscores, e.g.
zh_TW, ja, de). Tests and the check-no-cjk guard cover the
existing locales only; extend them if you start using glyphs from
another writing system that needs guarding.
scripts/check-no-cjk.mjs runs as part of npm run check and fails
when src/ contains hard-coded CJK characters outside an allow-list
(currently only src/shared/prompt.ts, which holds LLM-prompt
scaffolding governed by summaryLanguage).
Batch-validate a list of real article URLs end-to-end against the real
provider. Create a newline-delimited URL file (blank lines and # comments
are ignored):
https://arstechnica.com/google/2025/09/google-announces-massive-expansion-of-ai-features-in-chrome/
https://aeon.co/essays/sure-ai-can-do-writing-but-memoir-not-so-much
Run the smoke test:
npm run anchor:smoke -- \
--urls urls.txt \
--output-dir reports \
--timeout-ms 60000 \
--network-idle-ms 7000 \
--settle-ms 2500For each URL, the smoke test opens the page in Chrome, extracts raw and Readability text, calls the provider, and validates each returned anchor against:
- raw page text
- Readability article text
- DOM Range location on the live page
Reports are written as JSON and Markdown under reports/.
Use thresholds when you want the smoke test to fail with a non-zero exit code:
npm run anchor:smoke -- --urls urls.txt --min-dom-hit-rate 90% --min-readable-chars 1000You can also gate an existing JSON report without rerunning the browser or model:
npm run anchor:smoke -- \
--gate-report reports/anchor-smoke-2026-04-29T02-27-25-530Z.json \
--min-dom-hit-rate 90% --min-readable-chars 1000--min-dom-hit-rate accepts 0.9, 90, or 90%. --min-readable-chars
checks the selected text version sent to the model. A page that is still
loading or only partly extractable can still be inspected manually; the gate
lets each batch decide how strict it should be.
- Backend or ephemeral-token credential model (blocker for any public release — see Credential Boundary).
- More provider profiles (Anthropic, native OpenAI, local models) once the credential boundary is replaced.
- Per-card "open in note" export to Obsidian / Markdown.
- Additional UI locales (Japanese / Korean / German) — the
chrome.i18nplumbing is in place; new locales just need a_locales/<lang>/messages.jsonbundle.
Issues and PRs are welcome — especially anchor-correctness regressions on
real article URLs (please attach the URL and the JSON report from
npm run anchor:smoke).
Before opening a PR:
npm run check # tests + lint + typecheck + build + audit
npm run e2e # extension smoke (Playwright)Coding conventions are enforced by Biome (biome.json) and TypeScript
(tsconfig.json). Please do not modify either config in the same PR as a
behavior change.
License is not yet specified. Until a LICENSE file is added, treat the
source as "all rights reserved" by the repository author. If you would like
to use this code under a specific license, open an issue.