aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
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 };