pi-chrome 0.12.1: fix 07/13/21/27/28 anti-automation regressions - 07 pointer-properties: pass force: 0.5 on every CDP mousePressed so PointerEvent.pressure reports 0.5 for trusted clicks (was 0, inconsistent with real mouse behaviour). - 13 focus-order: after a CDP-driven click on a focusable target, re-focus with focusVisible: false so :focus-visible matches pointer focus semantics (CDP press leaves :focus-visible=true otherwise). - 21 keyboard-modifiers: chrome.debugger.attach occasionally rejects with 'Cannot access a chrome-extension:// URL of different extension' immediately after navigation. attachDebugger now waits 180ms, verifies the tab URL isn't privileged, and retries once. - 27 scroll-momentum + 28 intersection-visibility: replace gentle ~1.25%/step linear taper with a ramp-up (~15%) + geometric decay (~12% drop/step). Sizes n adaptively so peak event stays ≲22px so small visibility targets get low-ratio IO samples while a clear momentum tail remains for decay-pattern detectors. - trustedDrag mousePressed also gets force: 0.5 (consistency).
Tianren Dong tianrendong.dtr@gmail.com
Wed, 13 May 2026 02:11:03 -0400
3 files changed,
68 insertions(+),
19 deletions(-)
M
extensions/chrome-profile-bridge/browser-extension/manifest.json
→
extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,7 +1,7 @@
{ "manifest_version": 3, "name": "Pi Chrome Connector", - "version": "0.12.0", + "version": "0.12.1", "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/*"],
M
extensions/chrome-profile-bridge/browser-extension/service_worker.js
→
extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -67,7 +67,22 @@ const entry = attachedTabs.get(tabId);
entry.detachAt = Date.now() + TRUSTED_IDLE_DETACH_MS; return entry; } - await chrome.debugger.attach({ tabId }, CDP_VERSION); + try { + await chrome.debugger.attach({ tabId }, CDP_VERSION); + } catch (error) { + // Chrome occasionally rejects attach with "Cannot access a chrome-extension:// URL of + // different extension" right after a navigation, even when the target tab's URL is a + // normal page. Wait a tick, verify the tab is on a non-privileged URL, and retry once. + const msg = String(error?.message || error); + const transient = /Cannot access a chrome-extension|Cannot access contents of|No tab with id|Debugger is not attached|Another debugger|Target closed/i.test(msg); + if (!transient) throw error; + await sleep(180); + const tab = await chrome.tabs.get(tabId).catch(() => null); + if (!tab || (tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) { + throw new Error(`Chrome can't attach the debugger to this tab (${tab?.url ?? "unknown"}). Open a normal http(s) tab and try again.`); + } + await chrome.debugger.attach({ tabId }, CDP_VERSION); + } // Seed pointer in a plausible "just left the address bar" location. const entry = { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } }; attachedTabs.set(tabId, entry);@@ -256,9 +271,28 @@ 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" }); + 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 + // pointer focus to suppress the focus ring (synthetic clicks naturally land on false). + 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); + } return { trusted: true, x: point.x, y: point.y, tag: resolved.tag }; }@@ -320,7 +354,7 @@ // Focus target by clicking it first.
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" }); + 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, 110)); await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" }); await sleep(rng(50, 120));@@ -344,7 +378,7 @@ const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
await cdpMoveTo(tab.id, point.x, point.y); // Triple-click selects all in input fields. for (let i = 1; i <= 3; i++) { - await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse" }); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse", force: 0.5 }); await sleep(rng(20, 60)); await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" }); await sleep(rng(20, 60));@@ -367,21 +401,36 @@ const resolved = (params.selector || params.uid) ? await resolveTargetInTab(tab.id, params) : { x: 100, y: 100, rect: null };
const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x; const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y; const totalY = params.deltaY || 0, totalX = params.deltaX || 0; - // Per-event delta cap so IntersectionObserver / scroll-driven animations get gradient samples. - // A trackpad inertia tick often delivers ~10-30px per frame; using ~25px keeps small-target - // visibility transitions detectable while not making large scrolls take forever. - const MAX_STEP = 25; + // Profile mimics a trackpad flick: short ramp-up (~15% of events), then geometric decay + // with a ~12% drop per event. Gives momentum tail tests something to find, and the small + // tail deltas (a handful of <20px events) put IntersectionObserver thresholds in range. const peak = Math.max(Math.abs(totalY), Math.abs(totalX)); - // Front-loaded weights peak at ~1.5× average, so choose n so peak event stays under MAX_STEP. - const minN = Math.ceil(peak * 1.5 / MAX_STEP); - const n = Math.max(6, Math.min(200, params.steps || Math.max(minN, 12))); - // Front-loaded but smooth weights: w[i] = 1 + 0.5 * (1 - i/(n-1)) so the first event has - // weight 1.5, the last has 1.0, average ~1.25; redistribution stays predictable. + // Aim peak event ~22px so cumulative wheel approach to target seeds low-ratio IO samples. + const PEAK_TARGET = 22; const w = []; - for (let i = 0; i < n; i++) { - const t = i / Math.max(1, n - 1); - w.push(1 + 0.5 * (1 - t)); + // Build weights for an arbitrary n, then iterate to find an n where peak * (w_peak/sum) <= PEAK_TARGET. + function build(n) { + const arr = []; + const peakIdx = Math.max(1, Math.floor(n * 0.15)); + for (let i = 0; i < n; i++) { + if (i <= peakIdx) arr.push(0.5 + 0.5 * (i / peakIdx)); // 0.5 → 1.0 + else arr.push(Math.pow(0.88, i - peakIdx)); // ~12% drop per step + } + return arr; + } + let n = Math.max(12, params.steps || 24); + for (let attempt = 0; attempt < 8; attempt++) { + const arr = build(n); + const s = arr.reduce((a, b) => a + b, 0); + const peakStep = peak * (Math.max(...arr) / s); + if (peakStep <= PEAK_TARGET || n >= 240) { + w.length = 0; + w.push(...arr); + break; + } + n = Math.ceil(n * 1.4); } + if (w.length === 0) w.push(...build(n)); const sumW = w.reduce((a, b) => a + b, 0); for (let i = 0; i < n; i++) { const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);@@ -419,7 +468,7 @@ const to = await resolveTargetInTab(tab.id, { selector: params.toSelector ?? null, uid: params.toUid ?? null, x: params.toX ?? null, y: params.toY ?? null });
const fp = from.rect ? pickInsideRect(from.rect) : { x: from.x, y: from.y }; const tp = to.rect ? pickInsideRect(to.rect) : { x: to.x, y: to.y }; await cdpMoveTo(tab.id, fp.x, fp.y); - await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" }); + await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse", force: 0.5 }); await sleep(rng(60, 140)); const steps = params.steps || 20; for (let i = 1; i <= steps; i++) {
M
package.json
→
package.json
@@ -1,6 +1,6 @@
{ "name": "pi-chrome", - "version": "0.12.0", + "version": "0.12.1", "description": "Drive your existing logged-in Chrome from Pi — no re-login, no throwaway profile, watch the agent work in real time (or toggle quiet background mode).", "keywords": [ "pi-package",