aboutsummaryrefslogtreecommitdiff
path: root/extensions
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-06-07 23:30:40 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-06-07 23:30:40 -0400
commit35bebbbb35d4f461ca6d3302294b45d5602eca13 (patch)
treeba9e7c4bac4eb2c4e78efb394d5de877767209ec /extensions
parent4701544c02852a254f9cd49ade83a2fcaa0b1699 (diff)
0.15.38: harden chrome input overlay handling
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.js88
-rw-r--r--extensions/chrome-profile-bridge/index.ts1
3 files changed, 63 insertions, 28 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index fce30b1..b4ef2e3 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.15.37",
+ "version": "0.15.38",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": [
"tabs",
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index fc4ea92..55de152 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -7,7 +7,7 @@ const VALID_GROUP_COLORS = new Set(["grey", "blue", "red", "yellow", "green", "p
const COMMAND_TIMEOUT_MS = 25_000;
const CDP_COMMAND_TIMEOUT_MS = 5_000;
const SCRIPTING_TIMEOUT_MS = 8_000;
-const ATTACH_TIMEOUT_MS = 8_000;
+const ATTACH_TIMEOUT_MS = 3_000;
let polling = false;
function withTimeout(promise, ms, label, onTimeout) {
@@ -479,36 +479,70 @@ async function cdpTypeChar(tabId, ch) {
await sleep(rng(35, 130));
}
+async function domClickFallback(tabId, params, cause) {
+ const results = await executeScriptTimed({
+ target: { tabId, frameIds: [0] },
+ world: "MAIN",
+ func: (selector, uid, x, y) => {
+ const state = window.__PI_CHROME_STATE__;
+ let el = uid && state && state.elements ? state.elements[uid] : null;
+ if (uid && (!el || !el.isConnected)) return { staleUid: true, reason: `snapshot uid ${uid} is stale; refresh chrome_snapshot`, url: location.href };
+ if (!el && selector) el = document.querySelector(selector);
+ if (!el && typeof x === "number" && typeof y === "number") el = document.elementFromPoint(x, y);
+ if (!el) throw new Error(`DOM fallback target not found: ${uid || selector || `${x},${y}`}`);
+ el.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
+ const rect = el.getBoundingClientRect();
+ const eventInit = { bubbles: true, cancelable: true, view: window, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, button: 0, buttons: 1 };
+ el.dispatchEvent(new PointerEvent("pointerdown", { ...eventInit, pointerId: 1, pointerType: "mouse", isPrimary: true }));
+ el.dispatchEvent(new MouseEvent("mousedown", eventInit));
+ if (typeof el.focus === "function") el.focus({ preventScroll: true });
+ el.dispatchEvent(new PointerEvent("pointerup", { ...eventInit, pointerId: 1, pointerType: "mouse", isPrimary: true, buttons: 0 }));
+ el.dispatchEvent(new MouseEvent("mouseup", { ...eventInit, buttons: 0 }));
+ el.click();
+ return { tag: el.tagName, url: location.href };
+ },
+ args: [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null],
+ }, `DOM click fallback in tab ${tabId}`);
+ const v = results?.[0]?.result;
+ if (v?.staleUid) throw new Error(v.reason || "snapshot uid is stale; refresh chrome_snapshot");
+ return { input: "dom-fallback", reason: String(cause?.message || cause).slice(0, 500), tag: v?.tag };
+}
+
async function chromeInputClick(params) {
const tab = await getTabByParams(params);
if (params.foreground) await bringToFront(tab);
- await attachDebugger(tab.id);
- const resolved = await resolveTargetInTab(tab.id, params);
- const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
- await cdpMoveTo(tab.id, point.x, point.y);
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 });
- await sleep(rng(45, 140));
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
- // Reset :focus-visible if the click landed on a focusable element. CDP-driven pointer
- // focus can leave :focus-visible=true in Chromium, which trips heuristics that expect
- // Reset focus styling after pointer click when possible.
- if (params.selector || params.uid) {
- await chrome.scripting.executeScript({
- target: { tabId: tab.id, frameIds: [0] },
- world: "MAIN",
- func: (sel, uid) => {
- const state = window.__PI_CHROME_STATE__;
- let el = null;
- if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
- else if (sel) el = document.querySelector(sel);
- if (el && typeof el.focus === "function" && el === document.activeElement) {
- try { el.blur(); el.focus({ preventScroll: true, focusVisible: false }); } catch {}
- }
- },
- args: [params.selector ?? null, params.uid ?? null],
- }).catch(() => undefined);
+ try {
+ await attachDebugger(tab.id);
+ const resolved = await resolveTargetInTab(tab.id, params);
+ const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
+ await cdpMoveTo(tab.id, point.x, point.y);
+ await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 });
+ await sleep(rng(45, 140));
+ await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
+ // Reset :focus-visible if the click landed on a focusable element. CDP-driven pointer
+ // focus can leave :focus-visible=true in Chromium, which trips heuristics that expect
+ // Reset focus styling after pointer click when possible.
+ if (params.selector || params.uid) {
+ await executeScriptTimed({
+ target: { tabId: tab.id, frameIds: [0] },
+ world: "MAIN",
+ func: (sel, uid) => {
+ const state = window.__PI_CHROME_STATE__;
+ let el = null;
+ if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
+ else if (sel) el = document.querySelector(sel);
+ if (el && typeof el.focus === "function" && el === document.activeElement) {
+ try { el.blur(); el.focus({ preventScroll: true, focusVisible: false }); } catch {}
+ }
+ },
+ args: [params.selector ?? null, params.uid ?? null],
+ }, `reset focus style in tab ${tab.id}`).catch(() => undefined);
+ }
+ return { input: "chrome", x: point.x, y: point.y, tag: resolved.tag };
+ } catch (error) {
+ if (params.domFallback === false) throw error;
+ return domClickFallback(tab.id, params, error);
}
- return { input: "chrome", x: point.x, y: point.y, tag: resolved.tag };
}
async function chromeInputHover(params) {
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 7542708..424b9c1 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -1413,6 +1413,7 @@ Usage rules:
selector: Type.Optional(Type.String({ description: "CSS selector to click. Prefer uid from chrome_snapshot when available." })),
x: Type.Optional(Type.Number({ description: "Viewport x coordinate if uid/selector is omitted." })),
y: Type.Optional(Type.Number({ description: "Viewport y coordinate if uid/selector is omitted." })),
+ domFallback: Type.Optional(Type.Boolean({ description: "If true (default), fall back to DOM-dispatched click if Chrome's CDP input path is blocked by another extension overlay or debugger failure." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after the click." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),