diff options
| author | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-06-07 23:30:40 -0400 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-06-07 23:30:40 -0400 |
| commit | 35bebbbb35d4f461ca6d3302294b45d5602eca13 (patch) | |
| tree | ba9e7c4bac4eb2c4e78efb394d5de877767209ec /extensions | |
| parent | 4701544c02852a254f9cd49ade83a2fcaa0b1699 (diff) | |
0.15.38: harden chrome input overlay handling
Diffstat (limited to 'extensions')
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()), |
