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
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:
@@ -522,6 +522,8 @@ npm run package:extension:firefox # Firefox zip, strips manifest.key and Firef
|
|||||||
|
|
||||||
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`. For Firefox, use the `*-firefox-*` zip.
|
Chrome Web Store rejects `manifest.key`, so upload the `*-webstore-*` zip from `dist/`. For Firefox, use the `*-firefox-*` zip.
|
||||||
|
|
||||||
|
For Firefox temporary testing via `about:debugging#/runtime/this-firefox`, run `npm run package:extension:firefox` first and load `dist/extension-package-firefox/manifest.json`. Do **not** load `extension/manifest.json` directly: it is the Chromium MV3 manifest and Firefox currently rejects `background.service_worker` for temporary add-ons.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|||||||
@@ -78,9 +78,14 @@ def cmd_install(browser):
|
|||||||
console.print("\n[bold]Step 1:[/bold] Load the extension in your browser")
|
console.print("\n[bold]Step 1:[/bold] Load the extension in your browser")
|
||||||
console.print(f" 1. Open [cyan]{ext_url}[/cyan]")
|
console.print(f" 1. Open [cyan]{ext_url}[/cyan]")
|
||||||
if browser == "firefox":
|
if browser == "firefox":
|
||||||
console.print(" 2. Click [bold]Load Temporary Add-on...[/bold]")
|
repo_root = Path(__file__).parent.parent.parent
|
||||||
console.print(f" 3. Select: [cyan]{Path(__file__).parent.parent.parent / 'extension' / 'manifest.json'}[/cyan]")
|
firefox_manifest = repo_root / "dist" / "extension-package-firefox" / "manifest.json"
|
||||||
console.print(f" 4. Firefox extension ID is [cyan]{FIREFOX_EXTENSION_ID}[/cyan]")
|
console.print(" 2. Build the Firefox-compatible temporary extension:")
|
||||||
|
console.print(" [cyan]npm run package:extension:firefox[/cyan]")
|
||||||
|
console.print(" 3. Click [bold]Load Temporary Add-on...[/bold]")
|
||||||
|
console.print(f" 4. Select: [cyan]{firefox_manifest}[/cyan]")
|
||||||
|
console.print(" Do not select extension/manifest.json; Firefox currently rejects background.service_worker there.")
|
||||||
|
console.print(f" 5. Firefox extension ID is [cyan]{FIREFOX_EXTENSION_ID}[/cyan]")
|
||||||
console.print(" Note: Firefox support is experimental; tab-group commands require browser tab group APIs.\n")
|
console.print(" Note: Firefox support is experimental; tab-group commands require browser tab group APIs.\n")
|
||||||
else:
|
else:
|
||||||
console.print(" 2. Enable [bold]Developer mode[/bold] (top-right toggle)")
|
console.print(" 2. Enable [bold]Developer mode[/bold] (top-right toggle)")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.15.1",
|
"version": "0.15.2",
|
||||||
"description": "Control your browser from the terminal or Python SDK",
|
"description": "Control your browser from the terminal or Python SDK",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Cross-browser WebExtension API entry point.
|
||||||
|
*
|
||||||
|
* Firefox exposes the Promise-based WebExtension API as `browser.*`.
|
||||||
|
* Chromium exposes the same extension API as `chrome.*`.
|
||||||
|
* Runtime modules import this neutral adapter as `api`, so Firefox uses its
|
||||||
|
* native `browser` object and Chromium uses its native `chrome` object. No
|
||||||
|
* browser-specific global is faked or overwritten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { WebExtensionApi } from './types';
|
||||||
|
|
||||||
|
type WebExtensionGlobal = {
|
||||||
|
browser?: typeof browser;
|
||||||
|
chrome?: typeof chrome;
|
||||||
|
};
|
||||||
|
|
||||||
|
function currentApi(): typeof browser | typeof chrome {
|
||||||
|
const webExtensionGlobal = globalThis as object as WebExtensionGlobal;
|
||||||
|
const api = webExtensionGlobal.browser || webExtensionGlobal.chrome;
|
||||||
|
if (!api) {
|
||||||
|
throw new Error("WebExtension API is not available: expected browser.* or chrome.*");
|
||||||
|
}
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webExtApi = new Proxy({}, {
|
||||||
|
get(_target: object, property: string | symbol) {
|
||||||
|
return currentApi()[property as keyof ReturnType<typeof currentApi>];
|
||||||
|
},
|
||||||
|
}) as object as WebExtensionApi;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
import { CommandGroup } from './CommandGroup';
|
import { CommandGroup } from './CommandGroup';
|
||||||
import type { CommandContext, CommandEntry, CommandSpec } from './CommandGroup';
|
import type { CommandContext, CommandEntry, CommandSpec } from './CommandGroup';
|
||||||
import { NavigationCommands } from '../commands/navigation';
|
import { NavigationCommands } from '../commands/navigation';
|
||||||
@@ -74,7 +75,7 @@ export class CommandRegistry {
|
|||||||
/**
|
/**
|
||||||
* Builds the registry and registers every command group. The SessionCommands
|
* Builds the registry and registers every command group. The SessionCommands
|
||||||
* instance is returned alongside because index.ts wires its lifecycle methods
|
* instance is returned alongside because index.ts wires its lifecycle methods
|
||||||
* (chrome.tabs.onActivated → activateLazyTab) and NativeConnection references it
|
* (api.tabs.onActivated → activateLazyTab) and NativeConnection references it
|
||||||
* for the clients.rename_profile reconnect side-effect.
|
* for the clients.rename_profile reconnect side-effect.
|
||||||
*/
|
*/
|
||||||
export function assembleRegistry(ctx: CommandContext): { registry: CommandRegistry; session: SessionCommands } {
|
export function assembleRegistry(ctx: CommandContext): { registry: CommandRegistry; session: SessionCommands } {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
/**
|
/**
|
||||||
* Background-job retention helpers + the JobManager that owns the live job map.
|
* Background-job retention helpers + the JobManager that owns the live job map.
|
||||||
*
|
*
|
||||||
* `pruneFinishedJobs` / `MAX_FINISHED_JOBS` are kept free of chrome.* /
|
* `pruneFinishedJobs` / `MAX_FINISHED_JOBS` are kept free of api.* /
|
||||||
* service-worker side effects so the retention logic (memory-leak guard) can be
|
* service-worker side effects so the retention logic (memory-leak guard) can be
|
||||||
* unit-tested in isolation.
|
* unit-tested in isolation.
|
||||||
*/
|
*/
|
||||||
@@ -16,7 +17,7 @@ export const MAX_FINISHED_JOBS = 20;
|
|||||||
|
|
||||||
// Watchdog: if a runner never resolves/rejects (e.g. executeScript against a
|
// Watchdog: if a runner never resolves/rejects (e.g. executeScript against a
|
||||||
// dead tab), finalize the job as an error so its persist interval stops instead
|
// dead tab), finalize the job as an error so its persist interval stops instead
|
||||||
// of writing to chrome.storage.local every second forever.
|
// of writing to api.storage.local every second forever.
|
||||||
export const JOB_TIMEOUT_MS = 5 * 60 * 1000;
|
export const JOB_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,11 +66,11 @@ export class JobManager {
|
|||||||
const running = all.filter(job => job.status === "running");
|
const running = all.filter(job => job.status === "running");
|
||||||
const finished = all.filter(job => job.status !== "running").slice(-MAX_FINISHED_JOBS);
|
const finished = all.filter(job => job.status !== "running").slice(-MAX_FINISHED_JOBS);
|
||||||
const recentJobs = [...running, ...finished].map(({ __timer, __watchdog, ...rest }) => rest);
|
const recentJobs = [...running, ...finished].map(({ __timer, __watchdog, ...rest }) => rest);
|
||||||
await chrome.storage.local.set({ recentJobs });
|
await api.storage.local.set({ recentJobs });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict the oldest finished jobs once their count exceeds the retention cap.
|
// Evict the oldest finished jobs once their count exceeds the retention cap.
|
||||||
// Recent finished jobs remain queryable via chrome.storage.local (persistJobs)
|
// Recent finished jobs remain queryable via api.storage.local (persistJobs)
|
||||||
// even after eviction from the in-memory Map.
|
// even after eviction from the in-memory Map.
|
||||||
private pruneJobs() {
|
private pruneJobs() {
|
||||||
pruneFinishedJobs(this.jobs, MAX_FINISHED_JOBS);
|
pruneFinishedJobs(this.jobs, MAX_FINISHED_JOBS);
|
||||||
@@ -143,7 +144,7 @@ export class JobManager {
|
|||||||
async status({ jobId }: { jobId?: string }) {
|
async status({ jobId }: { jobId?: string }) {
|
||||||
const job = this.jobs.get(jobId);
|
const job = this.jobs.get(jobId);
|
||||||
if (job) return { ...job };
|
if (job) return { ...job };
|
||||||
const { recentJobs } = await chrome.storage.local.get<{ recentJobs?: Job[] }>("recentJobs");
|
const { recentJobs } = await api.storage.local.get<{ recentJobs?: Job[] }>("recentJobs");
|
||||||
const stored = (recentJobs || []).find(entry => entry.id === jobId);
|
const stored = (recentJobs || []).find(entry => entry.id === jobId);
|
||||||
if (!stored) throw new Error(`Job '${jobId}' not found`);
|
if (!stored) throw new Error(`Job '${jobId}' not found`);
|
||||||
return stored;
|
return stored;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
/**
|
/**
|
||||||
* Native-messaging port lifecycle: connect/keepalive/reconnect plus the inbound
|
* Native-messaging port lifecycle: connect/keepalive/reconnect plus the inbound
|
||||||
* message router that hands commands to the CommandRegistry.
|
* message router that hands commands to the CommandRegistry.
|
||||||
@@ -6,7 +7,7 @@
|
|||||||
import { getErrorMessage, getProfileAlias } from '../core';
|
import { getErrorMessage, getProfileAlias } from '../core';
|
||||||
import type { CommandRegistry } from './CommandRegistry';
|
import type { CommandRegistry } from './CommandRegistry';
|
||||||
import type { SessionCommands } from '../commands/session';
|
import type { SessionCommands } from '../commands/session';
|
||||||
import type { ControlMessage, ResponseMessage, IncomingMessage, PageRequest, DispatchArgs, Serializable } from '../types';
|
import type { ControlMessage, ResponseMessage, IncomingMessage, PageRequest, DispatchArgs, Serializable, RuntimePort } from '../types';
|
||||||
|
|
||||||
const NATIVE_HOST = "com.browsercli.host";
|
const NATIVE_HOST = "com.browsercli.host";
|
||||||
const DEBUG_LOG = false;
|
const DEBUG_LOG = false;
|
||||||
@@ -16,7 +17,7 @@ function debugLog(...args: Serializable[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NativeConnection {
|
export class NativeConnection {
|
||||||
private port: chrome.runtime.Port | null = null;
|
private port: RuntimePort | null = null;
|
||||||
private keepaliveEnabled = true;
|
private keepaliveEnabled = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -26,17 +27,17 @@ export class NativeConnection {
|
|||||||
|
|
||||||
/** Registers all runtime listeners and opens the initial connection. */
|
/** Registers all runtime listeners and opens the initial connection. */
|
||||||
start() {
|
start() {
|
||||||
chrome.runtime.onInstalled.addListener(() => this.connect());
|
api.runtime.onInstalled.addListener(() => this.connect());
|
||||||
chrome.runtime.onStartup.addListener(() => this.connect());
|
api.runtime.onStartup.addListener(() => this.connect());
|
||||||
chrome.runtime.onSuspend.addListener(() => {
|
api.runtime.onSuspend.addListener(() => {
|
||||||
this.disconnectPort({ sendBye: true });
|
this.disconnectPort({ sendBye: true });
|
||||||
});
|
});
|
||||||
chrome.windows.onCreated.addListener(() => {
|
api.windows.onCreated.addListener(() => {
|
||||||
this.keepaliveEnabled = true;
|
this.keepaliveEnabled = true;
|
||||||
if (!this.port) this.connect();
|
if (!this.port) this.connect();
|
||||||
});
|
});
|
||||||
chrome.windows.onRemoved.addListener(async () => {
|
api.windows.onRemoved.addListener(async () => {
|
||||||
const windows = await chrome.windows.getAll({});
|
const windows = await api.windows.getAll({});
|
||||||
if (windows.length > 0) return;
|
if (windows.length > 0) return;
|
||||||
|
|
||||||
this.keepaliveEnabled = false;
|
this.keepaliveEnabled = false;
|
||||||
@@ -46,15 +47,15 @@ export class NativeConnection {
|
|||||||
// Reconnect poll — wakes the worker to re-establish the native port if it
|
// Reconnect poll — wakes the worker to re-establish the native port if it
|
||||||
// dropped. 0.5 min is Chrome's minimum alarm period; lower values (e.g. 0.4)
|
// dropped. 0.5 min is Chrome's minimum alarm period; lower values (e.g. 0.4)
|
||||||
// are silently clamped and log a warning, so we set it explicitly.
|
// are silently clamped and log a warning, so we set it explicitly.
|
||||||
chrome.alarms.create("keepalive", { periodInMinutes: 0.5 });
|
api.alarms.create("keepalive", { periodInMinutes: 0.5 });
|
||||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
api.alarms.onAlarm.addListener((alarm) => {
|
||||||
if (alarm.name === "keepalive") {
|
if (alarm.name === "keepalive") {
|
||||||
if (!this.port && this.keepaliveEnabled) this.connect();
|
if (!this.port && this.keepaliveEnabled) this.connect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendControlMessage(targetPort: chrome.runtime.Port | null, message: ControlMessage) {
|
private sendControlMessage(targetPort: RuntimePort | null, message: ControlMessage) {
|
||||||
if (!targetPort) return;
|
if (!targetPort) return;
|
||||||
try {
|
try {
|
||||||
targetPort.postMessage(message);
|
targetPort.postMessage(message);
|
||||||
@@ -63,7 +64,7 @@ export class NativeConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendResponse(targetPort: chrome.runtime.Port | null, message: ResponseMessage) {
|
private sendResponse(targetPort: RuntimePort | null, message: ResponseMessage) {
|
||||||
if (!targetPort) return;
|
if (!targetPort) return;
|
||||||
try {
|
try {
|
||||||
targetPort.postMessage(message);
|
targetPort.postMessage(message);
|
||||||
@@ -90,12 +91,12 @@ export class NativeConnection {
|
|||||||
private async connect() {
|
private async connect() {
|
||||||
if (this.port || !this.keepaliveEnabled) return;
|
if (this.port || !this.keepaliveEnabled) return;
|
||||||
try {
|
try {
|
||||||
const nativePort = chrome.runtime.connectNative(NATIVE_HOST);
|
const nativePort = api.runtime.connectNative(NATIVE_HOST);
|
||||||
this.port = nativePort;
|
this.port = nativePort;
|
||||||
nativePort.onMessage.addListener((msg: IncomingMessage) => this.onMessage(msg));
|
nativePort.onMessage.addListener((msg: IncomingMessage) => this.onMessage(msg));
|
||||||
nativePort.onDisconnect.addListener(() => {
|
nativePort.onDisconnect.addListener(() => {
|
||||||
if (this.port === nativePort) this.port = null;
|
if (this.port === nativePort) this.port = null;
|
||||||
const err = chrome.runtime.lastError;
|
const err = api.runtime.lastError;
|
||||||
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
if (err) console.warn("[browser-cli] Native host disconnected:", err.message);
|
||||||
});
|
});
|
||||||
// Send hello so native host knows which profile/alias this is
|
// Send hello so native host knows which profile/alias this is
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { TabUpdateInfo } from '../types';
|
||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
import { getSessions, runLargeOperation, tabGroupsOnUpdated } from '../core';
|
import { getSessions, runLargeOperation, tabGroupsOnUpdated } from '../core';
|
||||||
import { captureCurrentSession } from './session-snapshot';
|
import { captureCurrentSession } from './session-snapshot';
|
||||||
|
|
||||||
@@ -16,31 +18,31 @@ export class AutoSaveManager {
|
|||||||
readonly autoSaveHandler = async (): Promise<void> => {
|
readonly autoSaveHandler = async (): Promise<void> => {
|
||||||
await this.scheduleAutoSave();
|
await this.scheduleAutoSave();
|
||||||
};
|
};
|
||||||
readonly autoSaveUpdatedHandler = async (_tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo = {}): Promise<void> => {
|
readonly autoSaveUpdatedHandler = async (_tabId: number, changeInfo: TabUpdateInfo = {}): Promise<void> => {
|
||||||
// Ignore noisy media/title/favicon/loading updates. Sessions only store URL and group/window structure.
|
// Ignore noisy media/title/favicon/loading updates. Sessions only store URL and group/window structure.
|
||||||
if (!("url" in changeInfo)) return;
|
if (!("url" in changeInfo)) return;
|
||||||
await this.scheduleAutoSave();
|
await this.scheduleAutoSave();
|
||||||
};
|
};
|
||||||
|
|
||||||
async setEnabled(enabled: boolean) {
|
async setEnabled(enabled: boolean) {
|
||||||
await chrome.storage.local.set({ autoSave: enabled });
|
await api.storage.local.set({ autoSave: enabled });
|
||||||
chrome.tabs.onCreated.removeListener(this.autoSaveHandler);
|
api.tabs.onCreated.removeListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onRemoved.removeListener(this.autoSaveHandler);
|
api.tabs.onRemoved.removeListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onMoved.removeListener(this.autoSaveHandler);
|
api.tabs.onMoved.removeListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onAttached.removeListener(this.autoSaveHandler);
|
api.tabs.onAttached.removeListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onDetached.removeListener(this.autoSaveHandler);
|
api.tabs.onDetached.removeListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onUpdated.removeListener(this.autoSaveUpdatedHandler);
|
api.tabs.onUpdated.removeListener(this.autoSaveUpdatedHandler);
|
||||||
tabGroupsOnUpdated()?.removeListener(this.autoSaveHandler);
|
tabGroupsOnUpdated()?.removeListener(this.autoSaveHandler);
|
||||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
||||||
this.autoSaveTimer = null;
|
this.autoSaveTimer = null;
|
||||||
this.autoSavePending = false;
|
this.autoSavePending = false;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
chrome.tabs.onCreated.addListener(this.autoSaveHandler);
|
api.tabs.onCreated.addListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onRemoved.addListener(this.autoSaveHandler);
|
api.tabs.onRemoved.addListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onMoved.addListener(this.autoSaveHandler);
|
api.tabs.onMoved.addListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onAttached.addListener(this.autoSaveHandler);
|
api.tabs.onAttached.addListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onDetached.addListener(this.autoSaveHandler);
|
api.tabs.onDetached.addListener(this.autoSaveHandler);
|
||||||
chrome.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler);
|
api.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler);
|
||||||
tabGroupsOnUpdated()?.addListener(this.autoSaveHandler);
|
tabGroupsOnUpdated()?.addListener(this.autoSaveHandler);
|
||||||
}
|
}
|
||||||
return { enabled };
|
return { enabled };
|
||||||
@@ -48,12 +50,12 @@ export class AutoSaveManager {
|
|||||||
|
|
||||||
private async saveAutoSessionIfChanged() {
|
private async saveAutoSessionIfChanged() {
|
||||||
const { session, signature, tabCount } = await captureCurrentSession();
|
const { session, signature, tabCount } = await captureCurrentSession();
|
||||||
const { autoSaveSignature } = await chrome.storage.local.get("autoSaveSignature");
|
const { autoSaveSignature } = await api.storage.local.get("autoSaveSignature");
|
||||||
if (autoSaveSignature === signature) return { skipped: true, tabs: tabCount };
|
if (autoSaveSignature === signature) return { skipped: true, tabs: tabCount };
|
||||||
|
|
||||||
const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
sessions.__auto__ = session;
|
sessions.__auto__ = session;
|
||||||
await chrome.storage.local.set({ sessions, autoSaveSignature: signature });
|
await api.storage.local.set({ sessions, autoSaveSignature: signature });
|
||||||
return { skipped: false, tabs: tabCount };
|
return { skipped: false, tabs: tabCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ export class AutoSaveManager {
|
|||||||
}
|
}
|
||||||
this.autoSaveInFlight = true;
|
this.autoSaveInFlight = true;
|
||||||
try {
|
try {
|
||||||
const { autoSave } = await chrome.storage.local.get("autoSave");
|
const { autoSave } = await api.storage.local.get("autoSave");
|
||||||
if (autoSave) await runLargeOperation("session.auto_save", () => this.saveAutoSessionIfChanged());
|
if (autoSave) await runLargeOperation("session.auto_save", () => this.saveAutoSessionIfChanged());
|
||||||
} finally {
|
} finally {
|
||||||
this.autoSaveInFlight = false;
|
this.autoSaveInFlight = false;
|
||||||
@@ -76,7 +78,7 @@ export class AutoSaveManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async scheduleAutoSave(delayMs = AUTOSAVE_DEBOUNCE_MS) {
|
private async scheduleAutoSave(delayMs = AUTOSAVE_DEBOUNCE_MS) {
|
||||||
const { autoSave } = await chrome.storage.local.get("autoSave");
|
const { autoSave } = await api.storage.local.get("autoSave");
|
||||||
if (!autoSave) return;
|
if (!autoSave) return;
|
||||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
||||||
this.autoSaveTimer = setTimeout(() => this.runAutoSave(), delayMs);
|
this.autoSaveTimer = setTimeout(() => this.runAutoSave(), delayMs);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { Tab } from '../types';
|
||||||
import { assertScriptableUrl, executeScript, fetchTabHtml, isBrowserErrorUrl, isErrorPageScriptError, resolveTabUrl } from '../core';
|
import { assertScriptableUrl, executeScript, fetchTabHtml, isBrowserErrorUrl, isErrorPageScriptError, resolveTabUrl } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
import type { DomArgs, DomEvalArgs, DomWaitForArgs, DomPollArgs, Serializable } from '../types';
|
import type { DomArgs, DomEvalArgs, DomWaitForArgs, DomPollArgs, Serializable } from '../types';
|
||||||
|
|
||||||
function fallbackForErrorPageDomOp(funcName: string, tab: chrome.tabs.Tab): Serializable {
|
function fallbackForErrorPageDomOp(funcName: string, tab: Tab): Serializable {
|
||||||
switch (funcName) {
|
switch (funcName) {
|
||||||
case "domExists":
|
case "domExists":
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
|
|
||||||
@@ -5,7 +6,7 @@ export class ExtensionCommands extends CommandGroup {
|
|||||||
readonly namespace = "extension";
|
readonly namespace = "extension";
|
||||||
readonly commands: Record<string, CommandEntry> = {
|
readonly commands: Record<string, CommandEntry> = {
|
||||||
"extension.reload": () => {
|
"extension.reload": () => {
|
||||||
setTimeout(() => chrome.runtime.reload(), 200);
|
setTimeout(() => api.runtime.reload(), 200);
|
||||||
return { reloading: true };
|
return { reloading: true };
|
||||||
},
|
},
|
||||||
"extension.info": () => this.extensionInfo(),
|
"extension.info": () => this.extensionInfo(),
|
||||||
@@ -29,9 +30,9 @@ export class ExtensionCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private extensionInfo() {
|
private extensionInfo() {
|
||||||
const manifest = chrome.runtime.getManifest();
|
const manifest = api.runtime.getManifest();
|
||||||
return {
|
return {
|
||||||
id: chrome.runtime.id,
|
id: api.runtime.id,
|
||||||
name: manifest.name,
|
name: manifest.name,
|
||||||
version: manifest.version,
|
version: manifest.version,
|
||||||
manifestVersion: manifest.manifest_version,
|
manifestVersion: manifest.manifest_version,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
import { asTabIds, buildTabBlocks, getLargeOperationThrottle, getTabGroup, groupTabs, moveTabGroup, processInBatches, queryTabGroups, resolveGroupId, runLargeOperation, tabInfo, ungroupTabs, updateTabGroup } from '../core';
|
import { asTabIds, buildTabBlocks, getLargeOperationThrottle, getTabGroup, groupTabs, moveTabGroup, processInBatches, queryTabGroups, resolveGroupId, runLargeOperation, tabInfo, ungroupTabs, updateTabGroup } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
@@ -18,7 +19,7 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async groupList() {
|
private async groupList() {
|
||||||
const groups = await queryTabGroups({});
|
const groups = await queryTabGroups({});
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
return groups.map(g => ({
|
return groups.map(g => ({
|
||||||
id: g.id,
|
id: g.id,
|
||||||
title: g.title,
|
title: g.title,
|
||||||
@@ -30,7 +31,7 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async groupTabs({ groupId }: GroupTabsArgs) {
|
private async groupTabs({ groupId }: GroupTabsArgs) {
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
return all.filter(t => t.groupId === groupId).map(tabInfo);
|
return all.filter(t => t.groupId === groupId).map(tabInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async groupClose({ groupId, gentleMode, __job }: GroupCloseArgs = {}) {
|
private async groupClose({ groupId, gentleMode, __job }: GroupCloseArgs = {}) {
|
||||||
return runLargeOperation("group.close", async () => {
|
return runLargeOperation("group.close", async () => {
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await api.tabs.query({});
|
||||||
const groupTabs = tabs.filter(t => t.groupId === groupId);
|
const groupTabs = tabs.filter(t => t.groupId === groupId);
|
||||||
const tabIds = groupTabs.map(t => t.id);
|
const tabIds = groupTabs.map(t => t.id);
|
||||||
const throttle = await getLargeOperationThrottle(tabIds.length, gentleMode);
|
const throttle = await getLargeOperationThrottle(tabIds.length, gentleMode);
|
||||||
@@ -57,7 +58,7 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async groupOpen({ name }: GroupOpenArgs) {
|
private async groupOpen({ name }: GroupOpenArgs) {
|
||||||
const tab = await chrome.tabs.create({ active: true });
|
const tab = await api.tabs.create({ active: true });
|
||||||
const groupId = await groupTabs({ tabIds: asTabIds([tab.id]) });
|
const groupId = await groupTabs({ tabIds: asTabIds([tab.id]) });
|
||||||
await updateTabGroup(groupId, { title: name });
|
await updateTabGroup(groupId, { title: name });
|
||||||
return { id: groupId, name };
|
return { id: groupId, name };
|
||||||
@@ -65,15 +66,15 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async groupAddTab({ group, url }: GroupAddTabArgs) {
|
private async groupAddTab({ group, url }: GroupAddTabArgs) {
|
||||||
const groupId = await resolveGroupId(group);
|
const groupId = await resolveGroupId(group);
|
||||||
const existingTabs = await chrome.tabs.query({ groupId });
|
const existingTabs = await api.tabs.query({ groupId });
|
||||||
const tab = await chrome.tabs.create({ url: url || "chrome://newtab/", active: true });
|
const tab = await api.tabs.create({ url: url || "chrome://newtab/", active: true });
|
||||||
await groupTabs({ tabIds: asTabIds([tab.id]), groupId });
|
await groupTabs({ tabIds: asTabIds([tab.id]), groupId });
|
||||||
// If a URL was provided, close any blank placeholder tabs left from group creation
|
// If a URL was provided, close any blank placeholder tabs left from group creation
|
||||||
if (url) {
|
if (url) {
|
||||||
const placeholders = existingTabs.filter(t =>
|
const placeholders = existingTabs.filter(t =>
|
||||||
t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/"
|
t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/"
|
||||||
);
|
);
|
||||||
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
|
if (placeholders.length) await api.tabs.remove(placeholders.map(t => t.id));
|
||||||
}
|
}
|
||||||
return { tabId: tab.id, groupId };
|
return { tabId: tab.id, groupId };
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,7 @@ export class GroupsCommands extends CommandGroup {
|
|||||||
private async groupMove({ group, forward, backward }: GroupMoveArgs) {
|
private async groupMove({ group, forward, backward }: GroupMoveArgs) {
|
||||||
const groupId = await resolveGroupId(group);
|
const groupId = await resolveGroupId(group);
|
||||||
const groupInfo = await getTabGroup(groupId);
|
const groupInfo = await getTabGroup(groupId);
|
||||||
const allTabs = await chrome.tabs.query({ windowId: groupInfo.windowId });
|
const allTabs = await api.tabs.query({ windowId: groupInfo.windowId });
|
||||||
allTabs.sort((a, b) => a.index - b.index);
|
allTabs.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
const blocks = buildTabBlocks(allTabs);
|
const blocks = buildTabBlocks(allTabs);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { Tab } from '../types';
|
||||||
import { getActiveTab, getAliases, groupTabs as groupTabIds, isBrowserErrorUrl, resolveGroupId, tabInfo, updateTabGroup } from '../core';
|
import { getActiveTab, getAliases, groupTabs as groupTabIds, isBrowserErrorUrl, resolveGroupId, tabInfo, updateTabGroup } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
@@ -26,19 +28,19 @@ export class NavigationCommands extends CommandGroup {
|
|||||||
const entry = Object.entries(aliases).find(([, v]) => v === windowName);
|
const entry = Object.entries(aliases).find(([, v]) => v === windowName);
|
||||||
if (entry) windowId = parseInt(entry[0]);
|
if (entry) windowId = parseInt(entry[0]);
|
||||||
}
|
}
|
||||||
const tab = await chrome.tabs.create({ url, active: Boolean(focus) && !background, windowId });
|
const tab = await api.tabs.create({ url, active: Boolean(focus) && !background, windowId });
|
||||||
if (groupNameOrId != null) {
|
if (groupNameOrId != null) {
|
||||||
let groupId;
|
let groupId;
|
||||||
try {
|
try {
|
||||||
groupId = await resolveGroupId(groupNameOrId);
|
groupId = await resolveGroupId(groupNameOrId);
|
||||||
// Close any blank placeholder tabs that were created when the group was made
|
// Close any blank placeholder tabs that were created when the group was made
|
||||||
const groupTabs = await chrome.tabs.query({ groupId });
|
const groupTabs = await api.tabs.query({ groupId });
|
||||||
const placeholders = groupTabs.filter(t =>
|
const placeholders = groupTabs.filter(t =>
|
||||||
t.id !== tab.id &&
|
t.id !== tab.id &&
|
||||||
(t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/")
|
(t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/")
|
||||||
);
|
);
|
||||||
await groupTabIds({ tabIds: [tab.id], groupId });
|
await groupTabIds({ tabIds: [tab.id], groupId });
|
||||||
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
|
if (placeholders.length) await api.tabs.remove(placeholders.map(t => t.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof Error) || !e.message.startsWith("No tab group found")) throw e;
|
if (!(e instanceof Error) || !e.message.startsWith("No tab group found")) throw e;
|
||||||
// Group doesn't exist — create it with the tab already in it
|
// Group doesn't exist — create it with the tab already in it
|
||||||
@@ -46,14 +48,37 @@ export class NavigationCommands extends CommandGroup {
|
|||||||
await updateTabGroup(groupId, { title: String(groupNameOrId) });
|
await updateTabGroup(groupId, { title: String(groupNameOrId) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { id: tab.id, url: tab.url };
|
const loadedTab = await this.waitForOpenedTabUrl(tab.id, url, tab);
|
||||||
|
return { id: loadedTab.id, url: loadedTab.url || loadedTab.pendingUrl || url };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForOpenedTabUrl(tabId: number, targetUrl: string, initialTab: Tab): Promise<Tab> {
|
||||||
|
const initialUrl = initialTab.url || initialTab.pendingUrl || "";
|
||||||
|
if (this.isOpenedTabUrlReady(initialUrl, targetUrl)) return initialTab;
|
||||||
|
|
||||||
|
const deadline = Date.now() + 2000;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const current = await api.tabs.get(tabId);
|
||||||
|
const currentUrl = current.url || current.pendingUrl || "";
|
||||||
|
if (this.isOpenedTabUrlReady(currentUrl, targetUrl)) return current;
|
||||||
|
await new Promise(r => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.tabs.get(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isOpenedTabUrlReady(currentUrl: string, targetUrl: string): boolean {
|
||||||
|
if (!currentUrl) return false;
|
||||||
|
if (currentUrl === targetUrl || currentUrl.startsWith(targetUrl)) return true;
|
||||||
|
if (targetUrl === "about:blank" || targetUrl === "chrome://newtab/") return currentUrl === targetUrl;
|
||||||
|
return currentUrl !== "about:blank" && currentUrl !== "chrome://newtab/";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navTo({ tabId, url }: NavToArgs) {
|
private async navTo({ tabId, url }: NavToArgs) {
|
||||||
const tab = await chrome.tabs.update(tabId, { url });
|
const tab = await api.tabs.update(tabId, { url });
|
||||||
const deadline = Date.now() + 1000;
|
const deadline = Date.now() + 1000;
|
||||||
while (tabId && Date.now() < deadline) {
|
while (tabId && Date.now() < deadline) {
|
||||||
const current = await chrome.tabs.get(tabId);
|
const current = await api.tabs.get(tabId);
|
||||||
const currentUrl = current.url || current.pendingUrl || "";
|
const currentUrl = current.url || current.pendingUrl || "";
|
||||||
if (currentUrl === url || currentUrl.startsWith(url)) {
|
if (currentUrl === url || currentUrl.startsWith(url)) {
|
||||||
return { id: current.id, url: currentUrl };
|
return { id: current.id, url: currentUrl };
|
||||||
@@ -65,35 +90,35 @@ export class NavigationCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async navReload({ tabId }: NavTabArgs, bypassCache: boolean) {
|
private async navReload({ tabId }: NavTabArgs, bypassCache: boolean) {
|
||||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||||
await chrome.tabs.reload(tab.id, { bypassCache });
|
await api.tabs.reload(tab.id, { bypassCache });
|
||||||
return { tabId: tab.id };
|
return { tabId: tab.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navBack({ tabId }: NavTabArgs) {
|
private async navBack({ tabId }: NavTabArgs) {
|
||||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||||
await chrome.tabs.goBack(tab.id);
|
await api.tabs.goBack(tab.id);
|
||||||
return { tabId: tab.id };
|
return { tabId: tab.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navForward({ tabId }: NavTabArgs) {
|
private async navForward({ tabId }: NavTabArgs) {
|
||||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||||
await chrome.tabs.goForward(tab.id);
|
await api.tabs.goForward(tab.id);
|
||||||
return { tabId: tab.id };
|
return { tabId: tab.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async navFocus({ pattern }: NavFocusArgs) {
|
private async navFocus({ pattern }: NavFocusArgs) {
|
||||||
// If pattern is a plain integer, treat it as a tab ID
|
// If pattern is a plain integer, treat it as a tab ID
|
||||||
const asInt = parseInt(pattern);
|
const asInt = parseInt(pattern);
|
||||||
let match: chrome.tabs.Tab | undefined;
|
let match: Tab | undefined;
|
||||||
if (!isNaN(asInt) && String(asInt) === String(pattern)) {
|
if (!isNaN(asInt) && String(asInt) === String(pattern)) {
|
||||||
match = await chrome.tabs.get(asInt);
|
match = await api.tabs.get(asInt);
|
||||||
} else {
|
} else {
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern)));
|
match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern)));
|
||||||
}
|
}
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
await chrome.windows.update(match.windowId, { focused: true });
|
await api.windows.update(match.windowId, { focused: true });
|
||||||
await chrome.tabs.update(match.id, { active: true });
|
await api.tabs.update(match.id, { active: true });
|
||||||
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
|
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +127,7 @@ export class NavigationCommands extends CommandGroup {
|
|||||||
const deadline = Date.now() + timeout;
|
const deadline = Date.now() + timeout;
|
||||||
const interval = 200;
|
const interval = 200;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const t = await chrome.tabs.get(tab.id);
|
const t = await api.tabs.get(tab.id);
|
||||||
const currentUrl = t.url || t.pendingUrl || "";
|
const currentUrl = t.url || t.pendingUrl || "";
|
||||||
if (isBrowserErrorUrl(currentUrl)) {
|
if (isBrowserErrorUrl(currentUrl)) {
|
||||||
throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`);
|
throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { Tab, TabGroup } from '../types';
|
||||||
import { normalizeGroupColor, queryTabGroups } from '../core';
|
import { normalizeGroupColor, queryTabGroups } from '../core';
|
||||||
import type { SessionTab, StoredSession } from '../types';
|
import type { SessionTab, StoredSession } from '../types';
|
||||||
|
|
||||||
export function buildSessionSnapshot(tabs: chrome.tabs.Tab[], groups: chrome.tabGroups.TabGroup[]): SessionTab[] {
|
export function buildSessionSnapshot(tabs: Tab[], groups: TabGroup[]): SessionTab[] {
|
||||||
const groupById = new Map(groups.map(group => [group.id, group]));
|
const groupById = new Map(groups.map(group => [group.id, group]));
|
||||||
return tabs
|
return tabs
|
||||||
.filter(tab => Boolean(tab.url || tab.pendingUrl))
|
.filter(tab => Boolean(tab.url || tab.pendingUrl))
|
||||||
@@ -27,7 +29,7 @@ export function buildSessionSnapshot(tabs: chrome.tabs.Tab[], groups: chrome.tab
|
|||||||
* its change-detection signature. Shared by session.save and the autosave path.
|
* its change-detection signature. Shared by session.save and the autosave path.
|
||||||
*/
|
*/
|
||||||
export async function captureCurrentSession(): Promise<{ session: StoredSession; signature: string; tabCount: number }> {
|
export async function captureCurrentSession(): Promise<{ session: StoredSession; signature: string; tabCount: number }> {
|
||||||
const tabs = await chrome.tabs.query({});
|
const tabs = await api.tabs.query({});
|
||||||
const groups = await queryTabGroups({});
|
const groups = await queryTabGroups({});
|
||||||
const sessionTabs = buildSessionSnapshot(tabs, groups);
|
const sessionTabs = buildSessionSnapshot(tabs, groups);
|
||||||
const signature = sessionSignature(sessionTabs);
|
const signature = sessionSignature(sessionTabs);
|
||||||
|
|||||||
@@ -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 { getLargeOperationThrottle, getProfileAlias, getSessionTabs, getSessions, groupTabs, normalizeGroupColor, runLargeOperation, throwIfJobCancelled, updateJobProgress, updateTabGroup, yieldForLargeOperation } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import { AutoSaveManager } from './autosave';
|
import { AutoSaveManager } from './autosave';
|
||||||
@@ -32,18 +33,18 @@ export class SessionCommands extends CommandGroup {
|
|||||||
const { session, tabCount } = await captureCurrentSession();
|
const { session, tabCount } = await captureCurrentSession();
|
||||||
const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
sessions[name] = session;
|
sessions[name] = session;
|
||||||
await chrome.storage.local.set({ sessions });
|
await api.storage.local.set({ sessions });
|
||||||
return { name, tabs: tabCount };
|
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) {
|
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];
|
const entry = lazySessionTabs?.[tabId];
|
||||||
if (!entry?.url) return false;
|
if (!entry?.url) return false;
|
||||||
delete lazySessionTabs[tabId];
|
delete lazySessionTabs[tabId];
|
||||||
await chrome.storage.local.set({ lazySessionTabs });
|
await api.storage.local.set({ lazySessionTabs });
|
||||||
await chrome.tabs.update(Number(tabId), { url: entry.url });
|
await api.tabs.update(Number(tabId), { url: entry.url });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,24 +59,24 @@ export class SessionCommands extends CommandGroup {
|
|||||||
const throttle = await getLargeOperationThrottle(sessionTabs.length, gentleMode);
|
const throttle = await getLargeOperationThrottle(sessionTabs.length, gentleMode);
|
||||||
const createBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
const createBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
||||||
const eagerLimit = lazy ? Math.max(0, Number(eagerTabs) || 0) : sessionTabs.length;
|
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 || {};
|
const lazyMap: LazySessionMap = lazySessionTabs || {};
|
||||||
updateJobProgress(__job, { phase: "opening", current: 0, total: sessionTabs.length });
|
updateJobProgress(__job, { phase: "opening", current: 0, total: sessionTabs.length });
|
||||||
|
|
||||||
for (const [idx, entry] of sessionTabs.entries()) {
|
for (const [idx, entry] of sessionTabs.entries()) {
|
||||||
throwIfJobCancelled(__job);
|
throwIfJobCancelled(__job);
|
||||||
const shouldLazy = lazy && idx >= eagerLimit;
|
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 });
|
createdTabs.push({ tabId: tab.id, entry });
|
||||||
if (shouldLazy) {
|
if (shouldLazy) {
|
||||||
lazyMap[String(tab.id)] = { url: entry.url, createdAt: Date.now() };
|
lazyMap[String(tab.id)] = { url: entry.url, createdAt: Date.now() };
|
||||||
} else if (discardBackgroundTabs && !entry.pinned && chrome.tabs.discard) {
|
} else if (discardBackgroundTabs && !entry.pinned && api.tabs.discard) {
|
||||||
try { await chrome.tabs.discard(tab.id); } catch (_) {}
|
try { await api.tabs.discard(tab.id); } catch (_) {}
|
||||||
}
|
}
|
||||||
updateJobProgress(__job, { phase: shouldLazy ? "creating lazy placeholders" : "opening", current: createdTabs.length, total: sessionTabs.length });
|
updateJobProgress(__job, { phase: shouldLazy ? "creating lazy placeholders" : "opening", current: createdTabs.length, total: sessionTabs.length });
|
||||||
await yieldForLargeOperation(createdTabs.length, createBatchSize, Math.max(50, throttle.pauseMs));
|
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();
|
const groups = new Map();
|
||||||
for (const { tabId, entry } of createdTabs) {
|
for (const { tabId, entry } of createdTabs) {
|
||||||
@@ -119,7 +120,7 @@ export class SessionCommands extends CommandGroup {
|
|||||||
const sessions = await getSessions();
|
const sessions = await getSessions();
|
||||||
if (!(name in sessions)) throw new Error(`Session '${name}' not found`);
|
if (!(name in sessions)) throw new Error(`Session '${name}' not found`);
|
||||||
delete sessions[name];
|
delete sessions[name];
|
||||||
await chrome.storage.local.set({ sessions });
|
await api.storage.local.set({ sessions });
|
||||||
return { name };
|
return { name };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,16 +155,18 @@ export class SessionCommands extends CommandGroup {
|
|||||||
if (!overwrite && sessions[name]) throw new Error(`Session '${name}' already exists`);
|
if (!overwrite && sessions[name]) throw new Error(`Session '${name}' already exists`);
|
||||||
const stored = session as object as StoredSession;
|
const stored = session as object as StoredSession;
|
||||||
sessions[name] = { ...stored, savedAt: Number(stored.savedAt) || Date.now() };
|
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 };
|
return { name, tabs: getSessionTabs(sessions[name]).length };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clientsList() {
|
private async clientsList() {
|
||||||
const manifest = chrome.runtime.getManifest();
|
const manifest = api.runtime.getManifest();
|
||||||
const alias = await getProfileAlias();
|
const alias = await getProfileAlias();
|
||||||
|
const browserInfo = api.runtime.getBrowserInfo ? await api.runtime.getBrowserInfo() : null;
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
return [{
|
return [{
|
||||||
name: "Chrome",
|
name: browserInfo?.name || (userAgent.includes("Firefox/") ? "Firefox" : "Chrome"),
|
||||||
version: navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1] || "unknown",
|
version: browserInfo?.version || userAgent.match(/(?:Chrome|Firefox)\/([\d.]+)/)?.[1] || "unknown",
|
||||||
platform: navigator.platform,
|
platform: navigator.platform,
|
||||||
extensionVersion: manifest.version,
|
extensionVersion: manifest.version,
|
||||||
profile: alias,
|
profile: alias,
|
||||||
@@ -171,7 +174,7 @@ export class SessionCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async clientsRenameProfile({ alias }: ClientsRenameProfileArgs) {
|
private async clientsRenameProfile({ alias }: ClientsRenameProfileArgs) {
|
||||||
await chrome.storage.local.set({ profileAlias: alias });
|
await api.storage.local.set({ profileAlias: alias });
|
||||||
return { alias };
|
return { alias };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
import { fetchTabHtml, getActiveTab, getAliases, isBrowserErrorUrl, tabInfo } from '../core';
|
import { fetchTabHtml, getActiveTab, getAliases, isBrowserErrorUrl, tabInfo } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
@@ -17,7 +18,7 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async tabsList() {
|
private async tabsList() {
|
||||||
const windows = await chrome.windows.getAll({ populate: true });
|
const windows = await api.windows.getAll({ populate: true });
|
||||||
const aliases = await getAliases();
|
const aliases = await getAliases();
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
for (const w of windows) {
|
for (const w of windows) {
|
||||||
@@ -34,7 +35,7 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tabsActiveInWindow({ windowId }: TabsActiveInWindowArgs) {
|
private async tabsActiveInWindow({ windowId }: TabsActiveInWindowArgs) {
|
||||||
const activeTabs = await chrome.tabs.query({ windowId, active: true });
|
const activeTabs = await api.tabs.query({ windowId, active: true });
|
||||||
const tab = activeTabs[0];
|
const tab = activeTabs[0];
|
||||||
if (!tab) {
|
if (!tab) {
|
||||||
throw new Error(`No active tab found for window ${windowId}`);
|
throw new Error(`No active tab found for window ${windowId}`);
|
||||||
@@ -43,24 +44,24 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tabsStatus({ tabId }: TabIdArgs) {
|
private async tabsStatus({ tabId }: TabIdArgs) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||||
return tabInfo(tab);
|
return tabInfo(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsCount({ pattern }: TabsPatternArgs) {
|
private async tabsCount({ pattern }: TabsPatternArgs) {
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
||||||
return all.length;
|
return all.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsQuery({ search }: TabsQueryArgs) {
|
private async tabsQuery({ search }: TabsQueryArgs) {
|
||||||
const q = search.toLowerCase();
|
const q = search.toLowerCase();
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
return all.filter(t =>
|
return all.filter(t =>
|
||||||
(t.url && t.url.toLowerCase().includes(q)) ||
|
(t.url && t.url.toLowerCase().includes(q)) ||
|
||||||
(t.title && t.title.toLowerCase().includes(q))
|
(t.title && t.title.toLowerCase().includes(q))
|
||||||
@@ -68,7 +69,7 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tabsWatchUrl({ pattern, timeout = 30000, tabId }: TabsWatchUrlArgs = {}) {
|
private async tabsWatchUrl({ pattern, timeout = 30000, tabId }: TabsWatchUrlArgs = {}) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||||
const deadline = Date.now() + timeout;
|
const deadline = Date.now() + timeout;
|
||||||
const regex = new RegExp(pattern);
|
const regex = new RegExp(pattern);
|
||||||
let lastUrl = tab.url || tab.pendingUrl || "";
|
let lastUrl = tab.url || tab.pendingUrl || "";
|
||||||
@@ -81,7 +82,7 @@ export class TabsQueryCommands extends CommandGroup {
|
|||||||
if (matches(lastUrl)) return tabInfo(tab);
|
if (matches(lastUrl)) return tabInfo(tab);
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const t = await chrome.tabs.get(tab.id);
|
const t = await api.tabs.get(tab.id);
|
||||||
lastUrl = t.url || t.pendingUrl || "";
|
lastUrl = t.url || t.pendingUrl || "";
|
||||||
lastStatus = t.status || "unknown";
|
lastStatus = t.status || "unknown";
|
||||||
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { TabMoveProperties, BrowserWindow } from '../types';
|
||||||
import { asTabIds, getActiveTab, getLargeOperationThrottle, groupTabs, processInBatches, resolveTabForDirectAction, runLargeOperation, throwIfJobCancelled, updateJobProgress, yieldForLargeOperation } from '../core';
|
import { asTabIds, getActiveTab, getLargeOperationThrottle, groupTabs, processInBatches, resolveTabForDirectAction, runLargeOperation, throwIfJobCancelled, updateJobProgress, yieldForLargeOperation } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
@@ -23,7 +25,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
return runLargeOperation("tabs.close", async () => {
|
return runLargeOperation("tabs.close", async () => {
|
||||||
let toClose: number[] = [];
|
let toClose: number[] = [];
|
||||||
if (duplicates) {
|
if (duplicates) {
|
||||||
const windows = await chrome.windows.getAll({ populate: true });
|
const windows = await api.windows.getAll({ populate: true });
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
for (const w of windows) {
|
for (const w of windows) {
|
||||||
for (const t of w.tabs || []) {
|
for (const t of w.tabs || []) {
|
||||||
@@ -34,7 +36,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (inactive) {
|
} else if (inactive) {
|
||||||
const all = await chrome.tabs.query({});
|
const all = await api.tabs.query({});
|
||||||
toClose = all.filter(t => !t.active).map(t => t.id);
|
toClose = all.filter(t => !t.active).map(t => t.id);
|
||||||
} else if (tabIds?.length) {
|
} else if (tabIds?.length) {
|
||||||
toClose = tabIds.filter(id => id != null);
|
toClose = tabIds.filter(id => id != null);
|
||||||
@@ -42,17 +44,17 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
toClose = [tabId];
|
toClose = [tabId];
|
||||||
}
|
}
|
||||||
const throttle = await getLargeOperationThrottle(toClose.length, gentleMode);
|
const throttle = await getLargeOperationThrottle(toClose.length, gentleMode);
|
||||||
await processInBatches(toClose, throttle, batch => chrome.tabs.remove(batch), { job: __job, phase: "closing tabs" });
|
await processInBatches(toClose, throttle, batch => api.tabs.remove(batch), { job: __job, phase: "closing tabs" });
|
||||||
return { closed: toClose.length, gentle: throttle.gentle, audible: throttle.audible };
|
return { closed: toClose.length, gentle: throttle.gentle, audible: throttle.audible };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsMove({ tabId, groupId, windowId, index, forward, backward }: TabsMoveArgs) {
|
private async tabsMove({ tabId, groupId, windowId, index, forward, backward }: TabsMoveArgs) {
|
||||||
const moveProps: Partial<chrome.tabs.MoveProperties> = {};
|
const moveProps: Partial<TabMoveProperties> = {};
|
||||||
if (windowId != null) moveProps.windowId = windowId;
|
if (windowId != null) moveProps.windowId = windowId;
|
||||||
|
|
||||||
if (forward || backward) {
|
if (forward || backward) {
|
||||||
const tab = await chrome.tabs.get(tabId);
|
const tab = await api.tabs.get(tabId);
|
||||||
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
|
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
|
||||||
else moveProps.index = Math.max(0, tab.index - 1);
|
else moveProps.index = Math.max(0, tab.index - 1);
|
||||||
} else if (index != null) {
|
} else if (index != null) {
|
||||||
@@ -62,7 +64,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `index` is always assigned by one of the branches above before this call.
|
// `index` is always assigned by one of the branches above before this call.
|
||||||
await chrome.tabs.move(tabId, moveProps as chrome.tabs.MoveProperties);
|
await api.tabs.move(tabId, moveProps as TabMoveProperties);
|
||||||
if (groupId != null) {
|
if (groupId != null) {
|
||||||
await groupTabs({ tabIds: asTabIds([tabId]), groupId });
|
await groupTabs({ tabIds: asTabIds([tabId]), groupId });
|
||||||
}
|
}
|
||||||
@@ -70,7 +72,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tabsActive({ tabId }: TabIdArgs) {
|
private async tabsActive({ tabId }: TabIdArgs) {
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
await api.tabs.update(tabId, { active: true });
|
||||||
return { tabId };
|
return { tabId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
|
|
||||||
private async tabsSort({ by, gentleMode, __job }: TabsSortArgs = {}) {
|
private async tabsSort({ by, gentleMode, __job }: TabsSortArgs = {}) {
|
||||||
return runLargeOperation("tabs.sort", async () => {
|
return runLargeOperation("tabs.sort", async () => {
|
||||||
const windows = await chrome.windows.getAll({ populate: true });
|
const windows = await api.windows.getAll({ populate: true });
|
||||||
let moved = 0;
|
let moved = 0;
|
||||||
const totalTabs = windows.reduce((sum, w) => sum + (w.tabs?.length || 0), 0);
|
const totalTabs = windows.reduce((sum, w) => sum + (w.tabs?.length || 0), 0);
|
||||||
updateJobProgress(__job, { phase: "sorting tabs", current: 0, total: totalTabs });
|
updateJobProgress(__job, { phase: "sorting tabs", current: 0, total: totalTabs });
|
||||||
@@ -98,7 +100,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
const moveBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
const moveBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
||||||
for (let i = 0; i < sorted.length; i++) {
|
for (let i = 0; i < sorted.length; i++) {
|
||||||
throwIfJobCancelled(__job);
|
throwIfJobCancelled(__job);
|
||||||
await chrome.tabs.move(sorted[i].id, { index: i });
|
await api.tabs.move(sorted[i].id, { index: i });
|
||||||
moved++;
|
moved++;
|
||||||
updateJobProgress(__job, { phase: "sorting tabs", current: moved, total: totalTabs });
|
updateJobProgress(__job, { phase: "sorting tabs", current: moved, total: totalTabs });
|
||||||
await yieldForLargeOperation(moved, moveBatchSize, throttle.pauseMs);
|
await yieldForLargeOperation(moved, moveBatchSize, throttle.pauseMs);
|
||||||
@@ -108,13 +110,13 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private windowHasAudibleTabs(window: chrome.windows.Window): boolean {
|
private windowHasAudibleTabs(window: BrowserWindow): boolean {
|
||||||
return Boolean(window.tabs?.some(tab => tab.audible && !tab.mutedInfo?.muted));
|
return Boolean(window.tabs?.some(tab => tab.audible && !tab.mutedInfo?.muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsMergeWindows({ gentleMode, __job }: TabsMergeWindowsArgs = {}) {
|
private async tabsMergeWindows({ gentleMode, __job }: TabsMergeWindowsArgs = {}) {
|
||||||
return runLargeOperation("tabs.merge_windows", async () => {
|
return runLargeOperation("tabs.merge_windows", async () => {
|
||||||
const all = await chrome.windows.getAll({ populate: true });
|
const all = await api.windows.getAll({ populate: true });
|
||||||
const movableWindows = all.filter(w => !this.windowHasAudibleTabs(w));
|
const movableWindows = all.filter(w => !this.windowHasAudibleTabs(w));
|
||||||
const target = movableWindows.find(w => w.focused) || movableWindows[0];
|
const target = movableWindows.find(w => w.focused) || movableWindows[0];
|
||||||
if (!target) return { moved: 0, skippedAudibleWindows: all.length };
|
if (!target) return { moved: 0, skippedAudibleWindows: all.length };
|
||||||
@@ -127,7 +129,7 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
const ids = w.tabs.map(t => t.id);
|
const ids = w.tabs.map(t => t.id);
|
||||||
const throttle = await getLargeOperationThrottle(ids.length, gentleMode);
|
const throttle = await getLargeOperationThrottle(ids.length, gentleMode);
|
||||||
moved = await processInBatches(ids, throttle,
|
moved = await processInBatches(ids, throttle,
|
||||||
batch => chrome.tabs.move(batch, { windowId: target.id, index: -1 }),
|
batch => api.tabs.move(batch, { windowId: target.id, index: -1 }),
|
||||||
{ job: __job, phase: "merging windows", total: totalTabs, baseCurrent: moved });
|
{ job: __job, phase: "merging windows", total: totalTabs, baseCurrent: moved });
|
||||||
}
|
}
|
||||||
return { moved, skippedAudibleWindows: all.length - movableWindows.length };
|
return { moved, skippedAudibleWindows: all.length - movableWindows.length };
|
||||||
@@ -135,42 +137,42 @@ export class TabsMutationCommands extends CommandGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tabsPin({ tabId }: TabIdArgs) {
|
private async tabsPin({ tabId }: TabIdArgs) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||||
await chrome.tabs.update(tab.id, { pinned: true });
|
await api.tabs.update(tab.id, { pinned: true });
|
||||||
return { tabId: tab.id, pinned: true };
|
return { tabId: tab.id, pinned: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsUnpin({ tabId }: TabIdArgs) {
|
private async tabsUnpin({ tabId }: TabIdArgs) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||||
await chrome.tabs.update(tab.id, { pinned: false });
|
await api.tabs.update(tab.id, { pinned: false });
|
||||||
return { tabId: tab.id, pinned: false };
|
return { tabId: tab.id, pinned: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsScreenshot({ tabId, format = "png", quality }: TabsScreenshotArgs = {}) {
|
private async tabsScreenshot({ tabId, format = "png", quality }: TabsScreenshotArgs = {}) {
|
||||||
let windowId: number | undefined;
|
let windowId: number | undefined;
|
||||||
if (tabId) {
|
if (tabId) {
|
||||||
const tab = await chrome.tabs.get(tabId);
|
const tab = await api.tabs.get(tabId);
|
||||||
await chrome.tabs.update(tabId, { active: true });
|
await api.tabs.update(tabId, { active: true });
|
||||||
windowId = tab.windowId;
|
windowId = tab.windowId;
|
||||||
} else {
|
} else {
|
||||||
const tab = await getActiveTab();
|
const tab = await getActiveTab();
|
||||||
windowId = tab.windowId;
|
windowId = tab.windowId;
|
||||||
}
|
}
|
||||||
const opts: chrome.extensionTypes.ImageDetails = { format: format as chrome.extensionTypes.ImageFormat };
|
const opts: browser.extensionTypes.ImageDetails = { format: format as browser.extensionTypes.ImageFormat };
|
||||||
if (format === "jpeg" && quality != null) opts.quality = quality;
|
if (format === "jpeg" && quality != null) opts.quality = quality;
|
||||||
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, opts);
|
const dataUrl = await api.tabs.captureVisibleTab(windowId, opts);
|
||||||
return { dataUrl, format };
|
return { dataUrl, format };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsMute({ tabId }: TabIdArgs) {
|
private async tabsMute({ tabId }: TabIdArgs) {
|
||||||
const tab = await resolveTabForDirectAction(tabId, "mute");
|
const tab = await resolveTabForDirectAction(tabId, "mute");
|
||||||
await chrome.tabs.update(tab.id, { muted: true });
|
await api.tabs.update(tab.id, { muted: true });
|
||||||
return { tabId: tab.id, muted: true };
|
return { tabId: tab.id, muted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tabsUnmute({ tabId }: TabIdArgs) {
|
private async tabsUnmute({ tabId }: TabIdArgs) {
|
||||||
const tab = await resolveTabForDirectAction(tabId, "unmute");
|
const tab = await resolveTabForDirectAction(tabId, "unmute");
|
||||||
await chrome.tabs.update(tab.id, { muted: false });
|
await api.tabs.update(tab.id, { muted: false });
|
||||||
return { tabId: tab.id, muted: false };
|
return { tabId: tab.id, muted: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { WindowCreateData } from '../types';
|
||||||
import { getAliases } from '../core';
|
import { getAliases } from '../core';
|
||||||
import { CommandGroup } from '../classes/CommandGroup';
|
import { CommandGroup } from '../classes/CommandGroup';
|
||||||
import type { CommandEntry } from '../classes/CommandGroup';
|
import type { CommandEntry } from '../classes/CommandGroup';
|
||||||
@@ -13,7 +15,7 @@ export class WindowsCommands extends CommandGroup {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private async windowsList() {
|
private async windowsList() {
|
||||||
const windows = await chrome.windows.getAll({ populate: true });
|
const windows = await api.windows.getAll({ populate: true });
|
||||||
const aliases = await getAliases();
|
const aliases = await getAliases();
|
||||||
return windows.map(w => ({
|
return windows.map(w => ({
|
||||||
id: w.id,
|
id: w.id,
|
||||||
@@ -27,19 +29,19 @@ export class WindowsCommands extends CommandGroup {
|
|||||||
private async windowsRename({ windowId, name }: WindowsRenameArgs) {
|
private async windowsRename({ windowId, name }: WindowsRenameArgs) {
|
||||||
const aliases = await getAliases();
|
const aliases = await getAliases();
|
||||||
aliases[windowId] = name;
|
aliases[windowId] = name;
|
||||||
await chrome.storage.local.set({ windowAliases: aliases });
|
await api.storage.local.set({ windowAliases: aliases });
|
||||||
return { windowId, name };
|
return { windowId, name };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async windowsClose({ windowId }: WindowsCloseArgs) {
|
private async windowsClose({ windowId }: WindowsCloseArgs) {
|
||||||
await chrome.windows.remove(windowId);
|
await api.windows.remove(windowId);
|
||||||
return { windowId };
|
return { windowId };
|
||||||
}
|
}
|
||||||
|
|
||||||
private async windowsOpen({ url }: WindowsOpenArgs) {
|
private async windowsOpen({ url }: WindowsOpenArgs) {
|
||||||
const createData: chrome.windows.CreateData = { focused: true };
|
const createData: WindowCreateData = { focused: true };
|
||||||
if (url) createData.url = url;
|
if (url) createData.url = url;
|
||||||
const w = await chrome.windows.create(createData);
|
const w = await api.windows.create(createData);
|
||||||
return { id: w.id };
|
return { id: w.id };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { TabGroupColor } from '../types';
|
||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
// Tab-group resolution and normalization helpers.
|
// Tab-group resolution and normalization helpers.
|
||||||
import { queryTabGroups } from './tab-groups';
|
import { queryTabGroups } from './tab-groups';
|
||||||
|
|
||||||
@@ -10,7 +12,7 @@ export async function resolveGroupId(nameOrId: string | number): Promise<number>
|
|||||||
return match.id;
|
return match.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeGroupColor(color: string | undefined): chrome.tabGroups.Color {
|
export function normalizeGroupColor(color: string | undefined): TabGroupColor {
|
||||||
const allowed = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
|
const allowed = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
|
||||||
return (allowed.has(color as string) ? color : "grey") as chrome.tabGroups.Color;
|
return (allowed.has(color as string) ? color : "grey") as TabGroupColor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
// chrome.scripting.executeScript wrapper with transient-error retry.
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { ScriptInjection, ScriptInjectionResult } from '../types';
|
||||||
|
// api.scripting.executeScript wrapper with transient-error retry.
|
||||||
import { isTransientScriptError } from './errors';
|
import { isTransientScriptError } from './errors';
|
||||||
import { sleep } from './throttle';
|
import { sleep } from './throttle';
|
||||||
import type { Serializable } from '../types';
|
import type { Serializable } from '../types';
|
||||||
|
|
||||||
export async function executeScript<Args extends Serializable[], Result>(
|
export async function executeScript<Args extends Serializable[], Result>(
|
||||||
options: chrome.scripting.ScriptInjection<Args, Result>,
|
options: ScriptInjection<Args>,
|
||||||
retries = 3,
|
retries = 3,
|
||||||
): Promise<chrome.scripting.InjectionResult<chrome.scripting.Awaited<Result>>[]> {
|
): Promise<ScriptInjectionResult<Result>[]> {
|
||||||
for (let i = 0; i < retries; i++) {
|
for (let i = 0; i < retries; i++) {
|
||||||
try {
|
try {
|
||||||
return await chrome.scripting.executeScript(options);
|
return await api.scripting.executeScript(options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (i < retries - 1 && isTransientScriptError(e)) {
|
if (i < retries - 1 && isTransientScriptError(e)) {
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// chrome.storage.local accessors for profile alias, window aliases, and sessions.
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
// api.storage.local accessors for profile alias, window aliases, and sessions.
|
||||||
import type { SessionTab, StoredSession } from '../types';
|
import type { SessionTab, StoredSession } from '../types';
|
||||||
|
|
||||||
export async function getProfileAlias(): Promise<string> {
|
export async function getProfileAlias(): Promise<string> {
|
||||||
const { profileAlias } = await chrome.storage.local.get<{ profileAlias?: string }>("profileAlias");
|
const { profileAlias } = await api.storage.local.get<{ profileAlias?: string }>("profileAlias");
|
||||||
return profileAlias || "default";
|
return profileAlias || "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,11 +21,11 @@ export function getSessionTabs(session: StoredSession | undefined | null): Sessi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getAliases(): Promise<Record<string, string>> {
|
export async function getAliases(): Promise<Record<string, string>> {
|
||||||
const { windowAliases } = await chrome.storage.local.get<{ windowAliases?: Record<string, string> }>("windowAliases");
|
const { windowAliases } = await api.storage.local.get<{ windowAliases?: Record<string, string> }>("windowAliases");
|
||||||
return windowAliases || {};
|
return windowAliases || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessions(): Promise<Record<string, StoredSession>> {
|
export async function getSessions(): Promise<Record<string, StoredSession>> {
|
||||||
const { sessions } = await chrome.storage.local.get<{ sessions?: Record<string, StoredSession> }>("sessions");
|
const { sessions } = await api.storage.local.get<{ sessions?: Record<string, StoredSession> }>("sessions");
|
||||||
return sessions || {};
|
return sessions || {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { TabGroupQueryInfo, TabGroup, TabGroupUpdateProperties, TabGroupMoveProperties, TabGroupOptions, BrowserEvent } from '../types';
|
||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
// Optional tab-group API accessors. Firefox currently does not implement the
|
// Optional tab-group API accessors. Firefox currently does not implement the
|
||||||
// Chromium tabGroups/tabs.group APIs, so keep runtime checks in one place and
|
// Chromium tabGroups/tabs.group APIs, so keep runtime checks in one place and
|
||||||
// use bracket access to avoid Firefox package validation flagging static API
|
// use bracket access to avoid Firefox package validation flagging static API
|
||||||
@@ -5,43 +7,43 @@
|
|||||||
|
|
||||||
const TAB_GROUPS_UNSUPPORTED = "Tab groups are not supported by this browser";
|
const TAB_GROUPS_UNSUPPORTED = "Tab groups are not supported by this browser";
|
||||||
|
|
||||||
function tabGroupsApi(): typeof chrome.tabGroups {
|
function tabGroupsApi(): typeof api.tabGroups {
|
||||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||||
if (!api) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
if (!tabGroups) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||||
return api;
|
return tabGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tabsGroupApi(): typeof chrome.tabs.group {
|
function tabsGroupApi(): typeof api.tabs.group {
|
||||||
const fn = chrome.tabs["group" as keyof typeof chrome.tabs] as typeof chrome.tabs.group | undefined;
|
const fn = api.tabs["group" as keyof typeof api.tabs] as typeof api.tabs.group | undefined;
|
||||||
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||||
return fn.bind(chrome.tabs);
|
return fn.bind(api.tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function tabsUngroupApi(): typeof chrome.tabs.ungroup {
|
function tabsUngroupApi(): typeof api.tabs.ungroup {
|
||||||
const fn = chrome.tabs["ungroup" as keyof typeof chrome.tabs] as typeof chrome.tabs.ungroup | undefined;
|
const fn = api.tabs["ungroup" as keyof typeof api.tabs] as typeof api.tabs.ungroup | undefined;
|
||||||
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
if (!fn) throw new Error(TAB_GROUPS_UNSUPPORTED);
|
||||||
return fn.bind(chrome.tabs);
|
return fn.bind(api.tabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryTabGroups(queryInfo: chrome.tabGroups.QueryInfo = {}): Promise<chrome.tabGroups.TabGroup[]> {
|
export async function queryTabGroups(queryInfo: TabGroupQueryInfo = {}): Promise<TabGroup[]> {
|
||||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||||
if (!api) return [];
|
if (!tabGroups) return [];
|
||||||
return api.query(queryInfo);
|
return tabGroups.query(queryInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTabGroup(groupId: number): Promise<chrome.tabGroups.TabGroup> {
|
export async function getTabGroup(groupId: number): Promise<TabGroup> {
|
||||||
return tabGroupsApi().get(groupId);
|
return tabGroupsApi().get(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateTabGroup(groupId: number, updateProperties: chrome.tabGroups.UpdateProperties): Promise<chrome.tabGroups.TabGroup> {
|
export async function updateTabGroup(groupId: number, updateProperties: TabGroupUpdateProperties): Promise<TabGroup> {
|
||||||
return tabGroupsApi().update(groupId, updateProperties);
|
return tabGroupsApi().update(groupId, updateProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function moveTabGroup(groupId: number, moveProperties: chrome.tabGroups.MoveProperties): Promise<chrome.tabGroups.TabGroup> {
|
export async function moveTabGroup(groupId: number, moveProperties: TabGroupMoveProperties): Promise<TabGroup> {
|
||||||
return tabGroupsApi().move(groupId, moveProperties);
|
return tabGroupsApi().move(groupId, moveProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function groupTabs(createProperties: chrome.tabs.GroupOptions): Promise<number> {
|
export async function groupTabs(createProperties: TabGroupOptions): Promise<number> {
|
||||||
return tabsGroupApi()(createProperties);
|
return tabsGroupApi()(createProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +51,7 @@ export async function ungroupTabs(tabIds: [number, ...number[]]): Promise<void>
|
|||||||
return tabsUngroupApi()(tabIds);
|
return tabsUngroupApi()(tabIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tabGroupsOnUpdated(): chrome.events.Event<(group: chrome.tabGroups.TabGroup) => void> | undefined {
|
export function tabGroupsOnUpdated(): BrowserEvent<(group: TabGroup) => void> | undefined {
|
||||||
const api = chrome["tabGroups" as keyof typeof chrome] as typeof chrome.tabGroups | undefined;
|
const tabGroups = api["tabGroups" as keyof typeof api] as typeof api.tabGroups | undefined;
|
||||||
return api?.onUpdated;
|
return tabGroups?.onUpdated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
|
import type { Tab } from '../types';
|
||||||
// Tab-related shared helpers: info shaping, scriptable-url checks, active-tab
|
// Tab-related shared helpers: info shaping, scriptable-url checks, active-tab
|
||||||
// resolution, and HTML fetching.
|
// resolution, and HTML fetching.
|
||||||
import { isBrowserErrorUrl, isErrorPageScriptError } from './errors';
|
import { isBrowserErrorUrl, isErrorPageScriptError } from './errors';
|
||||||
@@ -5,8 +7,8 @@ import { executeScript } from './scripting';
|
|||||||
import type { TabBlock } from '../types';
|
import type { TabBlock } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Narrow a plain id array to the non-empty-tuple shape that chrome.tabs.group /
|
* Narrow a plain id array to the non-empty-tuple shape that api.tabs.group /
|
||||||
* chrome.tabs.ungroup declare. The runtime happily accepts any array (including
|
* api.tabs.ungroup declare. The runtime happily accepts any array (including
|
||||||
* a single element); the published @types/chrome just over-constrain the param
|
* a single element); the published @types/chrome just over-constrain the param
|
||||||
* to `[number, ...number[]]`. Callers guarantee non-emptiness before calling.
|
* to `[number, ...number[]]`. Callers guarantee non-emptiness before calling.
|
||||||
*/
|
*/
|
||||||
@@ -14,7 +16,7 @@ export function asTabIds(ids: number[]): [number, ...number[]] {
|
|||||||
return ids as [number, ...number[]];
|
return ids as [number, ...number[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tabInfo(t: chrome.tabs.Tab) {
|
export function tabInfo(t: Tab) {
|
||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
windowId: t.windowId,
|
windowId: t.windowId,
|
||||||
@@ -36,16 +38,16 @@ export function isScriptableUrl(url: string | undefined | null): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getActiveTab() {
|
export async function getActiveTab() {
|
||||||
const activeTabs = await chrome.tabs.query({ active: true });
|
const activeTabs = await api.tabs.query({ active: true });
|
||||||
if (!activeTabs.length) throw new Error("No active tab found");
|
if (!activeTabs.length) throw new Error("No active tab found");
|
||||||
|
|
||||||
const windows = await chrome.windows.getAll({ populate: false });
|
const windows = await api.windows.getAll({ populate: false });
|
||||||
const focusedWindowIds = new Set(windows.filter(window => window.focused).map(window => window.id));
|
const focusedWindowIds = new Set(windows.filter(window => window.focused).map(window => window.id));
|
||||||
|
|
||||||
const chooseTab = (predicate: (tab: chrome.tabs.Tab) => boolean) => activeTabs.find(predicate);
|
const chooseTab = (predicate: (tab: Tab) => boolean) => activeTabs.find(predicate);
|
||||||
const byFocusAndScriptable = (tab: chrome.tabs.Tab) => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || "");
|
const byFocusAndScriptable = (tab: Tab) => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||||
const byScriptable = (tab: chrome.tabs.Tab) => isScriptableUrl(tab.url || tab.pendingUrl || "");
|
const byScriptable = (tab: Tab) => isScriptableUrl(tab.url || tab.pendingUrl || "");
|
||||||
const byFocus = (tab: chrome.tabs.Tab) => focusedWindowIds.has(tab.windowId);
|
const byFocus = (tab: Tab) => focusedWindowIds.has(tab.windowId);
|
||||||
|
|
||||||
return chooseTab(byFocusAndScriptable)
|
return chooseTab(byFocusAndScriptable)
|
||||||
|| chooseTab(byScriptable)
|
|| chooseTab(byScriptable)
|
||||||
@@ -54,8 +56,8 @@ export async function getActiveTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve the target tab (explicit id or the active tab) and its current URL. */
|
/** Resolve the target tab (explicit id or the active tab) and its current URL. */
|
||||||
export async function resolveTabUrl(tabId?: number | null): Promise<{ tab: chrome.tabs.Tab; url: string }> {
|
export async function resolveTabUrl(tabId?: number | null): Promise<{ tab: Tab; url: string }> {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||||
return { tab, url: tab.url || tab.pendingUrl || "" };
|
return { tab, url: tab.url || tab.pendingUrl || "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +72,11 @@ export function assertScriptableUrl(url: string, action: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveTabForDirectAction(tabId: number | undefined | null, actionName: string): Promise<chrome.tabs.Tab> {
|
export async function resolveTabForDirectAction(tabId: number | undefined | null, actionName: string): Promise<Tab> {
|
||||||
if (tabId != null) {
|
if (tabId != null) {
|
||||||
return chrome.tabs.get(tabId);
|
return api.tabs.get(tabId);
|
||||||
}
|
}
|
||||||
const allTabs = await chrome.tabs.query({});
|
const allTabs = await api.tabs.query({});
|
||||||
if (allTabs.length !== 1) {
|
if (allTabs.length !== 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open`
|
`Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open`
|
||||||
@@ -83,7 +85,7 @@ export async function resolveTabForDirectAction(tabId: number | undefined | null
|
|||||||
return allTabs[0];
|
return allTabs[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildTabBlocks(tabs: chrome.tabs.Tab[]): TabBlock[] {
|
export function buildTabBlocks(tabs: Tab[]): TabBlock[] {
|
||||||
const blocks: TabBlock[] = [];
|
const blocks: TabBlock[] = [];
|
||||||
for (const tab of tabs) {
|
for (const tab of tabs) {
|
||||||
const normalizedGroupId = tab.groupId >= 0 ? tab.groupId : null;
|
const normalizedGroupId = tab.groupId >= 0 ? tab.groupId : null;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { webExtApi as api } from '../browser-api';
|
||||||
// Large-operation throttling, performance profile, and job-progress helpers.
|
// Large-operation throttling, performance profile, and job-progress helpers.
|
||||||
import type { Job, JobProgressUpdate } from '../types';
|
import type { Job, JobProgressUpdate } from '../types';
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ function debugLargeOperation(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function hasAudibleTabs() {
|
export async function hasAudibleTabs() {
|
||||||
const audibleTabs = await chrome.tabs.query({ audible: true });
|
const audibleTabs = await api.tabs.query({ audible: true });
|
||||||
return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted));
|
return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,14 +37,14 @@ export async function runLargeOperation<T>(name: string, fn: () => Promise<T>):
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPerformanceProfile() {
|
export async function getPerformanceProfile() {
|
||||||
const { performanceProfile } = await chrome.storage.local.get<{ performanceProfile?: string }>("performanceProfile");
|
const { performanceProfile } = await api.storage.local.get<{ performanceProfile?: string }>("performanceProfile");
|
||||||
return performanceProfile || "auto";
|
return performanceProfile || "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setPerformanceProfile(profile: string) {
|
export async function setPerformanceProfile(profile: string) {
|
||||||
const allowed = new Set(["auto", "normal", "gentle", "ultra"]);
|
const allowed = new Set(["auto", "normal", "gentle", "ultra"]);
|
||||||
const performanceProfile = allowed.has(profile) ? profile : "auto";
|
const performanceProfile = allowed.has(profile) ? profile : "auto";
|
||||||
await chrome.storage.local.set({ performanceProfile });
|
await api.storage.local.set({ performanceProfile });
|
||||||
return { performanceProfile };
|
return { performanceProfile };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* the native connection.
|
* the native connection.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { webExtApi as api } from './browser-api';
|
||||||
import { JobManager } from './classes/JobManager';
|
import { JobManager } from './classes/JobManager';
|
||||||
import { assembleRegistry } from './classes/CommandRegistry';
|
import { assembleRegistry } from './classes/CommandRegistry';
|
||||||
import { NativeConnection } from './classes/NativeConnection';
|
import { NativeConnection } from './classes/NativeConnection';
|
||||||
@@ -15,7 +16,7 @@ const jobs = new JobManager();
|
|||||||
const ctx: CommandContext = { jobs };
|
const ctx: CommandContext = { jobs };
|
||||||
const { registry, session } = assembleRegistry(ctx);
|
const { registry, session } = assembleRegistry(ctx);
|
||||||
|
|
||||||
chrome.tabs.onActivated.addListener(async ({ tabId }) => {
|
api.tabs.onActivated.addListener(async ({ tabId }) => {
|
||||||
await session.activateLazyTab(tabId);
|
await session.activateLazyTab(tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export interface DomEvalArgs { code?: string; tabId?: number; }
|
|||||||
export interface DomWaitForArgs { selector?: string; timeout?: number; visible?: boolean; hidden?: boolean; tabId?: number; }
|
export interface DomWaitForArgs { selector?: string; timeout?: number; visible?: boolean; hidden?: boolean; tabId?: number; }
|
||||||
export interface DomPollArgs { selector?: string; pattern?: string; attr?: string; timeout?: number; interval?: number; tabId?: number; }
|
export interface DomPollArgs { selector?: string; pattern?: string; attr?: string; timeout?: number; interval?: number; tabId?: number; }
|
||||||
|
|
||||||
/** Arguments forwarded to the in-page content functions over chrome.scripting. */
|
/** Arguments forwarded to the in-page content functions over browser.scripting. */
|
||||||
export interface ContentArgs {
|
export interface ContentArgs {
|
||||||
selector?: string;
|
selector?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export * from './json';
|
|||||||
export * from './jobs';
|
export * from './jobs';
|
||||||
export * from './session';
|
export * from './session';
|
||||||
export * from './tabs';
|
export * from './tabs';
|
||||||
|
export * from './webextension';
|
||||||
export * from './messages';
|
export * from './messages';
|
||||||
export * from './command-args';
|
export * from './command-args';
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Serializable } from './json';
|
||||||
|
|
||||||
|
export type RuntimePort = browser.runtime.Port;
|
||||||
|
export type Tab = browser.tabs.Tab & {
|
||||||
|
groupId?: number;
|
||||||
|
pendingUrl?: string;
|
||||||
|
};
|
||||||
|
export type TabUpdateInfo = Parameters<typeof browser.tabs.onUpdated.addListener>[0] extends (tabId: number, changeInfo: infer ChangeInfo, tab: browser.tabs.Tab) => void ? ChangeInfo : { url?: string };
|
||||||
|
export type BrowserWindow = browser.windows.Window & { tabs?: Tab[] };
|
||||||
|
export type WindowCreateData = browser.windows._CreateCreateData;
|
||||||
|
export type TabMoveProperties = browser.tabs._MoveMoveProperties;
|
||||||
|
export type TabGroupOptions = browser.tabs._GroupOptions;
|
||||||
|
export type TabGroup = browser.tabGroups.TabGroup;
|
||||||
|
export type TabGroupColor = browser.tabGroups.Color;
|
||||||
|
export type TabGroupQueryInfo = browser.tabGroups._QueryInfo;
|
||||||
|
export type TabGroupUpdateProperties = browser.tabGroups._UpdateProperties;
|
||||||
|
export type TabGroupMoveProperties = browser.tabGroups._MoveProperties;
|
||||||
|
export type BrowserEvent<TCallback extends (...args: never[]) => void> = {
|
||||||
|
addListener(cb: TCallback): void;
|
||||||
|
removeListener(cb: TCallback): void;
|
||||||
|
hasListener(cb: TCallback): boolean;
|
||||||
|
};
|
||||||
|
export type ScriptInjection<Args extends Serializable[]> = browser.scripting.ScriptInjection<Args>;
|
||||||
|
export type ScriptInjectionResult<Result> = browser.scripting.InjectionResult & { result?: Awaited<Result> };
|
||||||
|
export type StorageLocal = Omit<typeof browser.storage.local, "get"> & {
|
||||||
|
get<T extends object = { [key: string]: Serializable }>(keys?: string | string[] | object | null): Promise<T>;
|
||||||
|
};
|
||||||
|
export type WebExtensionApi = Omit<typeof browser, "tabs" | "windows" | "storage"> & {
|
||||||
|
tabs: Omit<typeof browser.tabs, "query" | "get" | "create" | "update" | "move"> & {
|
||||||
|
query(queryInfo: browser.tabs._QueryQueryInfo): Promise<Tab[]>;
|
||||||
|
get(tabId: number): Promise<Tab>;
|
||||||
|
create(createProperties: browser.tabs._CreateCreateProperties): Promise<Tab>;
|
||||||
|
update(tabId: number, updateProperties: browser.tabs._UpdateUpdateProperties): Promise<Tab>;
|
||||||
|
move(tabIds: number | number[], moveProperties: TabMoveProperties): Promise<Tab | Tab[]>;
|
||||||
|
};
|
||||||
|
windows: Omit<typeof browser.windows, "getAll" | "create"> & {
|
||||||
|
getAll(getInfo?: browser.windows._GetAllGetInfo): Promise<BrowserWindow[]>;
|
||||||
|
create(createData?: WindowCreateData): Promise<BrowserWindow>;
|
||||||
|
};
|
||||||
|
storage: Omit<typeof browser.storage, "local"> & { local: StorageLocal };
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { webExtApi } from '../src/browser-api';
|
||||||
|
|
||||||
|
test('browser-api uses Firefox browser.* before Chromium chrome.*', () => {
|
||||||
|
const originalChrome = globalThis.chrome;
|
||||||
|
const originalBrowser = globalThis.browser;
|
||||||
|
const firefoxApi = { runtime: { id: 'firefox-api' } };
|
||||||
|
const chromiumApi = { runtime: { id: 'chromium-api' } };
|
||||||
|
|
||||||
|
try {
|
||||||
|
globalThis.chrome = chromiumApi;
|
||||||
|
globalThis.browser = firefoxApi;
|
||||||
|
|
||||||
|
assert.equal(webExtApi.runtime, firefoxApi.runtime);
|
||||||
|
} finally {
|
||||||
|
if (originalChrome === undefined) delete globalThis.chrome;
|
||||||
|
else globalThis.chrome = originalChrome;
|
||||||
|
|
||||||
|
if (originalBrowser === undefined) delete globalThis.browser;
|
||||||
|
else globalThis.browser = originalBrowser;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('browser-api falls back to chrome.* in Chromium', () => {
|
||||||
|
const originalChrome = globalThis.chrome;
|
||||||
|
const originalBrowser = globalThis.browser;
|
||||||
|
const chromiumApi = { runtime: { id: 'chromium-api' } };
|
||||||
|
|
||||||
|
try {
|
||||||
|
globalThis.chrome = chromiumApi;
|
||||||
|
delete globalThis.browser;
|
||||||
|
|
||||||
|
assert.equal(webExtApi.runtime, chromiumApi.runtime);
|
||||||
|
} finally {
|
||||||
|
if (originalChrome === undefined) delete globalThis.chrome;
|
||||||
|
else globalThis.chrome = originalChrome;
|
||||||
|
|
||||||
|
if (originalBrowser === undefined) delete globalThis.browser;
|
||||||
|
else globalThis.browser = originalBrowser;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { SessionCommands } from '../src/commands/session';
|
||||||
|
import { JobManager } from '../src/classes/JobManager';
|
||||||
|
import { makeChromeMock } from './chrome-mock';
|
||||||
|
|
||||||
|
function makeSessionCommands() {
|
||||||
|
return new SessionCommands({ jobs: new JobManager() });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('clients.list uses Firefox runtime.getBrowserInfo when available', async () => {
|
||||||
|
const originalChrome = globalThis.chrome;
|
||||||
|
const originalBrowser = globalThis.browser;
|
||||||
|
const originalNavigator = globalThis.navigator;
|
||||||
|
const chromeMock = makeChromeMock();
|
||||||
|
|
||||||
|
try {
|
||||||
|
delete globalThis.chrome;
|
||||||
|
globalThis.browser = {
|
||||||
|
...chromeMock,
|
||||||
|
runtime: {
|
||||||
|
getManifest: () => ({ version: '0.15.1' }),
|
||||||
|
getBrowserInfo: async () => ({ name: 'Firefox', vendor: 'Mozilla', version: '149.0', buildID: 'test' }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: { platform: 'test-platform', userAgent: 'Mozilla/5.0 Firefox/149.0' },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clients = await makeSessionCommands().commands['clients.list']({});
|
||||||
|
|
||||||
|
assert.equal(clients[0].name, 'Firefox');
|
||||||
|
assert.equal(clients[0].version, '149.0');
|
||||||
|
assert.equal(clients[0].extensionVersion, '0.15.1');
|
||||||
|
} finally {
|
||||||
|
if (originalChrome === undefined) delete globalThis.chrome;
|
||||||
|
else globalThis.chrome = originalChrome;
|
||||||
|
|
||||||
|
if (originalBrowser === undefined) delete globalThis.browser;
|
||||||
|
else globalThis.browser = originalBrowser;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: originalNavigator,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clients.list falls back to Chromium user-agent when getBrowserInfo is missing', async () => {
|
||||||
|
const originalChrome = globalThis.chrome;
|
||||||
|
const originalBrowser = globalThis.browser;
|
||||||
|
const originalNavigator = globalThis.navigator;
|
||||||
|
const chromeMock = makeChromeMock();
|
||||||
|
|
||||||
|
try {
|
||||||
|
delete globalThis.browser;
|
||||||
|
globalThis.chrome = {
|
||||||
|
...chromeMock,
|
||||||
|
runtime: {
|
||||||
|
getManifest: () => ({ version: '0.15.1' }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: { platform: 'test-platform', userAgent: 'Mozilla/5.0 Chrome/149.0.0.0 Safari/537.36' },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clients = await makeSessionCommands().commands['clients.list']({});
|
||||||
|
|
||||||
|
assert.equal(clients[0].name, 'Chrome');
|
||||||
|
assert.equal(clients[0].version, '149.0.0.0');
|
||||||
|
} finally {
|
||||||
|
if (originalChrome === undefined) delete globalThis.chrome;
|
||||||
|
else globalThis.chrome = originalChrome;
|
||||||
|
|
||||||
|
if (originalBrowser === undefined) delete globalThis.browser;
|
||||||
|
else globalThis.browser = originalBrowser;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: originalNavigator,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { NavigationCommands } from '../src/commands/navigation';
|
||||||
|
import { JobManager } from '../src/classes/JobManager';
|
||||||
|
import { makeChromeMock } from './chrome-mock';
|
||||||
|
|
||||||
|
function makeNavigationCommands() {
|
||||||
|
return new NavigationCommands({ jobs: new JobManager() });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('navigate.open waits until Firefox updates about:blank to the requested URL', async () => {
|
||||||
|
const originalChrome = globalThis.chrome;
|
||||||
|
const originalBrowser = globalThis.browser;
|
||||||
|
const originalNavigator = globalThis.navigator;
|
||||||
|
const firefoxApi = makeChromeMock();
|
||||||
|
const targetUrl = 'https://example.com/?browser-cli-firefox-open-wait=1';
|
||||||
|
let getCalls = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
delete globalThis.chrome;
|
||||||
|
globalThis.browser = {
|
||||||
|
...firefoxApi,
|
||||||
|
runtime: {
|
||||||
|
getManifest: () => ({ version: '0.15.1' }),
|
||||||
|
getBrowserInfo: async () => ({ name: 'Firefox', vendor: 'Mozilla', version: '151.0.2', buildID: 'test' }),
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
...firefoxApi.tabs,
|
||||||
|
create: async () => ({ id: 123, windowId: 1, index: 0, active: true, groupId: -1, url: 'about:blank' }),
|
||||||
|
get: async () => {
|
||||||
|
getCalls += 1;
|
||||||
|
return {
|
||||||
|
id: 123,
|
||||||
|
windowId: 1,
|
||||||
|
index: 0,
|
||||||
|
active: true,
|
||||||
|
groupId: -1,
|
||||||
|
url: getCalls < 2 ? 'about:blank' : targetUrl,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: { platform: 'test-platform', userAgent: 'Mozilla/5.0 Firefox/151.0.2' },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await makeNavigationCommands().commands['navigate.open']({ url: targetUrl, focus: true });
|
||||||
|
|
||||||
|
assert.equal(result.id, 123);
|
||||||
|
assert.equal(result.url, targetUrl);
|
||||||
|
assert.ok(getCalls >= 2);
|
||||||
|
} finally {
|
||||||
|
if (originalChrome === undefined) delete globalThis.chrome;
|
||||||
|
else globalThis.chrome = originalChrome;
|
||||||
|
|
||||||
|
if (originalBrowser === undefined) delete globalThis.browser;
|
||||||
|
else globalThis.browser = originalBrowser;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
|
value: originalNavigator,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Generated
+8
@@ -7,6 +7,7 @@
|
|||||||
"name": "browser-cli-extension-build",
|
"name": "browser-cli-extension-build",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.1.40",
|
"@types/chrome": "^0.1.40",
|
||||||
|
"@types/firefox-webext-browser": "^143.0.0",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
@@ -481,6 +482,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/firefox-webext-browser": {
|
||||||
|
"version": "143.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-143.0.0.tgz",
|
||||||
|
"integrity": "sha512-865dYKMOP0CllFyHmgXV4IQgVL51OSQQCwSoihQ17EwugePKFSAZRc0EI+y7Ly4q7j5KyURlA7LgRpFieO4JOw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/har-format": {
|
"node_modules/@types/har-format": {
|
||||||
"version": "1.2.16",
|
"version": "1.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chrome": "^0.1.40",
|
"@types/chrome": "^0.1.40",
|
||||||
|
"@types/firefox-webext-browser": "^143.0.0",
|
||||||
"esbuild": "^0.28.0",
|
"esbuild": "^0.28.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.15.1"
|
version = "0.15.2"
|
||||||
description = "Control your real running browser from the terminal or Python SDK"
|
description = "Control your real running browser from the terminal or Python SDK"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ def test_install_writes_firefox_allowed_extensions(tmp_path):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
assert "about:debugging#/runtime/this-firefox" in result.output
|
assert "about:debugging#/runtime/this-firefox" in result.output
|
||||||
|
assert "npm run package:extension:firefox" in result.output
|
||||||
|
assert "dist/extension-package-firefo" in result.output
|
||||||
|
assert "x/manifest.json" in result.output
|
||||||
|
assert "Do not select extension/manifest.json" in result.output
|
||||||
assert "Firefox extension ID" in result.output
|
assert "Firefox extension ID" in result.output
|
||||||
|
|
||||||
def test_install_windows_registers_native_host(tmp_path):
|
def test_install_windows_registers_native_host(tmp_path):
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def test_large_extension_operations_yield_between_batches():
|
|||||||
assert "GENTLE_OPERATION_PAUSE_MS" in core
|
assert "GENTLE_OPERATION_PAUSE_MS" in core
|
||||||
assert "itemCount >= 300" in core
|
assert "itemCount >= 300" in core
|
||||||
assert "itemCount >= 100" in core
|
assert "itemCount >= 100" in core
|
||||||
assert "chrome.tabs.query({ audible: true })" in core
|
assert "api.tabs.query({ audible: true })" in core
|
||||||
# The centralized batch loop drives cancellation + progress + throttled yield.
|
# The centralized batch loop drives cancellation + progress + throttled yield.
|
||||||
assert "processInBatches" in core
|
assert "processInBatches" in core
|
||||||
assert "throwIfJobCancelled(progress.job)" in core
|
assert "throwIfJobCancelled(progress.job)" in core
|
||||||
@@ -93,7 +93,7 @@ def test_large_extension_operations_yield_between_batches():
|
|||||||
assert "yieldForLargeOperation(createdTabs.length" in session
|
assert "yieldForLargeOperation(createdTabs.length" in session
|
||||||
assert "getLargeOperationThrottle" in session
|
assert "getLargeOperationThrottle" in session
|
||||||
assert "runLargeOperation(\"session.load\"" in session
|
assert "runLargeOperation(\"session.load\"" in session
|
||||||
assert "chrome.tabs.discard" in session
|
assert "api.tabs.discard" in session
|
||||||
assert "lazyPlaceholderUrl" in session
|
assert "lazyPlaceholderUrl" in session
|
||||||
assert "activateLazyTab" in session
|
assert "activateLazyTab" in session
|
||||||
assert "lazySessionTabs" in session
|
assert "lazySessionTabs" in session
|
||||||
@@ -136,8 +136,8 @@ def test_built_extension_avoids_static_firefox_unsupported_tab_group_api_refs():
|
|||||||
assert "chrome.tabGroups" not in background
|
assert "chrome.tabGroups" not in background
|
||||||
assert "chrome.tabs.group" not in background
|
assert "chrome.tabs.group" not in background
|
||||||
assert "chrome.tabs.ungroup" not in background
|
assert "chrome.tabs.ungroup" not in background
|
||||||
assert 'chrome["tabGroups"' in background
|
assert 'webExtApi["tabGroups"' in background
|
||||||
assert 'chrome.tabs["group"' in background
|
assert 'webExtApi.tabs["group"' in background
|
||||||
|
|
||||||
def test_built_extension_avoids_direct_eval_token_for_firefox_linter():
|
def test_built_extension_avoids_direct_eval_token_for_firefox_linter():
|
||||||
background = read_built_background()
|
background = read_built_background()
|
||||||
@@ -164,9 +164,9 @@ def test_session_autosave_is_debounced_and_non_overlapping():
|
|||||||
assert "autoSaveSignature" in autosave
|
assert "autoSaveSignature" in autosave
|
||||||
# AutoSaveManager binds the handlers as instance fields (this.*), so the
|
# AutoSaveManager binds the handlers as instance fields (this.*), so the
|
||||||
# add/removeListener references stay identity-stable across enable/disable.
|
# add/removeListener references stay identity-stable across enable/disable.
|
||||||
assert "chrome.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler)" in autosave
|
assert "api.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler)" in autosave
|
||||||
assert "chrome.tabs.onCreated.addListener(this.autoSaveHandler)" in autosave
|
assert "api.tabs.onCreated.addListener(this.autoSaveHandler)" in autosave
|
||||||
assert "chrome.tabs.onMoved.addListener(this.autoSaveHandler)" in autosave
|
assert "api.tabs.onMoved.addListener(this.autoSaveHandler)" in autosave
|
||||||
assert "if (!(\"url\" in changeInfo)) return;" in autosave
|
assert "if (!(\"url\" in changeInfo)) return;" in autosave
|
||||||
assert "setTimeout(() => this.runAutoSave(), delayMs)" in autosave
|
assert "setTimeout(() => this.runAutoSave(), delayMs)" in autosave
|
||||||
assert "clearTimeout(this.autoSaveTimer)" in autosave
|
assert "clearTimeout(this.autoSaveTimer)" in autosave
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"lib": ["ES2022", "DOM"],
|
"lib": ["ES2022", "DOM"],
|
||||||
"types": ["chrome"],
|
"types": ["chrome", "firefox-webext-browser"],
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|||||||
@@ -465,7 +465,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "real-browser-cli"
|
name = "real-browser-cli"
|
||||||
version = "0.15.1"
|
version = "0.15.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user