aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 02:35:01 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 02:35:01 -0400
commit26ab0d36e45b1b65f667d4743eb30e83389ff57f (patch)
tree678f92867c16671caa116b4e1762a672703f9863 /extensions
parentb319c82a75e9a6488a6454790d1436f74a655e7c (diff)
pi-chrome 0.14.2: recover from foreign-extension input overlays
When a different Chrome extension (password manager, autofill popup, etc.) anchors an overlay to the focused input, Chrome's CDP refuses our Input.dispatchMouseEvent with 'Cannot access a chrome-extension:// URL of different extension'. The CDP target list shows the foreign chrome-extension:// URL as type:'other' attached to the same tab. New behaviour in cdp() wrapper: 1. On that specific error for any Input.* call, snapshot CDP targets to identify the offending extension id and log it (foreign-ext- detected event added to ring buffer). 2. Send Esc via Input.dispatchKeyEvent (key dispatch routes by focus, not coordinates, so it bypasses the overlay) to dismiss the popup. 3. Retry the original Input.* call once. 4. If retry still fails, throw a clear error naming the foreign extension id and listing three concrete workarounds: disable that extension on the page, use Tab-to-focus instead of clicking, or run /chrome quiet off.
Diffstat (limited to 'extensions')
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js60
-rw-r--r--extensions/chrome-profile-bridge/index.ts5
3 files changed, 65 insertions, 2 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index aa7bfae..f0f8a6a 100644
--- a/extensions/chrome-profile-bridge/browser-extension/manifest.json
+++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Pi Chrome Connector",
- "version": "0.14.1",
+ "version": "0.14.2",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index d256f1c..36e5b20 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -185,12 +185,72 @@ function cdpRaw(tabId, method, params) {
// chrome.debugger.attach can stay cached in attachedTabs even after Chrome killed
// the session (tab nav, devtools opened/closed, etc). Recover by detaching the
// stale entry and re-attaching, then retry the command once.
+// Find foreign chrome-extension targets currently anchored to the tab. Password managers,
+// autofill helpers, and other input-attached extensions create type:"other" CDP targets
+// whose URL is chrome-extension://<otherId>/... When that target is in focus, CDP refuses
+// our Input.dispatchMouseEvent calls with "Cannot access a chrome-extension:// URL of
+// different extension" — surfacing a cryptic error to the user.
+async function findForeignExtensionTargets() {
+ try {
+ const targets = await new Promise((resolve) => chrome.debugger.getTargets((t) => resolve(t || [])));
+ return targets.filter((t) => {
+ const url = String(t.url || "");
+ if (!url.startsWith("chrome-extension://")) return false;
+ if (t.extensionId === chrome.runtime.id) return false;
+ return true;
+ });
+ } catch {
+ return [];
+ }
+}
+
+function extractForeignExtId(targets) {
+ for (const t of targets) {
+ if (t.extensionId && t.extensionId !== chrome.runtime.id) return t.extensionId;
+ const m = String(t.url || "").match(/chrome-extension:\/\/([a-p]+)\//);
+ if (m && m[1] !== chrome.runtime.id) return m[1];
+ }
+ 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.
+ 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 {}
+}
+
async function cdp(tabId, method, params) {
try {
return await cdpRaw(tabId, method, params);
} 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);
+ } 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, focus the field via Tab instead of clicking, or run /chrome quiet off so the agent uses synthetic input here.`,
+ );
+ }
+ throw retryErr;
+ }
+ }
if (!isStale) throw error;
attachedTabs.delete(tabId);
await chrome.debugger.attach({ tabId }, CDP_VERSION).catch(() => undefined);
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 2fa51b0..2923040 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -1,6 +1,5 @@
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
-import { StringEnum } from "@earendil-works/pi-ai";
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
import { Type } from "typebox";
import { existsSync, readFileSync, statSync } from "node:fs";
@@ -381,6 +380,10 @@ const tabActionValues = ["list", "new", "activate", "close", "version"] as const
const imageFormatValues = ["png", "jpeg"] as const;
const waitForValues = ["selector", "expression"] as const;
+function StringEnum<T extends readonly [string, ...string[]]>(values: T) {
+ return Type.Union(values.map((value) => Type.Literal(value)) as [ReturnType<typeof Type.Literal>, ...ReturnType<typeof Type.Literal>[]]);
+}
+
export default function (pi: ExtensionAPI): void {
const globalState = globalThis as typeof globalThis & {
[PI_CHROME_GLOBAL_KEY]?: { version: string; root: string };