pi-chrome 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Pi Chrome Connector",
4
- "version": "0.12.0",
4
+ "version": "0.12.1",
5
5
  "description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
6
6
  "permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
7
7
  "host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
@@ -67,7 +67,22 @@ async function attachDebugger(tabId) {
67
67
  entry.detachAt = Date.now() + TRUSTED_IDLE_DETACH_MS;
68
68
  return entry;
69
69
  }
70
- await chrome.debugger.attach({ tabId }, CDP_VERSION);
70
+ try {
71
+ await chrome.debugger.attach({ tabId }, CDP_VERSION);
72
+ } catch (error) {
73
+ // Chrome occasionally rejects attach with "Cannot access a chrome-extension:// URL of
74
+ // different extension" right after a navigation, even when the target tab's URL is a
75
+ // normal page. Wait a tick, verify the tab is on a non-privileged URL, and retry once.
76
+ const msg = String(error?.message || error);
77
+ 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);
78
+ if (!transient) throw error;
79
+ await sleep(180);
80
+ const tab = await chrome.tabs.get(tabId).catch(() => null);
81
+ if (!tab || (tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) {
82
+ throw new Error(`Chrome can't attach the debugger to this tab (${tab?.url ?? "unknown"}). Open a normal http(s) tab and try again.`);
83
+ }
84
+ await chrome.debugger.attach({ tabId }, CDP_VERSION);
85
+ }
71
86
  // Seed pointer in a plausible "just left the address bar" location.
72
87
  const entry = { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } };
73
88
  attachedTabs.set(tabId, entry);
@@ -256,9 +271,28 @@ async function trustedClick(params) {
256
271
  const resolved = await resolveTargetInTab(tab.id, params);
257
272
  const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
258
273
  await cdpMoveTo(tab.id, point.x, point.y);
259
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
274
+ 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 });
260
275
  await sleep(rng(45, 140));
261
276
  await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
277
+ // Reset :focus-visible if the click landed on a focusable element. CDP-driven pointer
278
+ // focus can leave :focus-visible=true in Chromium, which trips heuristics that expect
279
+ // pointer focus to suppress the focus ring (synthetic clicks naturally land on false).
280
+ if (params.selector || params.uid) {
281
+ await chrome.scripting.executeScript({
282
+ target: { tabId: tab.id, frameIds: [0] },
283
+ world: "MAIN",
284
+ func: (sel, uid) => {
285
+ const state = window.__PI_CHROME_STATE__;
286
+ let el = null;
287
+ if (uid && state && state.elements && state.elements[uid]) el = state.elements[uid];
288
+ else if (sel) el = document.querySelector(sel);
289
+ if (el && typeof el.focus === "function" && el === document.activeElement) {
290
+ try { el.blur(); el.focus({ preventScroll: true, focusVisible: false }); } catch {}
291
+ }
292
+ },
293
+ args: [params.selector ?? null, params.uid ?? null],
294
+ }).catch(() => undefined);
295
+ }
262
296
  return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
263
297
  }
264
298
 
@@ -320,7 +354,7 @@ async function trustedType(params) {
320
354
  const resolved = await resolveTargetInTab(tab.id, params);
321
355
  const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
322
356
  await cdpMoveTo(tab.id, point.x, point.y);
323
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
357
+ 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 });
324
358
  await sleep(rng(45, 110));
325
359
  await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: 1, pointerType: "mouse" });
326
360
  await sleep(rng(50, 120));
@@ -344,7 +378,7 @@ async function trustedFill(params) {
344
378
  await cdpMoveTo(tab.id, point.x, point.y);
345
379
  // Triple-click selects all in input fields.
346
380
  for (let i = 1; i <= 3; i++) {
347
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: point.x, y: point.y, button: "left", buttons: 1, clickCount: i, pointerType: "mouse" });
381
+ 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 });
348
382
  await sleep(rng(20, 60));
349
383
  await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mouseReleased", x: point.x, y: point.y, button: "left", buttons: 0, clickCount: i, pointerType: "mouse" });
350
384
  await sleep(rng(20, 60));
@@ -367,21 +401,36 @@ async function trustedScroll(params) {
367
401
  const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
368
402
  const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y;
369
403
  const totalY = params.deltaY || 0, totalX = params.deltaX || 0;
370
- // Per-event delta cap so IntersectionObserver / scroll-driven animations get gradient samples.
371
- // A trackpad inertia tick often delivers ~10-30px per frame; using ~25px keeps small-target
372
- // visibility transitions detectable while not making large scrolls take forever.
373
- const MAX_STEP = 25;
404
+ // Profile mimics a trackpad flick: short ramp-up (~15% of events), then geometric decay
405
+ // with a ~12% drop per event. Gives momentum tail tests something to find, and the small
406
+ // tail deltas (a handful of <20px events) put IntersectionObserver thresholds in range.
374
407
  const peak = Math.max(Math.abs(totalY), Math.abs(totalX));
375
- // Front-loaded weights peak at ~1.5× average, so choose n so peak event stays under MAX_STEP.
376
- const minN = Math.ceil(peak * 1.5 / MAX_STEP);
377
- const n = Math.max(6, Math.min(200, params.steps || Math.max(minN, 12)));
378
- // Front-loaded but smooth weights: w[i] = 1 + 0.5 * (1 - i/(n-1)) so the first event has
379
- // weight 1.5, the last has 1.0, average ~1.25; redistribution stays predictable.
408
+ // Aim peak event ~22px so cumulative wheel approach to target seeds low-ratio IO samples.
409
+ const PEAK_TARGET = 22;
380
410
  const w = [];
381
- for (let i = 0; i < n; i++) {
382
- const t = i / Math.max(1, n - 1);
383
- w.push(1 + 0.5 * (1 - t));
411
+ // Build weights for an arbitrary n, then iterate to find an n where peak * (w_peak/sum) <= PEAK_TARGET.
412
+ function build(n) {
413
+ const arr = [];
414
+ const peakIdx = Math.max(1, Math.floor(n * 0.15));
415
+ for (let i = 0; i < n; i++) {
416
+ if (i <= peakIdx) arr.push(0.5 + 0.5 * (i / peakIdx)); // 0.5 → 1.0
417
+ else arr.push(Math.pow(0.88, i - peakIdx)); // ~12% drop per step
418
+ }
419
+ return arr;
420
+ }
421
+ let n = Math.max(12, params.steps || 24);
422
+ for (let attempt = 0; attempt < 8; attempt++) {
423
+ const arr = build(n);
424
+ const s = arr.reduce((a, b) => a + b, 0);
425
+ const peakStep = peak * (Math.max(...arr) / s);
426
+ if (peakStep <= PEAK_TARGET || n >= 240) {
427
+ w.length = 0;
428
+ w.push(...arr);
429
+ break;
430
+ }
431
+ n = Math.ceil(n * 1.4);
384
432
  }
433
+ if (w.length === 0) w.push(...build(n));
385
434
  const sumW = w.reduce((a, b) => a + b, 0);
386
435
  for (let i = 0; i < n; i++) {
387
436
  const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
@@ -419,7 +468,7 @@ async function trustedDrag(params) {
419
468
  const fp = from.rect ? pickInsideRect(from.rect) : { x: from.x, y: from.y };
420
469
  const tp = to.rect ? pickInsideRect(to.rect) : { x: to.x, y: to.y };
421
470
  await cdpMoveTo(tab.id, fp.x, fp.y);
422
- await cdp(tab.id, "Input.dispatchMouseEvent", { type: "mousePressed", x: fp.x, y: fp.y, button: "left", buttons: 1, clickCount: 1, pointerType: "mouse" });
471
+ 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 });
423
472
  await sleep(rng(60, 140));
424
473
  const steps = params.steps || 20;
425
474
  for (let i = 1; i <= steps; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "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).",
5
5
  "keywords": [
6
6
  "pi-package",