aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 01:29:51 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-05-13 01:29:51 -0400
commit6ddb6007df14a865d4226d7ee16e08ff4d3c36ca (patch)
treef9cb55e32c3991171b2f8a449de781eb30d04458
parent6dd4b2b0abe30ed11b0969fe838fa24f67b7d6d1 (diff)
pi-chrome 0.10.1: chrome_key modifiers param for trusted chords (Cmd+V etc)
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js26
-rw-r--r--extensions/chrome-profile-bridge/index.ts6
-rw-r--r--package.json2
-rw-r--r--test-suite/challenges/10-user-activation.html7
5 files changed, 35 insertions, 8 deletions
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 969293a..cd5e1b5 100644
--- a/extensions/chrome-profile-bridge/browser-extension/manifest.json
+++ b/extensions/chrome-profile-bridge/browser-extension/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Pi Existing Chrome Profile Bridge",
- "version": "0.10.0",
+ "version": "0.10.1",
"description": "Lets Pi control tabs in this existing Chrome profile via a local bridge at 127.0.0.1.",
"permissions": ["tabs", "scripting", "storage", "activeTab", "alarms", "webNavigation", "debugger"],
"host_permissions": ["<all_urls>", "http://127.0.0.1:17318/*"],
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index beb2dbf..bd00b1e 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -236,18 +236,36 @@ async function trustedKey(params) {
await attachDebugger(tab.id);
const key = String(params.key || "");
if (!key) throw new Error("trusted.key: missing key");
+ const mods = params.modifiers || {};
+ const modBits = cdpModifiersFor(mods);
+ // Press modifiers in standard order, then key, then release in reverse.
+ const modOrder = [];
+ if (mods.metaKey) modOrder.push({ key: "Meta", code: "MetaLeft", vk: 91 });
+ if (mods.ctrlKey) modOrder.push({ key: "Control", code: "ControlLeft", vk: 17 });
+ if (mods.altKey) modOrder.push({ key: "Alt", code: "AltLeft", vk: 18 });
+ if (mods.shiftKey) modOrder.push({ key: "Shift", code: "ShiftLeft", vk: 16 });
+ for (const m of modOrder) {
+ await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyDown", key: m.key, code: m.code, windowsVirtualKeyCode: m.vk, modifiers: modBits });
+ await sleep(rng(6, 18));
+ }
const info = cdpKeyInfo(key);
+ // When modifiers are active, browsers usually emit "rawKeyDown" (no text) so chords like Cmd+V don't insert the literal char.
+ const downType = modBits ? "rawKeyDown" : "keyDown";
await cdp(tab.id, "Input.dispatchKeyEvent", {
- type: "keyDown", key: info.key, code: info.code,
+ type: downType, key: info.key, code: info.code,
windowsVirtualKeyCode: info.windowsVirtualKeyCode, nativeVirtualKeyCode: info.windowsVirtualKeyCode,
- text: info.text, unmodifiedText: info.text,
+ text: modBits ? "" : info.text, unmodifiedText: modBits ? "" : info.text, modifiers: modBits,
});
await sleep(rng(25, 90));
await cdp(tab.id, "Input.dispatchKeyEvent", {
type: "keyUp", key: info.key, code: info.code,
- windowsVirtualKeyCode: info.windowsVirtualKeyCode,
+ windowsVirtualKeyCode: info.windowsVirtualKeyCode, modifiers: modBits,
});
- return { trusted: true, key: info.key };
+ for (const m of modOrder.reverse()) {
+ await sleep(rng(5, 18));
+ await cdp(tab.id, "Input.dispatchKeyEvent", { type: "keyUp", key: m.key, code: m.code, windowsVirtualKeyCode: m.vk, modifiers: 0 });
+ }
+ return { trusted: true, key: info.key, modifiers: mods };
}
async function trustedType(params) {
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index 11e1a40..56bf224 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -882,6 +882,12 @@ Usage rules:
promptSnippet: "Press keys in Chrome through the companion extension.",
parameters: Type.Object({
key: Type.String(),
+ modifiers: Type.Optional(Type.Object({
+ shiftKey: Type.Optional(Type.Boolean()),
+ ctrlKey: Type.Optional(Type.Boolean()),
+ altKey: Type.Optional(Type.Boolean()),
+ metaKey: Type.Optional(Type.Boolean()),
+ }, { description: "Modifier keys to hold while pressing the key (chord). Only honoured for trusted-mode presses; synthetic path ignores." })),
includeSnapshot: Type.Optional(Type.Boolean({ description: "If true, include a fresh chrome_snapshot result after the keypress." })),
maxElements: Type.Optional(Type.Number({ default: MAX_ELEMENTS, description: "Max elements in the included snapshot." })),
targetId: Type.Optional(Type.String()),
diff --git a/package.json b/package.json
index 4fdd66f..7da23c9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-chrome",
- "version": "0.10.0",
+ "version": "0.10.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",
diff --git a/test-suite/challenges/10-user-activation.html b/test-suite/challenges/10-user-activation.html
index d0929d8..1a12e39 100644
--- a/test-suite/challenges/10-user-activation.html
+++ b/test-suite/challenges/10-user-activation.html
@@ -12,15 +12,18 @@
<script>
Challenge.init({ id: "user-activation", instructions: "click button; user-activation must unlock gated APIs" });
document.getElementById("go").addEventListener("click", async () => {
+ // Capture activation state synchronously before any await (activation can be consumed by gated APIs).
const ua = navigator.userActivation;
- Challenge.log("activation", { isActive: ua?.isActive, hasBeenActive: ua?.hasBeenActive });
+ const wasActive = !!ua?.isActive;
+ const hadBeenActive = !!ua?.hasBeenActive;
+ Challenge.log("activation", { isActive: wasActive, hasBeenActive: hadBeenActive });
let clip = "skip", fs = "skip";
try { await navigator.clipboard.writeText("pi-chrome-test"); clip = "ok"; }
catch (e) { clip = "err:" + e.name; }
try { await document.documentElement.requestFullscreen(); fs = "ok"; document.exitFullscreen?.(); }
catch (e) { fs = "err:" + e.name; }
const bad = [];
- if (!ua?.isActive) bad.push("userActivation.isActive === false (synthetic click)");
+ if (!wasActive && !hadBeenActive) bad.push("userActivation.isActive/hasBeenActive both false (synthetic click)");
if (clip.startsWith("err")) bad.push("clipboard.writeText " + clip);
if (fs.startsWith("err")) bad.push("requestFullscreen " + fs);
Challenge.log("gates", { clip, fs });