aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-08 15:32:11 +0000
committerIgor Soarez <igor@soarez.org>2026-11-08 15:32:11 +0000
commit56356bd3400c104eb0b7af1322ff515c155ef7a4 (patch)
tree193aa48263fb7a90bb1e229be5a1ab4288f71deb
parent0edeb17be41b4f57e40b16b9c86bf5ce85add971 (diff)
overlay.clear: click the page body outside the prompt, and wait for it to go
Measured on 1Password's inline menu: blur and Escape did not free the tab, a click outside did. overlay.clear now also sends pointer/mouse events to the body and polls the debugger for about a second instead of checking once.
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js24
-rw-r--r--extensions/chrome-profile-bridge/index.ts2
-rw-r--r--test-suite/unit/overlay.test.mjs7
3 files changed, 23 insertions, 10 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index 73aabc5..b1710ef 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -709,7 +709,7 @@ async function overlayBlockedError(tabId) {
}
// 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.
+// open shadow roots), blurs the focused field, sends Escape and clicks the page body.
function clearForeignOverlays(ownId) {
const removed = [];
const visit = (root) => {
@@ -726,6 +726,12 @@ function clearForeignOverlays(ownId) {
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 }));
+ // A click outside is what closes a password manager's inline menu (measured on 1Password: this,
+ // not Escape or blur, is what freed the tab). Sent to the body, so no page control is clicked.
+ for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
+ const Ctor = type.startsWith("pointer") && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
+ document.body.dispatchEvent(new Ctor(type, { bubbles: true, cancelable: true, view: window, button: 0, clientX: 900, clientY: 500 }));
+ }
return { removed };
}
@@ -738,14 +744,16 @@ async function clearOverlay(params) {
} 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;
+ // The prompt takes a moment to go: try the debugger for up to about a second.
+ let debuggerAttachable = false;
+ for (let attempt = 0; attempt < 6 && !debuggerAttachable; attempt++) {
+ await sleep(attempt === 0 ? 150 : 200);
+ try {
+ await attachDebugger(tab.id);
+ debuggerAttachable = true;
+ } catch {}
}
+ const remaining = (await findForeignExtensionTargets()).filter((t) => t.tabId === tab.id).map((t) => String(t.url || "").replace(/[?#].*$/, ""));
return { tab: { id: tab.id, url: tab.url, title: tab.title }, removedFrames: injected.removed, remainingForeignTargets: remaining, debuggerAttachable, cleared: debuggerAttachable && remaining.length === 0 };
}
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 3d6fa66..8442a58 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -2286,7 +2286,7 @@ Usage rules:
name: "chrome_clear_overlay",
label: "Chrome Clear Overlay",
description:
- "Close another extension's prompt that is blocking a tab, usually a password manager's (1Password) inline sign-in or autofill prompt. While such a prompt is open Chrome refuses the debugger on that tab, so screenshots, clicks, typing and evaluate fail with \"Another extension ... has a prompt open on this tab\". Decide for yourself whether to use it: if the prompt is just in your way (it popped up on a field you are filling, or you do not need anything from it), clear it. If you need the user to sign in, authorize or approve something with it, or the user may be in the middle of using it, do not clear it: tell the user what you are waiting for. It removes the other extension's frame, blurs the focused field and sends Escape, then reports whether the tab is usable again. It does not touch the page's own content, and it cannot close 1Password's browser-toolbar popup or the 1Password desktop app (if the result says the tab is still blocked, tell the user). Navigating the tab away also frees it.",
+ "Close another extension's prompt that is blocking a tab, usually a password manager's (1Password) inline sign-in or autofill prompt. While such a prompt is open Chrome refuses the debugger on that tab, so screenshots, clicks, typing and evaluate fail with \"Another extension ... has a prompt open on this tab\". Decide for yourself whether to use it: if the prompt is just in your way (it popped up on a field you are filling, or you do not need anything from it), clear it. If you need the user to sign in, authorize or approve something with it, or the user may be in the middle of using it, do not clear it: tell the user what you are waiting for. It removes the other extension's frame, blurs the focused field, sends Escape and clicks the page body (a click outside is what closes 1Password's inline menu), then reports whether the tab is usable again. It does not touch the page's own content, and it cannot close 1Password's browser-toolbar popup or the 1Password desktop app (if the result says the tab is still blocked, tell the user). Navigating the tab away also frees it.",
promptSnippet: "Close a password manager prompt that blocks a tab, unless you are waiting for the user to use it.",
parameters: Type.Object({
targetId: Type.Optional(Type.String()),
diff --git a/test-suite/unit/overlay.test.mjs b/test-suite/unit/overlay.test.mjs
index cd60522..a4247b3 100644
--- a/test-suite/unit/overlay.test.mjs
+++ b/test-suite/unit/overlay.test.mjs
@@ -71,9 +71,13 @@ test("the injected clean-up removes foreign extension iframes, not ours, and blu
const shadowFrame = mk("chrome-extension://aeblf/inline/b.html");
const host = { tagName: "DIV", shadowRoot: { querySelectorAll: () => [shadowFrame] } };
let blurred = false, escaped = false;
+ const bodyEvents = [];
const sandbox = {
- document: { querySelectorAll: () => [foreign, ours, page, host], activeElement: { blur: () => { blurred = true; } }, body: {}, dispatchEvent: (e) => { escaped = e.key === "Escape"; } },
+ document: { querySelectorAll: () => [foreign, ours, page, host], activeElement: { blur: () => { blurred = true; } }, body: { dispatchEvent: (e) => { bodyEvents.push(e.type); } }, dispatchEvent: (e) => { escaped = e.key === "Escape"; } },
KeyboardEvent: class { constructor(type, init) { this.type = type; Object.assign(this, init); } },
+ MouseEvent: class { constructor(type) { this.type = type; } },
+ PointerEvent: class { constructor(type) { this.type = type; } },
+ window: {},
Array,
};
const result = vm.runInNewContext(`(${w.clearForeignOverlays.toString()})("ours")`, sandbox);
@@ -81,6 +85,7 @@ test("the injected clean-up removes foreign extension iframes, not ours, and blu
assert.equal(foreign.removed && shadowFrame.removed, true);
assert.equal(ours.removed || page.removed, false);
assert.ok(blurred && escaped);
+ assert.deepEqual([...bodyEvents], ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]);
});
test("the tool and the primer let the agent decide: clear it when in the way, leave it when waiting for the user", () => {