pi-chrome 0.11.0 → 0.11.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.11.0",
4
+ "version": "0.11.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/*"],
@@ -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,48 @@ 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
+ // Cap per-step delta so IntersectionObserver / scroll-driven animations get gradient samples.
371
+ // Real wheel notches ~50-120px; we aim ~40-90.
372
+ const MAX_STEP = 80;
373
+ const minStepsByDelta = Math.ceil(Math.max(Math.abs(totalY), Math.abs(totalX)) / MAX_STEP);
374
+ const n = Math.max(6, Math.min(60, params.steps || Math.max(minStepsByDelta, 12)));
375
+ // Momentum shape: ramp-up, plateau, decay. Each weight peaked mid-sequence then tapers.
376
+ const w = [];
377
+ for (let i = 0; i < n; i++) {
378
+ const t = i / Math.max(1, n - 1);
379
+ // bell-ish curve biased earlier so motion starts strong then decays (like a trackpad flick).
380
+ const base = Math.sin(Math.min(1, t * 1.4) * Math.PI);
381
+ const decay = Math.pow(1 - t * 0.6, 2);
382
+ w.push(0.2 + base * 0.8 * decay);
383
+ }
355
384
  const sumW = w.reduce((a, b) => a + b, 0);
356
385
  for (let i = 0; i < n; i++) {
357
386
  const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
358
387
  await cdp(tab.id, "Input.dispatchMouseEvent", {
359
388
  type: "mouseWheel", x, y, deltaX: dx, deltaY: dy, pointerType: "mouse",
360
389
  });
361
- await sleep(rng(14, 32));
390
+ // Sleep ~one frame+ so IntersectionObserver / rAF samples can run between events.
391
+ await sleep(rng(22, 52));
362
392
  }
363
393
  return { trusted: true, deltaX: totalX, deltaY: totalY, steps: n };
364
394
  }
365
395
 
396
+ async function trustedTap(params) {
397
+ const tab = await getTabByParams(params);
398
+ if (params.foreground) await bringToFront(tab);
399
+ await attachDebugger(tab.id);
400
+ const resolved = (params.selector || params.uid || (typeof params.x === "number" && typeof params.y === "number"))
401
+ ? await resolveTargetInTab(tab.id, params)
402
+ : null;
403
+ if (!resolved || !resolved.found) throw new Error("trusted.tap: target not found");
404
+ const point = resolved.rect ? pickInsideRect(resolved.rect) : { x: resolved.x, y: resolved.y };
405
+ const tp = { x: point.x, y: point.y, radiusX: 8, radiusY: 8, rotationAngle: 0, force: 0.5, id: 1 };
406
+ await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchStart", touchPoints: [tp] });
407
+ await sleep(rng(40, 110));
408
+ await cdp(tab.id, "Input.dispatchTouchEvent", { type: "touchEnd", touchPoints: [] });
409
+ return { trusted: true, x: point.x, y: point.y, tag: resolved.tag };
410
+ }
411
+
366
412
  async function trustedDrag(params) {
367
413
  const tab = await getTabByParams(params);
368
414
  if (params.foreground) await bringToFront(tab);
@@ -537,6 +583,8 @@ async function dispatch(action, params) {
537
583
  case "page.scroll":
538
584
  if (await wantsTrusted(params)) return trustedScroll(params);
539
585
  return executeActionInTab(params, scrollPage, [params.selector ?? null, params.uid ?? null, params.deltaY ?? 0, params.deltaX ?? 0, params.steps ?? null]);
586
+ case "page.tap":
587
+ return trustedTap(params);
540
588
  case "trusted.mode":
541
589
  return setTrustedMode(params.mode);
542
590
  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.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",