diff options
Diffstat (limited to 'extensions')
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 }; |
