pi-chrome 0.11.0 → 0.11.2

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.11.0",
4
+ "version": "0.11.2",
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/*"],
@@ -103,7 +103,7 @@ setInterval(() => {
103
103
  }
104
104
  }, 5000);
105
105
 
106
- function cdp(tabId, method, params) {
106
+ function cdpRaw(tabId, method, params) {
107
107
  return new Promise((resolve, reject) => {
108
108
  chrome.debugger.sendCommand({ tabId }, method, params || {}, (result) => {
109
109
  if (chrome.runtime.lastError) reject(new Error(`${method}: ${chrome.runtime.lastError.message}`));
@@ -112,6 +112,24 @@ function cdp(tabId, method, params) {
112
112
  });
113
113
  }
114
114
 
115
+ // Wraps cdpRaw with one auto-recover on detached/closed sessions:
116
+ // chrome.debugger.attach can stay cached in attachedTabs even after Chrome killed
117
+ // the session (tab nav, devtools opened/closed, etc). Recover by detaching the
118
+ // stale entry and re-attaching, then retry the command once.
119
+ async function cdp(tabId, method, params) {
120
+ try {
121
+ return await cdpRaw(tabId, method, params);
122
+ } catch (error) {
123
+ const msg = String(error?.message || error);
124
+ const isStale = /Debugger is not attached|Detached while|Target closed|No tab with id/i.test(msg);
125
+ if (!isStale) throw error;
126
+ attachedTabs.delete(tabId);
127
+ await chrome.debugger.attach({ tabId }, CDP_VERSION).catch(() => undefined);
128
+ attachedTabs.set(tabId, { detachAt: Date.now() + TRUSTED_IDLE_DETACH_MS, pointer: { x: 120 + Math.random() * 200, y: 80 + Math.random() * 120 } });
129
+ return cdpRaw(tabId, method, params);
130
+ }
131
+ }
132
+
115
133
  // Resolve target -> {x, y, rect} in viewport coords by running tiny script in tab.
116
134
  async function resolveTargetInTab(tabId, params) {
117
135
  const results = await chrome.scripting.executeScript({
@@ -349,20 +367,49 @@ async function trustedScroll(params) {
349
367
  const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
350
368
  const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y;
351
369
  const totalY = params.deltaY || 0, totalX = params.deltaX || 0;
352
- const n = Math.max(3, Math.min(20, params.steps || Math.ceil(Math.abs(totalY) / 120)));
353
- // momentum-shaped front-loaded weights
354
- const w = []; for (let i = 1; i <= n; i++) w.push(1 / i);
370
+ // Per-event delta cap so IntersectionObserver / scroll-driven animations get gradient samples.
371
+ // A real wheel notch is ~50-120px; we aim for the lower end so visibility transitions are visible.
372
+ const MAX_STEP = 60;
373
+ const peak = Math.max(Math.abs(totalY), Math.abs(totalX));
374
+ // We weight events with mild front-loading. The peak weight = 1.5 / n (vs uniform 1/n), so the
375
+ // first event is ~1.5× average. Choose n so even the peak event stays under MAX_STEP.
376
+ const minN = Math.ceil(peak * 1.5 / MAX_STEP);
377
+ const n = Math.max(6, Math.min(80, 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.
380
+ 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));
384
+ }
355
385
  const sumW = w.reduce((a, b) => a + b, 0);
356
386
  for (let i = 0; i < n; i++) {
357
387
  const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
358
388
  await cdp(tab.id, "Input.dispatchMouseEvent", {
359
389
  type: "mouseWheel", x, y, deltaX: dx, deltaY: dy, pointerType: "mouse",
360
390
  });
361
- await sleep(rng(14, 32));
391
+ // Sleep one+ frame so IntersectionObserver / rAF samples can run between events.
392
+ await sleep(rng(22, 48));
362
393
  }
363
394
  return { trusted: true, deltaX: totalX, deltaY: totalY, steps: n };
364
395
  }
365
396
 
397
+ async function trustedTap(params) {
398
+ const tab = await getTabByParams(params);
399
+ if (params.foreground) await bringToFront(tab);
400
+ await attachDebugger(tab.id);
401
+ const resolved = (params.selector || params.uid || (typeof params.x === "number" && typeof params.y === "number"))
402
+ ? await resolveTargetInTab(tab.id, params)
403
+ : null;
404
+ if (!resolved || !resolved.found) throw new Error("trusted.tap: target not found");
405
+ const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
406
+ const tp = { x: point.x, y: point.y, radiusX: 8, radiusY: 8, rotationAngle: 0, force: 0.5, id: 1 };
407
+ await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [tp] });
408
+ await sleep(rng(40, 110));
409
+ await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
410
+ return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
411
+ }
412
+
366
413
  async function trustedDrag(params) {
367
414
  const tab = await getTabByParams(params);
368
415
  if (params.foreground) await bringToFront(tab);
@@ -537,6 +584,8 @@ async function dispatch(action, params) {
537
584
  case "page.scroll":
538
585
  if (await wantsTrusted(params)) return trustedScroll(params);
539
586
  return executeActionInTab(params, scrollPage, [params.selector ?? null, params.uid ?? null, params.deltaY ?? 0, params.deltaX ?? 0, params.steps ?? null]);
587
+ case "page.tap":
588
+ return trustedTap(params);
540
589
  case "trusted.mode":
541
590
  return setTrustedMode(params.mode);
542
591
  case "trusted.status":
@@ -1165,6 +1165,29 @@ Usage rules:
1165
1165
  },
1166
1166
  });
1167
1167
 
1168
+ pi.registerTool({
1169
+ name: "chrome_tap",
1170
+ label: "Chrome Tap (Touch)",
1171
+ description:
1172
+ "Dispatch a real browser-trusted touchstart/touchend tap via chrome.debugger (CDP Input.dispatchTouchEvent). Use for sites that gate on TouchEvent rather than MouseEvent (mobile-first PWAs, swipe carousels). Always uses the trusted CDP path — the yellow debugger banner appears.",
1173
+ promptSnippet: "Tap (real touch) a Chrome element by snapshot uid, selector, or coordinate.",
1174
+ parameters: Type.Object({
1175
+ uid: Type.Optional(Type.String()),
1176
+ selector: Type.Optional(Type.String()),
1177
+ x: Type.Optional(Type.Number()),
1178
+ y: Type.Optional(Type.Number()),
1179
+ targetId: Type.Optional(Type.String()),
1180
+ urlIncludes: Type.Optional(Type.String()),
1181
+ titleIncludes: Type.Optional(Type.String()),
1182
+ background: Type.Optional(Type.Boolean()),
1183
+ }),
1184
+ async execute(_id, params): Promise<ToolTextResult> {
1185
+ const result = await bridge.send("page.tap", withBackground(params), DEFAULT_TIMEOUT_MS);
1186
+ const target = params.uid ?? params.selector ?? `${params.x},${params.y}`;
1187
+ return { content: [{ type: "text", text: `Tapped ${target} (touch)` }], details: { result: result as Json } };
1188
+ },
1189
+ });
1190
+
1168
1191
  pi.registerTool({
1169
1192
  name: "chrome_scroll",
1170
1193
  label: "Chrome Scroll",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-chrome",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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",