aboutsummaryrefslogtreecommitdiff
path: root/extensions/chrome-profile-bridge/browser-extension
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-08 11:58:33 +0000
committerIgor Soarez <igor@soarez.org>2026-11-08 11:58:33 +0000
commite538c3e53e203fabb8a9fed94b6810c345ee6f80 (patch)
tree394fd12fa9bb6829034e606c2bf0781c088af33d /extensions/chrome-profile-bridge/browser-extension
parentff99a4432a3feedfac10d9514ce661a9cb95004c (diff)
navigate: answer a page's Leave-site prompt instead of hanging
A page with a beforeunload handler shows 'Leave site?' when the extension navigates away. Nothing answered it, so chrome_navigate waited out its timeout and the agent never learned why. The worker now hears Page.javascriptDialogOpening and dismisses the dialog (stay/cancel); page.navigate fails at once and says so. leaveIfAsked: true, for that call only, accepts a beforeunload prompt. Tool description and primer tell agents when to use it and to ask the user when unsure.
Diffstat (limited to 'extensions/chrome-profile-bridge/browser-extension')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js61
1 files changed, 57 insertions, 4 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index 1f178e4..c62aff9 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -439,6 +439,8 @@ async function attachDebugger(tabId) {
// Seed pointer in a plausible "just left the address bar" location.
const entry = { detachAt: Date.now() + INPUT_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 }, debuggee: attachedDebuggee || { tabId } };
attachedTabs.set(tabId, entry);
+ // Page events are what tell us a page opened a dialog; without them nobody answers it.
+ await cdpRaw(tabId, "Page.enable", {}).catch(() => undefined);
return entry;
}
@@ -1402,7 +1404,35 @@ function fileChooserTabIdForSource(source) {
return undefined;
}
+// Dialogs a page opens (alert, confirm, prompt, and the "Leave site?" prompt of a beforeunload
+// handler) block the tab until someone answers, and the agent cannot see them. We answer them:
+// "stay"/cancel, unless page.navigate was told leaveIfAsked and it is a beforeunload prompt.
+// page.navigate waits on dialogWaiters to report a dismissed one instead of timing out.
+const dialogPolicy = new Map(); // tabId -> { leave: boolean }, only while page.navigate runs
+const dialogWaiters = new Map(); // tabId -> resolve(dialog), only while page.navigate runs
+
+function describeDialog(dialog) {
+ if (dialog.type === "beforeunload") return 'a "Leave site? Changes you made may not be saved" prompt (the page has a beforeunload handler)';
+ return `a JavaScript ${dialog.type} dialog saying "${dialog.message}"`;
+}
+
+async function handleDialogOpening(tabId, params) {
+ const leave = params?.type === "beforeunload" && dialogPolicy.get(tabId)?.leave === true;
+ const dialog = { type: params?.type, message: params?.message ?? "", url: params?.url, accepted: leave };
+ try {
+ await cdp(tabId, "Page.handleJavaScriptDialog", { accept: leave });
+ } catch (error) {
+ console.warn(`[pi-chrome] could not answer a ${dialog.type} dialog on tab ${tabId}: ${error?.message || error}`);
+ }
+ if (!leave) dialogWaiters.get(tabId)?.(dialog);
+}
+
function handleDebuggerEvent(source, method, params) {
+ if (method === "Page.javascriptDialogOpening") {
+ const dialogTabId = fileChooserTabIdForSource(source);
+ if (dialogTabId !== undefined) void handleDialogOpening(dialogTabId, params);
+ return;
+ }
if (method !== "Page.fileChooserOpened") return;
const tabId = fileChooserTabIdForSource(source);
const entry = tabId === undefined ? undefined : pendingFileChoosers.get(tabId);
@@ -1922,14 +1952,37 @@ async function dispatch(action, params) {
// Register a one-shot document_start content script. We register, navigate, wait, then unregister.
await registerInitScript(tab.id, params.initScript);
}
- const wait = params.waitUntilLoad !== false ? waitForTabComplete(tab.id, params.timeoutMs || 15000) : Promise.resolve(undefined);
- const updated = await chrome.tabs.update(tab.id, { url: params.url });
+ // Attach first: the debugger is how we hear about, and answer, a "Leave site?" prompt.
+ await attachDebugger(tab.id);
+ await cdp(tab.id, "Page.enable", {}).catch(() => undefined);
+ dialogPolicy.set(tab.id, { leave: params.leaveIfAsked === true });
+ const dialogDismissed = new Promise((resolve) => dialogWaiters.set(tab.id, resolve));
+ const waitsForLoad = params.waitUntilLoad !== false;
+ const wait = waitsForLoad ? waitForTabComplete(tab.id, params.timeoutMs || 15000) : Promise.resolve(undefined);
+ wait.catch(() => undefined); // if the dialog wins the race, a later load timeout is not an error
try {
- await wait;
+ const updated = await chrome.tabs.update(tab.id, { url: params.url });
+ // Without waiting for the load, still give a prompt a moment to show up.
+ const outcome = await Promise.race([
+ wait.then(() => ({ loaded: true })),
+ dialogDismissed.then((dialog) => ({ dialog })),
+ ...(waitsForLoad ? [] : [new Promise((resolve) => setTimeout(() => resolve({ loaded: true }), 700))]),
+ ]);
+ if (outcome.dialog) {
+ throw new Error(
+ `Navigation to ${params.url} did not happen: the page at ${outcome.dialog.url || tab.url} opened ${describeDialog(outcome.dialog)}, ` +
+ `and it was dismissed, so the tab stayed where it was. ` +
+ (outcome.dialog.type === "beforeunload"
+ ? "Nothing was lost, but the page may hold unsaved changes. If you know nothing unsaved matters here (you just saved or submitted, or this is a throwaway tab), call chrome_navigate again with leaveIfAsked: true. Otherwise ask the user."
+ : "Look at the page to see what it was asking."),
+ );
+ }
+ return await formatTab(await chrome.tabs.get(updated.id));
} finally {
+ dialogPolicy.delete(tab.id);
+ dialogWaiters.delete(tab.id);
if (params.initScript) await unregisterInitScript(tab.id).catch(() => undefined);
}
- return await formatTab(await chrome.tabs.get(updated.id));
}
case "page.screenshot":
return takeScreenshot(params);