diff options
| author | Igor Soarez <igor@soarez.org> | 2026-11-08 15:19:35 +0000 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-11-08 15:19:35 +0000 |
| commit | 4d353df1a83e36b4e6b2edb0ba64190423e4a7f3 (patch) | |
| tree | 035e1f80347c5664105c265338e80ebfd2193af4 /extensions/chrome-profile-bridge/browser-extension | |
| parent | 9004966da6491b368d12abeeb175256eedb78a2a (diff) | |
overlay: tell the agent about a blocking password-manager prompt; chrome_clear_overlay closes it on request
Another extension's frame (1Password's inline prompt) makes Chrome refuse the
debugger on the tab. The old path sent Escape on its own, which cannot reach
that frame and would close a prompt the user may be using. Now nothing is closed
automatically: the error says what is blocking and tells the agent to ask the
user; chrome_clear_overlay removes the frame, blurs the field, sends Escape and
reports whether the tab is usable again.
Diffstat (limited to 'extensions/chrome-profile-bridge/browser-extension')
| -rw-r--r-- | extensions/chrome-profile-bridge/browser-extension/service_worker.js | 89 |
1 files changed, 60 insertions, 29 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js index 3dcb0f4..4aecf68 100644 --- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js +++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js @@ -453,6 +453,7 @@ async function attachDebugger(tabId) { if (err) err = await retryPageTargetIfExtensionBlocked(err, "attach-page-target-retry3"); if (err) { recordAttachEvent({ kind: "attach-retry2-failed", tabId, message: String(err.message || err), tabUrl: tabSnapshot?.url }); + if (FOREIGN_EXTENSION_BLOCK.test(String(err.message || err))) throw await overlayBlockedError(tabId); const meta = await describeInputTarget(tabId); throw new Error(`Chrome debugger attach failed for tab ${tabId}: ${String(err.message || err)}${targetMetaSuffix(meta)}`); } @@ -690,14 +691,62 @@ function extractForeignExtId(targets) { return null; } -async function dismissOverlayViaEscape(tabId) { - // Esc routes through key dispatcher (target-by-focus), not by mouse coordinates, so it - // works even when a foreign chrome-extension popup is intercepting pointer events. +const FOREIGN_EXTENSION_BLOCK = /Cannot access a chrome-extension:\/\/ URL of different extension/i; + +// Another extension's frame on the page (a password manager's inline prompt, an autofill menu) +// makes Chrome refuse the debugger on that tab. The user may be using that prompt this very +// moment, so we never close it on our own: the error tells the agent to ask, and the agent can +// then call overlay.clear (chrome_clear_overlay). +async function overlayBlockedError(tabId) { + const targets = (await findForeignExtensionTargets()).filter((t) => t.tabId === tabId); + const url = String(targets[0]?.url || "").replace(/[?#].*$/, ""); + const id = extractForeignExtId(targets) || "unknown"; + return new Error( + `Another extension (${id}${url ? `, ${url}` : ""}) has a prompt open on this tab, probably a password manager such as 1Password, and Chrome will not let the debugger work on the tab while it is there. ` + + `The user may be using it right now, so do not close it on your own: ask the user, or wait for them to finish. ` + + `When the user says to go ahead, or the tab is yours and idle, call chrome_clear_overlay for this tab. Navigating the tab away also works.`, + ); +} + +// Injected into the page by overlay.clear. Removes other extensions' iframes (including inside +// open shadow roots), blurs the focused field and sends Escape to the page. +function clearForeignOverlays(ownId) { + const removed = []; + const visit = (root) => { + for (const el of Array.from(root.querySelectorAll("*"))) { + if (el.tagName === "IFRAME" && /^chrome-extension:\/\//.test(el.src) && !el.src.startsWith(`chrome-extension://${ownId}/`)) { + removed.push(el.src.replace(/[?#].*$/, "")); + el.remove(); + } else if (el.shadowRoot) { + visit(el.shadowRoot); + } + } + }; + visit(document); + const active = document.activeElement; + if (active && active !== document.body && typeof active.blur === "function") active.blur(); + document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", bubbles: true })); + return { removed }; +} + +async function clearOverlay(params) { + const tab = await getTabByParams(params, { createOwnedTarget: false }); + let injected = { removed: [] }; try { - await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 }); - await cdpRaw(tabId, "Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 }); - await sleep(120); - } catch {} + const [first] = await executeScriptTimed({ target: { tabId: tab.id }, func: clearForeignOverlays, args: [chrome.runtime.id] }, "chrome_clear_overlay"); + injected = first?.result ?? injected; + } catch (error) { + throw new Error(`Could not run the clean-up on tab ${tab.id}: ${error?.message || error}`); + } + await sleep(250); + const remaining = (await findForeignExtensionTargets()).filter((t) => t.tabId === tab.id).map((t) => String(t.url || "").replace(/[?#].*$/, "")); + let debuggerAttachable = true; + try { + await attachDebugger(tab.id); + } catch { + debuggerAttachable = false; + } + return { tab: { id: tab.id, url: tab.url, title: tab.title }, removedFrames: injected.removed, remainingForeignTargets: remaining, debuggerAttachable, cleared: debuggerAttachable && remaining.length === 0 }; } async function cdp(tabId, method, params, opts) { @@ -706,28 +755,8 @@ async function cdp(tabId, method, params, opts) { } catch (error) { const msg = String(error?.message || error); const isStale = /Debugger is not attached|Detached while|Target closed|No tab with id/i.test(msg); - const isForeignExtBlock = /Cannot access a chrome-extension:\/\/ URL of different extension/i.test(msg); - if (isForeignExtBlock && /Input\./.test(method)) { - // Foreign chrome-extension popup (autofill, password manager) is hijacking input. - // Try once: dismiss via Esc, then retry. - const before = await findForeignExtensionTargets(); - recordAttachEvent({ kind: "foreign-ext-detected", tabId, method, foreignExtId: extractForeignExtId(before), targetCount: before.length }); - await dismissOverlayViaEscape(tabId); - try { - return await cdpRaw(tabId, method, params, opts); - } catch (retryErr) { - const retryMsg = String(retryErr?.message || retryErr); - if (/Cannot access a chrome-extension:\/\/ URL of different extension/i.test(retryMsg)) { - const after = await findForeignExtensionTargets(); - const id = extractForeignExtId(after) || extractForeignExtId(before) || "unknown"; - throw new Error( - `Another Chrome extension (${id}) has an input overlay on this page (e.g. a password manager / autofill popup). \n` + - `pi-chrome tried to dismiss it with Escape but it reappeared. Disable that extension on this page, close its popup, or focus the field via Tab instead of clicking.`, - ); - } - throw retryErr; - } - } + const isForeignExtBlock = FOREIGN_EXTENSION_BLOCK.test(msg); + if (isForeignExtBlock) throw await overlayBlockedError(tabId); if (!isStale) throw error; attachedTabs.delete(tabId); await attachDebugger(tabId).catch(() => undefined); @@ -2021,6 +2050,8 @@ async function dispatch(action, params) { } case "page.screenshot": return takeScreenshot(params); + case "overlay.clear": + return clearOverlay(params); case "cdp.targets": { // Diagnostics: CDP targets anchored to the resolved tab, including foreign overlay targets // (password managers, autofill, DevTools front-ends) that explain "Detached while handling |
