pi-chrome 0.10.2 → 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.
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ pi-chrome v<version>
|
|
|
69
69
|
|
|
70
70
|
By default, `chrome_*` clicks and keystrokes are **synthetic** DOM events (`event.isTrusted === false`). They drive React/Vue/Angular state correctly but **do not** satisfy Chrome's user-activation gates: clipboard write, fullscreen, file picker, and autoplay all need a real user gesture.
|
|
71
71
|
|
|
72
|
-
pi-chrome can optionally route input through `chrome.debugger` (CDP `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`) so each event arrives as `isTrusted=true`, satisfies user-activation, and bypasses site bot-detection that filters synthetic events. The tradeoff: Chrome pins a yellow *"Pi
|
|
72
|
+
pi-chrome can optionally route input through `chrome.debugger` (CDP `Input.dispatchMouseEvent` / `Input.dispatchKeyEvent`) so each event arrives as `isTrusted=true`, satisfies user-activation, and bypasses site bot-detection that filters synthetic events. The tradeoff: Chrome pins a yellow *"Pi Chrome Connector started debugging this browser"* banner to the top of any debugged tab.
|
|
73
73
|
|
|
74
74
|
Usage:
|
|
75
75
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"name": "Pi
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Lets Pi control tabs in
|
|
3
|
+
"name": "Pi Chrome Connector",
|
|
4
|
+
"version": "0.11.1",
|
|
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/*"],
|
|
8
8
|
"background": {
|
|
9
9
|
"service_worker": "service_worker.js"
|
|
10
10
|
},
|
|
11
11
|
"action": {
|
|
12
|
-
"default_title": "Pi Chrome
|
|
12
|
+
"default_title": "Pi Chrome Connector"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const BRIDGE_URL = "http://127.0.0.1:17318";
|
|
2
|
-
const CLIENT_NAME = `Pi Chrome
|
|
2
|
+
const CLIENT_NAME = `Pi Chrome Connector ${chrome.runtime.id}`;
|
|
3
3
|
const POLL_ERROR_BACKOFF_MS = 2000;
|
|
4
4
|
let polling = false;
|
|
5
5
|
|
|
6
6
|
// =================== Trusted-input (CDP) layer ===================
|
|
7
7
|
// Tracks which tabs we have attached chrome.debugger to, plus session-level mode.
|
|
8
8
|
const attachedTabs = new Map(); // tabId -> { detachAt: number, pointer: {x,y} }
|
|
9
|
-
let TRUSTED_MODE = "
|
|
9
|
+
let TRUSTED_MODE = "auto"; // "off" | "on" | "auto" (default: smart retry only)
|
|
10
10
|
const TRUSTED_IDLE_DETACH_MS = 15_000;
|
|
11
11
|
const CDP_VERSION = "1.3";
|
|
12
12
|
|
|
@@ -35,6 +35,31 @@ function trustedStatus() {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Auto-upgrade: if synthetic result carries suggestTrusted=true, the bridge mode is "auto"
|
|
39
|
+
// (default) or "on", and the caller didn't explicitly opt out, retry once with trusted CDP
|
|
40
|
+
// path. Surfaces both results so callers can see what happened.
|
|
41
|
+
async function maybeUpgradeToTrusted(kind, params, syntheticResult, trustedFn) {
|
|
42
|
+
if (!syntheticResult || !syntheticResult.suggestTrusted) return syntheticResult;
|
|
43
|
+
if (params && params.trusted === false) return syntheticResult;
|
|
44
|
+
if (TRUSTED_MODE === "off") return syntheticResult;
|
|
45
|
+
if (!chrome.debugger) return syntheticResult;
|
|
46
|
+
try {
|
|
47
|
+
const trustedResult = await trustedFn();
|
|
48
|
+
return {
|
|
49
|
+
...trustedResult,
|
|
50
|
+
autoRetried: true,
|
|
51
|
+
autoRetryReason: syntheticResult.suggestReason || `${kind} produced no mutation`,
|
|
52
|
+
syntheticAttempt: { pageMutated: syntheticResult.pageMutated, suggestReason: syntheticResult.suggestReason },
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return {
|
|
56
|
+
...syntheticResult,
|
|
57
|
+
autoRetryAttempted: true,
|
|
58
|
+
autoRetryError: error?.message || String(error),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
async function attachDebugger(tabId) {
|
|
39
64
|
if (!chrome.debugger) throw new Error("chrome.debugger API unavailable; reload the extension to grant the new permission");
|
|
40
65
|
if (attachedTabs.has(tabId)) {
|
|
@@ -78,7 +103,7 @@ setInterval(() => {
|
|
|
78
103
|
}
|
|
79
104
|
}, 5000);
|
|
80
105
|
|
|
81
|
-
function
|
|
106
|
+
function cdpRaw(tabId, method, params) {
|
|
82
107
|
return new Promise((resolve, reject) => {
|
|
83
108
|
chrome.debugger.sendCommand({ tabId }, method, params || {}, (result) => {
|
|
84
109
|
if (chrome.runtime.lastError) reject(new Error(`${method}: ${chrome.runtime.lastError.message}`));
|
|
@@ -87,6 +112,24 @@ function cdp(tabId, method, params) {
|
|
|
87
112
|
});
|
|
88
113
|
}
|
|
89
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
|
+
|
|
90
133
|
// Resolve target -> {x, y, rect} in viewport coords by running tiny script in tab.
|
|
91
134
|
async function resolveTargetInTab(tabId, params) {
|
|
92
135
|
const results = await chrome.scripting.executeScript({
|
|
@@ -324,20 +367,48 @@ async function trustedScroll(params) {
|
|
|
324
367
|
const x = resolved.rect ? resolved.rect.left + Math.min(resolved.rect.width, 800) / 2 : resolved.x;
|
|
325
368
|
const y = resolved.rect ? resolved.rect.top + Math.min(resolved.rect.height, 600) / 2 : resolved.y;
|
|
326
369
|
const totalY = params.deltaY || 0, totalX = params.deltaX || 0;
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
const
|
|
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
|
+
}
|
|
330
384
|
const sumW = w.reduce((a, b) => a + b, 0);
|
|
331
385
|
for (let i = 0; i < n; i++) {
|
|
332
386
|
const dy = totalY * (w[i] / sumW), dx = totalX * (w[i] / sumW);
|
|
333
387
|
await cdp(tab.id, "Input.dispatchMouseEvent", {
|
|
334
388
|
type: "mouseWheel", x, y, deltaX: dx, deltaY: dy, pointerType: "mouse",
|
|
335
389
|
});
|
|
336
|
-
|
|
390
|
+
// Sleep ~one frame+ so IntersectionObserver / rAF samples can run between events.
|
|
391
|
+
await sleep(rng(22, 52));
|
|
337
392
|
}
|
|
338
393
|
return { trusted: true, deltaX: totalX, deltaY: totalY, steps: n };
|
|
339
394
|
}
|
|
340
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
|
+
|
|
341
412
|
async function trustedDrag(params) {
|
|
342
413
|
const tab = await getTabByParams(params);
|
|
343
414
|
if (params.foreground) await bringToFront(tab);
|
|
@@ -485,9 +556,11 @@ async function dispatch(action, params) {
|
|
|
485
556
|
]);
|
|
486
557
|
case "page.evaluate":
|
|
487
558
|
return evaluateInTab(params);
|
|
488
|
-
case "page.click":
|
|
559
|
+
case "page.click": {
|
|
489
560
|
if (await wantsTrusted(params)) return trustedClick(params);
|
|
490
|
-
|
|
561
|
+
const synth = await executeActionInTab(params, clickPage, [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null]);
|
|
562
|
+
return await maybeUpgradeToTrusted("click", params, synth, () => trustedClick(params));
|
|
563
|
+
}
|
|
491
564
|
case "page.hover":
|
|
492
565
|
if (await wantsTrusted(params)) return trustedHover(params);
|
|
493
566
|
return executeActionInTab(params, hoverPage, [params.selector ?? null, params.uid ?? null, params.x ?? null, params.y ?? null]);
|
|
@@ -496,9 +569,11 @@ async function dispatch(action, params) {
|
|
|
496
569
|
return executeActionInTab(params, dragPage, [params.fromUid ?? null, params.fromSelector ?? null, params.fromX ?? null, params.fromY ?? null, params.toUid ?? null, params.toSelector ?? null, params.toX ?? null, params.toY ?? null, params.steps ?? 12]);
|
|
497
570
|
case "page.upload":
|
|
498
571
|
return executeActionInTab(params, uploadFiles, [params.selector ?? null, params.uid ?? null, params.files || []]);
|
|
499
|
-
case "page.type":
|
|
572
|
+
case "page.type": {
|
|
500
573
|
if (await wantsTrusted(params)) return trustedType(params);
|
|
501
|
-
|
|
574
|
+
const synth = await executeActionInTab(params, typeIntoPage, [params.selector ?? null, params.uid ?? null, params.text || "", Boolean(params.pressEnter)]);
|
|
575
|
+
return await maybeUpgradeToTrusted("type", params, synth, () => trustedType(params));
|
|
576
|
+
}
|
|
502
577
|
case "page.fill":
|
|
503
578
|
if (await wantsTrusted(params)) return trustedFill(params);
|
|
504
579
|
return executeActionInTab(params, fillPage, [params.selector ?? null, params.uid ?? null, params.text || "", params.submit === true]);
|
|
@@ -508,6 +583,8 @@ async function dispatch(action, params) {
|
|
|
508
583
|
case "page.scroll":
|
|
509
584
|
if (await wantsTrusted(params)) return trustedScroll(params);
|
|
510
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);
|
|
511
588
|
case "trusted.mode":
|
|
512
589
|
return setTrustedMode(params.mode);
|
|
513
590
|
case "trusted.status":
|
|
@@ -1262,23 +1339,46 @@ async function clickPage(selector, uid, x, y) {
|
|
|
1262
1339
|
// Heuristic: if the clicked thing looks like a media play affordance and the page has paused
|
|
1263
1340
|
// audio/video, the synthetic click may not unlock autoplay. Surface a warning.
|
|
1264
1341
|
let autoplayHint;
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1342
|
+
const labelRaw = (point.element.getAttribute("aria-label") || point.element.textContent || "").trim();
|
|
1343
|
+
const label = labelRaw.toLowerCase();
|
|
1344
|
+
if (/^(play|start|begin|next|continue|unmute)/.test(label)) {
|
|
1267
1345
|
const idleMedia = Array.from(document.querySelectorAll("audio,video")).some((m) => m.paused);
|
|
1268
1346
|
if (idleMedia) autoplayHint = "This element looks like a media affordance and the page has paused media. Synthetic clicks do not satisfy user-activation gates; audio/video may not start.";
|
|
1269
1347
|
}
|
|
1348
|
+
const pageMutated = pageHash() !== before;
|
|
1349
|
+
// Smart-auto retry hint: only set when synthetic produced no observable change AND the
|
|
1350
|
+
// element looks gated, OR the page just emitted a user-activation rejection. The dispatcher
|
|
1351
|
+
// uses this to decide whether to retry with trusted mode.
|
|
1352
|
+
let suggestTrusted = false;
|
|
1353
|
+
let suggestReason;
|
|
1354
|
+
if (!pageMutated) {
|
|
1355
|
+
if (autoplayHint) { suggestTrusted = true; suggestReason = "play/media affordance + idle media"; }
|
|
1356
|
+
else if (/copy(\s|$)|paste|share|download|fullscreen|sign in with|continue with|allow|enable/i.test(label)) {
|
|
1357
|
+
suggestTrusted = true; suggestReason = `label '${labelRaw.slice(0, 40)}' looks gated`;
|
|
1358
|
+
} else {
|
|
1359
|
+
// Inspect recent console errors for activation-gate rejections.
|
|
1360
|
+
const recent = (state.console || []).slice(-8);
|
|
1361
|
+
const hit = recent.find((e) => /NotAllowedError|Document is not focused|requires transient activation|gesture is required/.test(
|
|
1362
|
+
(e.args || []).map((a) => typeof a === "string" ? a : (a && a.message) || JSON.stringify(a)).join(" ")
|
|
1363
|
+
));
|
|
1364
|
+
if (hit) { suggestTrusted = true; suggestReason = "recent console error indicates user-activation gate"; }
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1270
1367
|
return {
|
|
1271
1368
|
x: point.x,
|
|
1272
1369
|
y: point.y,
|
|
1273
1370
|
selector,
|
|
1274
1371
|
uid,
|
|
1275
1372
|
tag: point.element.tagName,
|
|
1373
|
+
label: labelRaw.slice(0, 80) || undefined,
|
|
1276
1374
|
isTrusted: false,
|
|
1277
1375
|
defaultPrevented,
|
|
1278
1376
|
elementVisible: visible,
|
|
1279
1377
|
occludedBy: occluded || undefined,
|
|
1280
|
-
pageMutated
|
|
1378
|
+
pageMutated,
|
|
1281
1379
|
autoplayHint,
|
|
1380
|
+
suggestTrusted: suggestTrusted || undefined,
|
|
1381
|
+
suggestReason,
|
|
1282
1382
|
};
|
|
1283
1383
|
}
|
|
1284
1384
|
|
|
@@ -1526,15 +1626,27 @@ async function typeIntoPage(selector, uid, text, pressEnter) {
|
|
|
1526
1626
|
const before = pageHash();
|
|
1527
1627
|
let element = elementBySelectorOrUid(selector, uid) || document.activeElement;
|
|
1528
1628
|
if (!element) throw new Error(selector || uid ? `No element for ${selector || uid}` : "No active element");
|
|
1629
|
+
const initialValue = "value" in element ? element.value : (element.isContentEditable ? element.textContent : null);
|
|
1529
1630
|
element.focus();
|
|
1530
1631
|
if (!(element.isContentEditable || "value" in element)) throw new Error("Focused element is not text-editable");
|
|
1531
1632
|
for (const ch of Array.from(text)) await typeCharacter(element, ch);
|
|
1532
1633
|
if (pressEnter) pressKeyInPage("Enter");
|
|
1634
|
+
const finalValue = "value" in element ? element.value : element.textContent;
|
|
1635
|
+
const valueMatches = "value" in element ? element.value.includes(text) : (element.textContent || "").includes(text);
|
|
1636
|
+
const pageMutated = pageHash() !== before;
|
|
1637
|
+
// Smart-auto retry hint when typing didn't land at all (e.g., editor blocks synthetic input).
|
|
1638
|
+
let suggestTrusted = false, suggestReason;
|
|
1639
|
+
if (text.length > 0 && initialValue === finalValue) {
|
|
1640
|
+
suggestTrusted = true;
|
|
1641
|
+
suggestReason = "value did not change — editor likely rejects synthetic input";
|
|
1642
|
+
}
|
|
1533
1643
|
return {
|
|
1534
1644
|
selector, uid, length: text.length, pressEnter,
|
|
1535
1645
|
isTrusted: false,
|
|
1536
|
-
valueMatches
|
|
1537
|
-
pageMutated
|
|
1646
|
+
valueMatches,
|
|
1647
|
+
pageMutated,
|
|
1648
|
+
suggestTrusted: suggestTrusted || undefined,
|
|
1649
|
+
suggestReason,
|
|
1538
1650
|
};
|
|
1539
1651
|
}
|
|
1540
1652
|
|
|
@@ -471,7 +471,7 @@ Usage rules:
|
|
|
471
471
|
lines.push(
|
|
472
472
|
`✗ EXTENSION VERSION MISMATCH: companion extension is v${version.extensionVersion}, but pi-chrome is v${PI_CHROME_VERSION}.`,
|
|
473
473
|
` All chrome_* tools will run with the OLD extension code until this is fixed.`,
|
|
474
|
-
` Fix: open chrome://extensions and click reload on "Pi
|
|
474
|
+
` Fix: open chrome://extensions and click reload on "Pi Chrome Connector".`,
|
|
475
475
|
` (Future version drifts will self-heal: the extension now polls pi-chrome's expected version and reloads itself.)`,
|
|
476
476
|
);
|
|
477
477
|
} else {
|
|
@@ -518,7 +518,14 @@ Usage rules:
|
|
|
518
518
|
permissionGranted?: boolean;
|
|
519
519
|
};
|
|
520
520
|
if (status.permissionGranted) {
|
|
521
|
-
|
|
521
|
+
const attached = status.attachedTabs && status.attachedTabs.length ? `; attached to tab ${status.attachedTabs.join(",")}` : "";
|
|
522
|
+
const note =
|
|
523
|
+
status.mode === "auto"
|
|
524
|
+
? " — smart-retry enabled: synthetic input runs first; if a click/type produced no page change AND the target looks gated, the call is automatically re-run with trusted CDP (yellow debugger banner appears only for that retry)."
|
|
525
|
+
: status.mode === "on"
|
|
526
|
+
? " — every chrome_* call goes through CDP; the yellow debugger banner is visible while attached."
|
|
527
|
+
: " — synthetic events only; pass trusted=true on chrome_click/type/etc, or switch to auto/on with /chrome-trusted, when isTrusted or user-activation gates matter.";
|
|
528
|
+
lines.push(`✓ Trusted-input mode available via chrome.debugger (current: ${status.mode ?? "off"}${attached}).${note}`);
|
|
522
529
|
} else {
|
|
523
530
|
lines.push(`⚠ chrome.debugger API unavailable. The extension is missing the "debugger" permission — reload the extension in chrome://extensions and accept the new permission prompt.`);
|
|
524
531
|
}
|
|
@@ -560,7 +567,7 @@ Usage rules:
|
|
|
560
567
|
|
|
561
568
|
if (!status.permissionGranted) {
|
|
562
569
|
ctx.ui.notify(
|
|
563
|
-
"chrome.debugger API unavailable — the extension is missing the 'debugger' permission. Open chrome://extensions, reload 'Pi
|
|
570
|
+
"chrome.debugger API unavailable — the extension is missing the 'debugger' permission. Open chrome://extensions, reload 'Pi Chrome Connector', and accept the new permission prompt.",
|
|
564
571
|
"warning",
|
|
565
572
|
);
|
|
566
573
|
return;
|
|
@@ -577,9 +584,9 @@ Usage rules:
|
|
|
577
584
|
if (!target) {
|
|
578
585
|
// Interactive picker. Show current mode + tradeoffs in each label.
|
|
579
586
|
const options = [
|
|
580
|
-
`
|
|
581
|
-
`off${current === "off" ? " (current)" : ""} — synthetic DOM events only
|
|
582
|
-
`
|
|
587
|
+
`auto${current === "auto" ? " (current)" : ""} — default; synthetic first, retry with CDP only when a call looks gated`,
|
|
588
|
+
`off${current === "off" ? " (current)" : ""} — synthetic DOM events only; never auto-retry`,
|
|
589
|
+
`on${current === "on" ? " (current)" : ""} — every chrome_* call goes through CDP (yellow debugger banner permanently visible)`,
|
|
583
590
|
`status — print current mode and any attached tabs\u2026`,
|
|
584
591
|
];
|
|
585
592
|
const picked = await ctx.ui.select(
|
|
@@ -610,7 +617,7 @@ Usage rules:
|
|
|
610
617
|
if (target === "on" && current === "off") {
|
|
611
618
|
const ok = await ctx.ui.confirm(
|
|
612
619
|
"Turn on trusted-input mode?",
|
|
613
|
-
"All chrome_* tools will dispatch through chrome.debugger (CDP). Events will arrive as isTrusted=true and satisfy user-activation gates (clipboard, fullscreen, autoplay, file picker).\n\nChrome will pin a yellow 'Pi
|
|
620
|
+
"All chrome_* tools will dispatch through chrome.debugger (CDP). Events will arrive as isTrusted=true and satisfy user-activation gates (clipboard, fullscreen, autoplay, file picker).\n\nChrome will pin a yellow 'Pi Chrome Connector started debugging this browser' banner to the top of any debugged tab while attached. Clicking 'Cancel' on that banner detaches the debugger.",
|
|
614
621
|
);
|
|
615
622
|
if (!ok) {
|
|
616
623
|
ctx.ui.notify("Trusted-input mode unchanged.", "info");
|
|
@@ -835,7 +842,7 @@ Usage rules:
|
|
|
835
842
|
name: "chrome_click",
|
|
836
843
|
label: "Chrome Click",
|
|
837
844
|
description:
|
|
838
|
-
"Click a snapshot uid, CSS selector, or viewport coordinate
|
|
845
|
+
"Click a snapshot uid, CSS selector, or viewport coordinate. Default 'auto' mode runs synthetic DOM events first and silently retries with trusted CDP only when the click looks gated (no page change + affordance label matches play/copy/share/sign-in/etc, or a recent NotAllowedError). The yellow 'started debugging' banner appears only when the retry actually happens. Pass trusted=true to force CDP for this call (banner appears immediately). Pass trusted=false to skip retry. Pass includeSnapshot=true to return a fresh snapshot after the click.",
|
|
839
846
|
promptSnippet: "Click page elements in Chrome by snapshot uid, selector, or viewport coordinate.",
|
|
840
847
|
parameters: Type.Object({
|
|
841
848
|
uid: Type.Optional(Type.String({ description: "Stable element uid from chrome_snapshot. Prefer uid over selector after taking a snapshot." })),
|
|
@@ -868,7 +875,7 @@ Usage rules:
|
|
|
868
875
|
name: "chrome_type",
|
|
869
876
|
label: "Chrome Type",
|
|
870
877
|
description:
|
|
871
|
-
"Focus an optional snapshot uid or CSS selector, then type text
|
|
878
|
+
"Focus an optional snapshot uid or CSS selector, then type text. Default 'auto' mode runs synthetic per-character keydown/beforeinput/input/keyup first; if the input value doesn't change at all (editor rejected synthetic input) the call is silently retried through chrome.debugger so each keystroke is browser-trusted (isTrusted=true). Pass trusted=true to force CDP for this call. Pass trusted=false to skip retry. Pass includeSnapshot=true to return a fresh snapshot after typing.",
|
|
872
879
|
promptSnippet: "Type text into Chrome, optionally focusing a snapshot uid or selector first.",
|
|
873
880
|
parameters: Type.Object({
|
|
874
881
|
text: Type.String(),
|
|
@@ -1158,6 +1165,29 @@ Usage rules:
|
|
|
1158
1165
|
},
|
|
1159
1166
|
});
|
|
1160
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
|
+
|
|
1161
1191
|
pi.registerTool({
|
|
1162
1192
|
name: "chrome_scroll",
|
|
1163
1193
|
label: "Chrome Scroll",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-chrome",
|
|
3
|
-
"version": "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",
|