aboutsummaryrefslogtreecommitdiff
path: root/extensions/chrome-profile-bridge/browser-extension/service_worker.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/chrome-profile-bridge/browser-extension/service_worker.js')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js144
1 files changed, 80 insertions, 64 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index dd0567a..fd6c452 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -745,7 +745,7 @@ async function domClickFallback(tabId, params, cause) {
async function chromeInputClick(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
try {
await attachDebugger(tab.id);
const resolved = await resolveTargetInTab(tab.id, params);
@@ -782,7 +782,7 @@ async function chromeInputClick(params) {
async function chromeInputHover(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
const resolved = await resolveTargetInTab(tab.id, params);
const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
@@ -793,7 +793,7 @@ async function chromeInputHover(params) {
async function chromeInputKey(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
const key = String(params.key || "");
if (!key) throw new Error("chrome.key: missing key");
@@ -831,7 +831,7 @@ async function chromeInputKey(params) {
async function chromeInputType(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
if (params.selector || params.uid) {
// Focus target by clicking it first.
@@ -894,7 +894,7 @@ async function domFillFallback(tabId, params, cause) {
async function chromeInputFill(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
try {
await attachDebugger(tab.id);
if (!(params.selector || params.uid)) throw new Error("chrome.fill: selector or uid required");
@@ -924,7 +924,7 @@ async function chromeInputFill(params) {
async function chromeInputScroll(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
const resolved = (params.selector || params.uid) ? await resolveTargetInTab(tab.id, params) : { x: 100, y: 100, rect: null };
const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
@@ -974,7 +974,7 @@ async function chromeInputScroll(params) {
async function chromeInputTap(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
const resolved = (params.selector || params.uid || (typeof params.x === "number" && typeof params.y === "number"))
? await resolveTargetInTab(tab.id, params)
@@ -990,7 +990,7 @@ async function chromeInputTap(params) {
async function chromeInputDrag(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
const from = await resolveTargetInTab(tab.id, { selector: params.fromSelector ?? null, uid: params.fromUid ?? null, x: params.fromX ?? null, y: params.fromY ?? null });
const to = await resolveTargetInTab(tab.id, { selector: params.toSelector ?? null, uid: params.toUid ?? null, x: params.toX ?? null, y: params.toY ?? null });
@@ -1015,7 +1015,7 @@ async function chromeInputDrag(params) {
async function chromeInputUpload(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
await attachDebugger(tab.id);
if (!(params.selector || params.uid)) throw new Error("chrome.upload: selector or uid required");
const paths = Array.isArray(params.paths) ? params.paths.map(String) : [];
@@ -1208,22 +1208,29 @@ async function dispatch(action, params) {
extensionVersion: chrome.runtime.getManifest().version,
bridgeUrl: BRIDGE_URL,
userAgent: navigator.userAgent,
+ capabilities: { hardBackground: true },
};
case "tab.list": {
const tabs = await chrome.tabs.query({});
return Promise.all(tabs.map(formatTab));
}
+ case "tab.new.background":
+ case "page.screenshot.background":
+ // Older workers reject these action names before touching tabs. Do not replace this with
+ // a capability probe followed by an old action: a reload/profile change can race the probe.
+ return dispatch(action.slice(0, -".background".length), { ...params, background: true, foreground: false });
case "tab.new": {
// Every Pi-opened tab must join a tab group. There is intentionally no opt-out: an ungrouped
// Pi-created tab is easy to lose among user tabs. If grouping fails after creation, close the
// tab best-effort before surfacing the error so tab.new never leaves an ungrouped Pi tab.
const groupTitle = params.groupTitle || "Pi";
const existingGroup = await findGroupRecordByTitle(groupTitle);
- const createParams = { url: params.url || "about:blank", active: true };
+ const createParams = { url: params.url || "about:blank", active: foregroundRequested(params) };
if (existingGroup && typeof existingGroup.windowId === "number") createParams.windowId = existingGroup.windowId;
const tab = await chrome.tabs.create(createParams);
await trackSessionTab(sessionKeyOf(params), tab.id, true);
try {
+ await bringToFront(tab, params);
return await groupTab(tab, groupTitle, params.groupColor);
} catch (error) {
if (typeof tab.id === "number") await chrome.tabs.remove(tab.id).catch(() => {});
@@ -1231,12 +1238,14 @@ async function dispatch(action, params) {
}
}
case "tab.activate": {
+ if (!foregroundRequested(params)) {
+ throw new Error("Tab activation is blocked by background mode. Ask the user to run /chrome background off to allow foreground work.");
+ }
// Management actions never auto-create an automation target (createOwnedTarget:false): with
// no explicit target they act on an owned target if one exists, else error — they must never
// fall back to (or spawn a tab just to touch) the user's active tab.
const tab = await getTabByParams(params, { createOwnedTarget: false });
- await chrome.windows.update(tab.windowId, { focused: true });
- return formatTab(await chrome.tabs.update(tab.id, { active: true }));
+ return formatTab(await bringToFront(tab, params));
}
case "tab.group": {
const tab = await getTabByParams(params, { createOwnedTarget: false });
@@ -1292,7 +1301,7 @@ async function dispatch(action, params) {
// Poll from the service worker via CDP (bypasses CSP). The old approach ran the polling
// loop in-page with new Function() for expression checks, which fails under strict CSP.
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
const timeoutMs = params.timeoutMs || 10000;
const intervalMs = params.intervalMs || 250;
const started = Date.now();
@@ -1316,7 +1325,7 @@ async function dispatch(action, params) {
return executeInTab(params, probePage, []);
case "page.navigate": {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
if (params.initScript) {
// Register a one-shot document_start content script. We register, navigate, wait, then unregister.
await registerInitScript(tab.id, params.initScript);
@@ -1477,7 +1486,7 @@ const HELPER_FUNCS = [
async function executeInTab(params, func, args) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
// Phase 1: define the helpers and the action function as page globals via CDP
// Runtime.evaluate. This bypasses page CSP (no `eval`/`new Function`), which is the
@@ -1544,7 +1553,7 @@ function piEvalStringify(v) {
// pages that ship `script-src 'self'` without `'unsafe-eval'` (which blocks `eval`/`new Function`).
async function evaluateInTab(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
const expression = String(params.expression ?? "");
const stringifySrc = `(${piEvalStringify.toString()})`;
// Wrap the user expression so the result is run through piEvalStringify in-page before it
@@ -1591,7 +1600,7 @@ async function withOptionalSnapshot(params, actionFn) {
// It shares window.__PI_CHROME_STATE__ (same el- uid scheme) with the CDP-injected input helpers.
async function snapshotInTab(params) {
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
const args = [
params.maxElements || 80,
params.containingText ?? null,
@@ -1635,7 +1644,7 @@ async function snapshotInTab(params) {
async function inspectInTab(params) {
if (!params.uid && !params.selector) throw new Error("chrome_inspect requires uid or selector");
const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
+ await bringToFront(tab, params);
const args = [params.uid ?? null, params.selector ?? null, params.scrollIntoView === true];
await executeScriptTimed({
target: { tabId: tab.id, frameIds: [0] },
@@ -1705,9 +1714,15 @@ if (chrome.webNavigation && chrome.webNavigation.onCommitted) {
});
}
-async function bringToFront(tab) {
+function foregroundRequested(params) {
+ // Fail quiet when unspecified, and let background veto even a contradictory foreground flag.
+ return params?.foreground === true && params.background !== true;
+}
+
+async function bringToFront(tab, params) {
+ if (!foregroundRequested(params)) return tab;
await chrome.windows.update(tab.windowId, { focused: true });
- await chrome.tabs.update(tab.id, { active: true });
+ return chrome.tabs.update(tab.id, { active: true });
}
function waitForTabComplete(tabId, timeoutMs) {
@@ -1727,50 +1742,51 @@ function waitForTabComplete(tabId, timeoutMs) {
});
}
-async function takeScreenshot(params) {
- const tab = await getTabByParams(params);
- if (params.foreground) await bringToFront(tab);
- let previousActiveId;
- if (!tab.active) {
- const activeBefore = await chrome.tabs.query({ active: true, windowId: tab.windowId });
- previousActiveId = activeBefore[0]?.id;
- await chrome.tabs.update(tab.id, { active: true });
- }
+async function captureTabScreenshot(tabId, params) {
+ const format = params.format || "png";
try {
- if (params.fullPage) {
- // Tile-stitched full page capture: scroll, capture, paste, repeat.
- const tiles = await executeInTab({ ...params, foreground: false }, captureFullPageTiles, []);
- // captureFullPageTiles only computes scroll positions / metrics; we capture per scroll here
- // (chrome.tabs.captureVisibleTab can't be called from MAIN world).
- const captured = [];
+ await attachDebugger(tabId);
+ const captureParams = { format, fromSurface: true, captureBeyondViewport: false };
+ if (format === "jpeg" && params.quality !== undefined) captureParams.quality = params.quality;
+ const result = await cdp(tabId, "Page.captureScreenshot", captureParams);
+ if (typeof result?.data !== "string" || !result.data) throw new Error("CDP returned no screenshot data");
+ return `data:image/${format};base64,${result.data}`;
+ } catch (error) {
+ // captureVisibleTab requires activation and can race with the user switching tabs. Never
+ // use it as a fallback, even when debugger attachment or background rendering fails.
+ throw new Error(`Chrome screenshot via CDP failed; no tab-activation fallback was attempted. ${error?.message || error}`);
+ }
+}
+
+async function takeScreenshot(params) {
+ const tab = await bringToFront(await getTabByParams(params), params);
+ if (params.fullPage) {
+ // Preserve the existing tile + manifest contract. Every tile captures the same resolved tab,
+ // without activation; selector/title changes during capture must not retarget later tiles.
+ const targetParams = { ...params, targetId: tab.id, foreground: false };
+ const tiles = await executeInTab(targetParams, captureFullPageTiles, []);
+ const captured = [];
+ try {
for (const tile of tiles.tiles) {
- await executeInTab({ ...params, foreground: false }, scrollToY, [tile.scrollY]);
- // Small settle delay; many sites have on-scroll animations / lazy-load.
- await sleep(120);
- const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
- format: params.format || "png",
- quality: params.format === "jpeg" ? params.quality : undefined,
- });
- captured.push({ y: tile.y, dataUrl });
+ await executeInTab(targetParams, scrollToY, [tile.scrollY]);
+ await sleep(120); // Let scroll/lazy-load handlers settle.
+ captured.push({ y: tile.y, dataUrl: await captureTabScreenshot(tab.id, params) });
}
- await executeInTab({ ...params, foreground: false }, scrollToY, [tiles.originalScrollY]);
- return {
- fullPage: true,
- tab: await formatTab(tab),
- dimensions: { width: tiles.width, height: tiles.height, viewportHeight: tiles.viewportHeight, dpr: tiles.dpr },
- tiles: captured,
- };
- }
- const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
- format: params.format || "png",
- quality: params.format === "jpeg" ? params.quality : undefined,
- });
- return { dataUrl, tab: await formatTab(tab) };
- } finally {
- if (previousActiveId !== undefined && previousActiveId !== tab.id) {
- await chrome.tabs.update(previousActiveId, { active: true }).catch(() => undefined);
+ } finally {
+ // A failed tile must not strand the page at a new scroll position. Restore both axes
+ // best-effort, without masking the capture error if the tab/debugger is gone.
+ await executeInTab(targetParams, scrollToY, [tiles.originalScrollY, tiles.originalScrollX]).catch(() => undefined);
}
+ return {
+ fullPage: true,
+ method: "cdp",
+ tab: await formatTab(tab),
+ dimensions: { width: tiles.width, height: tiles.height, viewportHeight: tiles.viewportHeight, dpr: tiles.dpr },
+ tiles: captured,
+ };
}
+ const dataUrl = await captureTabScreenshot(tab.id, params);
+ return { dataUrl, method: "cdp", tab: await formatTab(tab) };
}
// ---------------------------------------------------------------------------
@@ -2197,8 +2213,7 @@ function probePage() {
}
function captureFullPageTiles() {
- // Returns the *plan* for tile capture; the actual chrome.tabs.captureVisibleTab calls happen
- // in the SW. We just report the scroll positions and metrics.
+ // Returns the plan for CDP tile capture in the worker: scroll positions and page metrics.
const html = document.documentElement;
const body = document.body;
const width = Math.max(html.scrollWidth, body ? body.scrollWidth : 0, innerWidth);
@@ -2206,17 +2221,18 @@ function captureFullPageTiles() {
const viewportHeight = innerHeight;
const dpr = window.devicePixelRatio || 1;
const originalScrollY = scrollY;
+ const originalScrollX = scrollX;
const tiles = [];
let y = 0;
while (y < height) {
tiles.push({ y, scrollY: y });
y += viewportHeight;
}
- return { width, height, viewportHeight, dpr, originalScrollY, tiles };
+ return { width, height, viewportHeight, dpr, originalScrollY, originalScrollX, tiles };
}
-function scrollToY(y) {
- window.scrollTo({ top: y, left: 0, behavior: "instant" });
+function scrollToY(y, x = 0) {
+ window.scrollTo({ top: y, left: x, behavior: "instant" });
return { scrollY };
}