Two independent signals feed one panel, per docs/05 §3: - capture/media.ts (background): webRequest-based, recognizes .m3u8/.mpd URLs and the HLS/DASH content types, deduped per (tab, url) so an HLS live-refresh doesn't re-fire. media-bridge.ts relays a hit to the tab's content script over runtime.sendMessage, and separately answers the content script's media.listVariants/media.addVariant calls by forwarding them to the background page's transport. - content/media-observer.ts: watches the page's own <video> elements (present now, added later, or with src changed) for the same extension signal, independent of what the network sniffer saw. Either firing opens content/video-panel.ts's "Download this video ▾" panel (built with createElement, matching the popup's innerHTML-free approach), which lists variants from media.listVariants and greys out any variant.drm or a wholly drmProtected manifest with "Protected content" rather than attempting it. The extension still never parses a manifest itself — that stays in the daemon, one language, one place. content/index.ts is the manifest-registered entry (content_scripts in manifest.json, added in the previous commit); build.mjs builds it as an IIFE rather than ESM, since a manifest content script has no "type": "module" declaration and an emitted top-level export would be a syntax error there. tsconfig.json adds DOM.Iterable for NodeList iteration. docs/05-extension-spec.md gets a short addendum (§8) documenting the popup/options bridge and this media-detection split, since neither was in the original design write-up. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
Owner: lane EXT. See ../docs/agents/AGENT-EXT.md and ../docs/05-extension-spec.md. Firefox MV3, TypeScript. Zero download logic.
Layout
src/background/transport/ Transport interface + WebSocket and native-messaging impls,
runtime picker. See docs/adr/0003 for why there are two.
src/shared/protocol/ GENERATED from ../contracts — never hand-edit.
tests/ vitest; webextension-polyfill is mocked in tests/setup.ts.
Develop
npm ci
npm run typecheck # tsc --noEmit, strict
npm test # vitest run
npm run lint # web-ext lint (AMO rules) — needs manifest.json
Build against tools/mockd over the WebSocket transport; veloxd is not required.
Transport quick start
import { createTransport } from './src/background/transport/index.js';
const t = await createTransport(); // reads the Options override; default 'auto'
t.onStateChange((s) => renderDot(s));
const rules = await t.call('capture.getRules', {});
createTransport resolves with a live, self-reconnecting transport. When veloxd is not
up yet it still resolves (WebSocket, status disconnected, retrying) — callers render the
red dot. It rejects only when the user explicitly forced native messaging and that failed.
Capture code treats every call() rejection as fail-open.