Manifest V3 extension for the podcast_reader desktop app:
submit the current tab for transcription, watch live progress in the popup,
get a notification on completion, and share a site login (cookie jar) when a
source needs authentication. Everything rides the engine's authenticated
localhost /v1 API; the user-mediated pairing flow (port + one-time code
from the desktop app's Settings) is how the extension obtains the bearer
token. See the repo README's "Chrome extension" section for the user-facing
walkthrough, and openspec/changes/chrome-extension/design.md for the
design record.
| Path | Purpose |
|---|---|
public/manifest.json |
MV3 manifest: least-privilege permissions, optional_permissions: cookies, no content scripts |
src/popup.ts + popup.html/popup.css |
The popup: pairing form, submit affordance, live progress (hydrate-then-stream), cookie capture |
src/sw.ts |
Service worker: context-menu submit + stateless 30 s alarm poll → notifications/badge |
src/client.ts |
Typed engine client (claim, health, jobs, events stream, cookies PUT) |
src/pairing.ts, src/connection.ts |
Pairing input parsing + claim flow; popup-open connection probe |
src/etld.ts, src/capture.ts, src/netscape.ts |
Registrable-domain derivation, capture targeting, Netscape jar serialization |
src/storage.ts, src/tracking.ts, src/jobs-view.ts |
chrome.storage.local wrapper, tracked-job list, pure presentation/poll logic |
tests/e2e/ |
Playwright suites: real built extension + the app's mock engine |
scripts/zip.mjs |
Deterministic podcast-reader-extension.zip from dist/ |
Shared API types import from ../app/src/shared/types.ts — the single
comment-pinned mirror both TS consumers use (its key-set parity against the
real engine is asserted by app/tests/e2e/integration.spec.ts).
Requires Node >= 24 (the e2e mock engine runs TypeScript via native type stripping).
npm install
npm run typecheck # tsc --noEmit (src + tests + configs)
npm run lint # eslint (includes the textContent-only DOM fence)
npm run test # vitest unit tests
npm run build # vite MV3 build → dist/ + deterministic zip
npm run dist # alias of build: dist/ + podcast-reader-extension.zipThere is no dev-server mode: MV3 service workers and popups load from disk,
so the loop is npm run build → reload the extension (chrome://extensions
→ the refresh icon on the card). The popup can be inspected like any page
(right-click the toolbar icon → Inspect popup, or open
chrome-extension://<id>/popup.html in a tab).
npm run build- Open
chrome://extensions, enable Developer mode - Load unpacked → select
extension/dist
python3 scripts/repro.py extension # from the repository root
python3 scripts/repro.py extension --grep jobs # focused title match
python3 scripts/repro.py extension --check-only # prerequisites onlyThe root command diagnoses Node, Chromium, and display support, builds the real
extension, and uses Xvfb automatically on headless Linux. The harness
(tests/e2e/fixtures.ts) spawns the app's scriptable mock
engine (../app/tests/mock-engine/server.ts) and launches a persistent
Chromium context with --load-extension — extensions need a headed
Chromium, hence xvfb in CI (the extension job in
.github/workflows/ci.yml). The popup is driven as a tab
(chrome-extension://<id>/popup.html); Chrome offers no automatable path to
the real toolbar popup window, and the page is identical either way.
The toolbar-popup submit (activeTab grant path). The e2e suite drives
the popup as a tab, which never exercises the click-granted activeTab
path: submit from a real https page via the toolbar popup (activeTab grant
path — not automatable). Confirm the page URL appears in the popup and the
submission queues in the desktop app.
The optional-permission prompt. Chrome renders the
chrome.permissions.request prompt outside any
automatable surface, so the cookie-capture e2e stubs the API: the grant and
decline branches are both exercised popup-level in
tests/e2e/cookies.spec.ts (permissions.request stubbed to true/false).
Only the real prompt itself is untestable. After changes touching capture,
verify it once by hand:
- Pair with a running desktop app, then submit a members-only source so a
job fails with
download_auth_required(any logged-out paywalled video URL works). - Click "Share your
<domain>login" in the popup and confirm Chrome's prompt names thecookiespermission and only that site's origins. - Decline → confirm nothing happens and the affordance remains.
- Click again, accept → confirm "Login shared." and the retry affordance, and that the domain appears in the desktop app's Settings → Cookies.
npm run dist produces podcast-reader-extension.zip — entries sorted,
stored uncompressed, fixed timestamps, so identical build bytes give an
identical archive. That zip is the Chrome Web Store upload artifact;
publication itself is blocked on a developer account (see
openspec/changes/chrome-extension/tasks.md, task 9.3). Until then,
install is load-unpacked from the zip or a local build.
Listing assets checklist (prepare alongside the account):
- Store icon 128×128 PNG (
public/icons/icon128.pngis the source) - At least one 1280×800 (or 640×400) screenshot — popup with a running job is the natural shot
- Short description (≤132 chars) and detailed description
- Category (Productivity) and language
- Privacy disclosures: single purpose statement; justification for each
permission — notably the optional
cookiespermission + broad optional host patterns (requested per-site at capture time, cookie data sent only to the user's own localhost engine, never retained by the extension) - Privacy policy URL (required once the
cookiespermission is declared)