Skip to content

feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection#2001

Open
ManthanNimodiya wants to merge 7 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-runtime
Open

feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection#2001
ManthanNimodiya wants to merge 7 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-runtime

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2000 #1998 , only the last commits are new here, the earlier ones will disappear once the base PR merges.

Makes the Firefox build fully functional; every fix here was found and verified in live Firefox QA (recording → instant upload → share-page playback confirmed working on Firefox ESR 140):

  • Recorder host: Firefox has no offscreen API, so the recorder document runs in a small branded popup window, centered over the browser; a one-click arm button supplies the transient activation getDisplayMedia requires, then the window minimizes and self-closes when idle. Lifecycle hardened: URL-based context lookup, unresponsive windows are closed on ready-timeout, interactive survives send retries.
  • storage.session.setAccessLevel doesn't exist on Firefox, the unconditional call killed the background on every start; now optional-chained. Background + overlay ship as classic scripts on Firefox (module event pages/import() in content scripts are unreliable/unsupported, bugzil.la/1536094); the overlay is injected by the SW via a new inject-overlay-module message.
  • storage.session is entirely absent in Firefox content scripts, shared session-state keys route through storage.local on Firefox (SHARED_STATE_AREA), cleared at browser startup to keep session semantics.
  • Capability gating (tab-capture mode and system-audio hidden on Firefox), welcome-page host-permission grant (Firefox MV3 host perms are opt-in), camera-permission copy, and a build guard that fails if the bundle is missing the background script.
  • Advisory for review: Firefox 152.0.4 on macOS 26.5 crashed reproducibly in SpiderMonkey's module loader while loading extension pages (SIGABRT in js::ModuleEnvironmentObject, no MozCrashReason) — unrelated to this code (ESR 140 is fine), but worth a check on another 152 machine before AMO submission.

Greptile Summary

This PR adds a Firefox-capable build for the browser extension. The main changes are:

  • Separate Chrome and Firefox manifests and build outputs.
  • A recorder host that uses offscreen documents on Chrome and a popup window on Firefox.
  • Firefox-specific overlay loading through service-worker injection.
  • Shared recording UI state routed through Firefox-compatible storage.
  • Firefox capability gates for tab capture, system audio, and host permissions.

Confidence Score: 4/5

The Firefox recorder startup and local-file permission paths need fixes before merging.

  • Closing the Firefox recorder popup during startup can leave the UI stuck in creating.
  • The Firefox host-permission grant flow skips the declared file:///* origin.
  • The build split and overlay-loading paths otherwise line up with the changed manifests.

apps/chrome-extension/src/background/service-worker.ts, apps/chrome-extension/src/welcome/main.ts, apps/chrome-extension/src/popup/main.tsx

Important Files Changed

Filename Overview
apps/chrome-extension/src/background/service-worker.ts Adds Firefox recorder-host routing, shared-state cleanup, overlay injection, and window/permission listeners; the new window-removal path can leave creating status stale.
apps/chrome-extension/src/background/recorder-host.ts Adds the cross-browser recorder host abstraction for Chrome offscreen documents and Firefox popup windows.
apps/chrome-extension/src/recorder/arm-gesture.ts Adds the Firefox arm-button gesture and recorder-window minimize behavior.
apps/chrome-extension/src/recorder/recorder.ts Moves the recorder entrypoint and adds gesture waiting, audio-context resume, Firefox minimize behavior, and explicit streamable pipeline selection.
apps/chrome-extension/src/welcome/main.ts Adds the Firefox host-permission grant UI, but it does not request the manifest's declared file-page permission.
apps/chrome-extension/src/popup/main.tsx Adds Firefox capability-aware UI and a host-permission prompt link; its permission check mirrors the missing file-page origin.
apps/chrome-extension/src/content/bootstrap.ts Adds Firefox overlay loading through service-worker script injection and switches shared-state reads to the selected storage area.
apps/chrome-extension/src/shared/storage.ts Routes session-like shared keys through storage.local on Firefox and clears those keys on browser startup.
apps/chrome-extension/manifests/manifest.firefox.json Adds the Firefox MV3 manifest with classic background scripts, Firefox settings, and overlay resources.
apps/chrome-extension/vite.config.ts Adds target-aware output directories, recorder HTML output, manifest copying, and a background build guard.
apps/chrome-extension/vite.content-overlay.config.ts Builds the overlay as an ES module for Chrome and an IIFE global for Firefox.
apps/chrome-extension/package.json Splits Chrome and Firefox build/dev scripts and updates e2e tests to use the Chrome build.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/chrome-extension/src/background/service-worker.ts:1925-1928
**Creating State Stays Stuck**

When the Firefox recorder popup is closed while it is waiting on the arm button, this removal handler calls `syncRecordingStatus()`, but that path only resets statuses considered active. `creating` is not active, so the service worker keeps returning the stale starting state and the recording UI can stay stuck until another action overwrites it.

### Issue 2 of 2
apps/chrome-extension/src/welcome/main.ts:53
**File Permission Never Granted**

The Firefox manifest declares `file:///*` and the injection logic accepts `file:` tabs, but this permission request only asks for `http` and `https`. A Firefox user can complete the new grant flow and still never grant local-file access, so the content script, countdown, and recording toolbar will not run on `file://` pages despite the manifest advertising support.

Reviews (1): Last reviewed commit: "fix(extension): harden Firefox recorder ..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

@ManthanNimodiya ManthanNimodiya changed the title Feat/extension firefox runtime feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection Jul 11, 2026
Comment on lines +1925 to +1928
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Creating State Stays Stuck

When the Firefox recorder popup is closed while it is waiting on the arm button, this removal handler calls syncRecordingStatus(), but that path only resets statuses considered active. creating is not active, so the service worker keeps returning the stale starting state and the recording UI can stay stuck until another action overwrites it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/service-worker.ts
Line: 1925-1928

Comment:
**Creating State Stays Stuck**

When the Firefox recorder popup is closed while it is waiting on the arm button, this removal handler calls `syncRecordingStatus()`, but that path only resets statuses considered active. `creating` is not active, so the service worker keeps returning the stale starting state and the recording UI can stay stuck until another action overwrites it.

How can I resolve this? If you propose a fix, please make it concise.

// script (overlay, countdown, recording bar) stays inert until the user
// grants access here. The click on the button supplies the required user
// gesture for permissions.request.
const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 File Permission Never Granted

The Firefox manifest declares file:///* and the injection logic accepts file: tabs, but this permission request only asks for http and https. A Firefox user can complete the new grant flow and still never grant local-file access, so the content script, countdown, and recording toolbar will not run on file:// pages despite the manifest advertising support.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/welcome/main.ts
Line: 53

Comment:
**File Permission Never Granted**

The Firefox manifest declares `file:///*` and the injection logic accepts `file:` tabs, but this permission request only asks for `http` and `https`. A Firefox user can complete the new grant flow and still never grant local-file access, so the content script, countdown, and recording toolbar will not run on `file://` pages despite the manifest advertising support.

How can I resolve this? If you propose a fix, please make it concise.

// setAccessLevel at all — calling it unconditionally throws and kills the
// whole background script — so content scripts there rely on the runtime
// message fallbacks instead of the session-storage mirror.
chrome.storage.session.setAccessLevel?.({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor hardening: this still throws if chrome.storage.session is undefined (not just setAccessLevel).

Suggested change
chrome.storage.session.setAccessLevel?.({
chrome.storage.session?.setAccessLevel?.({

Comment on lines +1925 to +1928
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fires on any window close; quick guard avoids extra getContexts calls when idle.

Suggested change
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
if (recordingStatus.phase === "idle") return;
void syncRecordingStatus().catch(() => undefined);
});

Comment on lines +42 to +45
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__dirname can be undefined under ESM (and this package is type: module). using import.meta.url keeps this stable in vitest.

Suggested change
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),
readFileSync(
new URL(`../../manifests/manifest.${target}.json`, import.meta.url),
"utf8",
),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant