feat(extension): add Firefox WebExtension support
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s

- Add a neutral WebExtension API adapter that uses Firefox browser.* or Chromium chrome.* without mutating globals.
- Switch extension runtime code to the adapter and add Firefox-specific typings for tabs, windows, tab groups, storage, scripting, and native messaging ports.
- Fix Firefox temporary add-on instructions to load the packaged manifest with background.scripts instead of the Chromium service worker manifest.
- Detect Firefox in clients.list via runtime.getBrowserInfo and keep Chromium user-agent fallback support.
- Make navigate.open wait briefly for Firefox to replace initial about:blank with the requested URL.
- Add JS coverage for API selection, clients.list browser detection, and Firefox navigate.open URL polling.
- Bump package and extension version to 0.15.2.
This commit is contained in:
2026-06-14 19:09:10 +02:00
parent 523108e442
commit 477a00db1a
37 changed files with 526 additions and 183 deletions
+19 -16
View File
@@ -1,3 +1,4 @@
import { webExtApi as api } from '../browser-api';
import { getLargeOperationThrottle, getProfileAlias, getSessionTabs, getSessions, groupTabs, normalizeGroupColor, runLargeOperation, throwIfJobCancelled, updateJobProgress, updateTabGroup, yieldForLargeOperation } from '../core';
import { CommandGroup } from '../classes/CommandGroup';
import { AutoSaveManager } from './autosave';
@@ -32,18 +33,18 @@ export class SessionCommands extends CommandGroup {
const { session, tabCount } = await captureCurrentSession();
const sessions = await getSessions();
sessions[name] = session;
await chrome.storage.local.set({ sessions });
await api.storage.local.set({ sessions });
return { name, tabs: tabCount };
}
// Public: invoked from index.ts on chrome.tabs.onActivated.
// Public: invoked from index.ts on api.tabs.onActivated.
async activateLazyTab(tabId: number | string) {
const { lazySessionTabs } = await chrome.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
const { lazySessionTabs } = await api.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
const entry = lazySessionTabs?.[tabId];
if (!entry?.url) return false;
delete lazySessionTabs[tabId];
await chrome.storage.local.set({ lazySessionTabs });
await chrome.tabs.update(Number(tabId), { url: entry.url });
await api.storage.local.set({ lazySessionTabs });
await api.tabs.update(Number(tabId), { url: entry.url });
return true;
}
@@ -58,24 +59,24 @@ export class SessionCommands extends CommandGroup {
const throttle = await getLargeOperationThrottle(sessionTabs.length, gentleMode);
const createBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
const eagerLimit = lazy ? Math.max(0, Number(eagerTabs) || 0) : sessionTabs.length;
const { lazySessionTabs } = await chrome.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
const { lazySessionTabs } = await api.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
const lazyMap: LazySessionMap = lazySessionTabs || {};
updateJobProgress(__job, { phase: "opening", current: 0, total: sessionTabs.length });
for (const [idx, entry] of sessionTabs.entries()) {
throwIfJobCancelled(__job);
const shouldLazy = lazy && idx >= eagerLimit;
const tab = await chrome.tabs.create({ url: shouldLazy ? lazyPlaceholderUrl(entry.url) : entry.url, active: false, pinned: Boolean(entry.pinned) });
const tab = await api.tabs.create({ url: shouldLazy ? lazyPlaceholderUrl(entry.url) : entry.url, active: false, pinned: Boolean(entry.pinned) });
createdTabs.push({ tabId: tab.id, entry });
if (shouldLazy) {
lazyMap[String(tab.id)] = { url: entry.url, createdAt: Date.now() };
} else if (discardBackgroundTabs && !entry.pinned && chrome.tabs.discard) {
try { await chrome.tabs.discard(tab.id); } catch (_) {}
} else if (discardBackgroundTabs && !entry.pinned && api.tabs.discard) {
try { await api.tabs.discard(tab.id); } catch (_) {}
}
updateJobProgress(__job, { phase: shouldLazy ? "creating lazy placeholders" : "opening", current: createdTabs.length, total: sessionTabs.length });
await yieldForLargeOperation(createdTabs.length, createBatchSize, Math.max(50, throttle.pauseMs));
}
if (lazy) await chrome.storage.local.set({ lazySessionTabs: lazyMap });
if (lazy) await api.storage.local.set({ lazySessionTabs: lazyMap });
const groups = new Map();
for (const { tabId, entry } of createdTabs) {
@@ -119,7 +120,7 @@ export class SessionCommands extends CommandGroup {
const sessions = await getSessions();
if (!(name in sessions)) throw new Error(`Session '${name}' not found`);
delete sessions[name];
await chrome.storage.local.set({ sessions });
await api.storage.local.set({ sessions });
return { name };
}
@@ -154,16 +155,18 @@ export class SessionCommands extends CommandGroup {
if (!overwrite && sessions[name]) throw new Error(`Session '${name}' already exists`);
const stored = session as object as StoredSession;
sessions[name] = { ...stored, savedAt: Number(stored.savedAt) || Date.now() };
await chrome.storage.local.set({ sessions });
await api.storage.local.set({ sessions });
return { name, tabs: getSessionTabs(sessions[name]).length };
}
private async clientsList() {
const manifest = chrome.runtime.getManifest();
const manifest = api.runtime.getManifest();
const alias = await getProfileAlias();
const browserInfo = api.runtime.getBrowserInfo ? await api.runtime.getBrowserInfo() : null;
const userAgent = navigator.userAgent;
return [{
name: "Chrome",
version: navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1] || "unknown",
name: browserInfo?.name || (userAgent.includes("Firefox/") ? "Firefox" : "Chrome"),
version: browserInfo?.version || userAgent.match(/(?:Chrome|Firefox)\/([\d.]+)/)?.[1] || "unknown",
platform: navigator.platform,
extensionVersion: manifest.version,
profile: alias,
@@ -171,7 +174,7 @@ export class SessionCommands extends CommandGroup {
}
private async clientsRenameProfile({ alias }: ClientsRenameProfileArgs) {
await chrome.storage.local.set({ profileAlias: alias });
await api.storage.local.set({ profileAlias: alias });
return { alias };
}
}