pi-chrome 0.15.33 → 0.15.35
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/CHANGELOG.md +5 -0
- package/extensions/chrome-profile-bridge/browser-extension/manifest.json +1 -1
- package/extensions/chrome-profile-bridge/browser-extension/service_worker.js +55 -13
- package/extensions/chrome-profile-bridge/index.ts +37 -1
- package/package.json +1 -1
- package/test-suite/unit/csp-eval.test.mjs +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable user-facing changes to `pi-chrome`.
|
|
4
4
|
|
|
5
|
+
## 0.15.34 — 2026-06-01
|
|
6
|
+
|
|
7
|
+
- **Every tab Pi uses now joins the session group.** Previously only `chrome_tab new`/`group` created/used the `Pi Session: <name-or-id>` group; tabs Pi drove via `page.*` actions (navigate, click, type, snapshot, screenshot, etc.) on the existing/active tab stayed ungrouped. Now any ungrouped tab Pi interacts with is pulled into this session's group, so the user can see exactly which tabs Pi is driving. Tabs already in a group (the user's or another session's) are left untouched.
|
|
8
|
+
- **Fixed punctuation being dropped by `chrome_type`/`chrome_key`.** Printable punctuation derived its `code`/virtual-keyCode from `charCodeAt()`, so `.` mapped to keyCode 46 (VK_DELETE), `-` to 45 (VK_INSERT), shifted symbols to wrong codes, and `code` was the raw char (`"."`) instead of the physical key (`"Period"`). Apps with keydown handlers (e.g. Gmail's filter address field) saw a Delete/Insert key and silently rejected the character. Both the CDP and DOM-event input paths now resolve keys through a proper US-keyboard layout map.
|
|
9
|
+
|
|
5
10
|
## 0.15.33 — 2026-05-31
|
|
6
11
|
|
|
7
12
|
- **Background is now the default.** `chrome_*` tools run silently without focusing Chrome unless you opt in. Pass `background: false` per call, or run `/chrome background off`, to bring Chrome forward and watch. Tool/param descriptions and docs updated to match.
|
|
@@ -315,6 +315,38 @@ function cdpModifiersFor(mods) {
|
|
|
315
315
|
return m;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Resolve a single printable character to { code, keyCode, needShift } on a US layout.
|
|
319
|
+
// Self-contained (maps defined inline) so it can be serialized into the page via
|
|
320
|
+
// HELPER_FUNCS for the DOM-event fallback as well as used by the CDP path.
|
|
321
|
+
// Using charCodeAt() for punctuation is wrong: e.g. "." is charCode 46 which collides
|
|
322
|
+
// with VK_DELETE, "-" is 45 (VK_INSERT), so app keydown handlers misfire and drop input.
|
|
323
|
+
function usKeyLayoutForChar(ch) {
|
|
324
|
+
const PUNCT = {
|
|
325
|
+
"`": { code: "Backquote", keyCode: 192 }, "~": { code: "Backquote", keyCode: 192, shift: true },
|
|
326
|
+
"-": { code: "Minus", keyCode: 189 }, "_": { code: "Minus", keyCode: 189, shift: true },
|
|
327
|
+
"=": { code: "Equal", keyCode: 187 }, "+": { code: "Equal", keyCode: 187, shift: true },
|
|
328
|
+
"[": { code: "BracketLeft", keyCode: 219 }, "{": { code: "BracketLeft", keyCode: 219, shift: true },
|
|
329
|
+
"]": { code: "BracketRight", keyCode: 221 }, "}": { code: "BracketRight", keyCode: 221, shift: true },
|
|
330
|
+
"\\": { code: "Backslash", keyCode: 220 }, "|": { code: "Backslash", keyCode: 220, shift: true },
|
|
331
|
+
";": { code: "Semicolon", keyCode: 186 }, ":": { code: "Semicolon", keyCode: 186, shift: true },
|
|
332
|
+
"'": { code: "Quote", keyCode: 222 }, "\"": { code: "Quote", keyCode: 222, shift: true },
|
|
333
|
+
",": { code: "Comma", keyCode: 188 }, "<": { code: "Comma", keyCode: 188, shift: true },
|
|
334
|
+
".": { code: "Period", keyCode: 190 }, ">": { code: "Period", keyCode: 190, shift: true },
|
|
335
|
+
"/": { code: "Slash", keyCode: 191 }, "?": { code: "Slash", keyCode: 191, shift: true },
|
|
336
|
+
" ": { code: "Space", keyCode: 32 },
|
|
337
|
+
};
|
|
338
|
+
// Shifted digit symbols share the digit's physical code + keyCode.
|
|
339
|
+
const SHIFT_DIGIT = { ")": "0", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9" };
|
|
340
|
+
if (/^[a-z]$/.test(ch)) return { code: `Key${ch.toUpperCase()}`, keyCode: ch.toUpperCase().charCodeAt(0), needShift: false };
|
|
341
|
+
if (/^[A-Z]$/.test(ch)) return { code: `Key${ch}`, keyCode: ch.charCodeAt(0), needShift: true };
|
|
342
|
+
if (/^[0-9]$/.test(ch)) return { code: `Digit${ch}`, keyCode: ch.charCodeAt(0), needShift: false };
|
|
343
|
+
if (SHIFT_DIGIT[ch]) { const d = SHIFT_DIGIT[ch]; return { code: `Digit${d}`, keyCode: d.charCodeAt(0), needShift: true }; }
|
|
344
|
+
const p = PUNCT[ch];
|
|
345
|
+
if (p) return { code: p.code, keyCode: p.keyCode, needShift: !!p.shift };
|
|
346
|
+
// Unknown char (e.g. unicode): keep text-driven insertion, avoid bogus keyCode collisions.
|
|
347
|
+
return { code: ch, keyCode: 0, needShift: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
318
350
|
function cdpKeyInfo(key, shifted) {
|
|
319
351
|
// Map common keys to CDP key event init fields. Returns { code, key, windowsVirtualKeyCode, text }.
|
|
320
352
|
const SPECIAL = {
|
|
@@ -336,11 +368,8 @@ function cdpKeyInfo(key, shifted) {
|
|
|
336
368
|
if (SPECIAL[key]) return { key, ...SPECIAL[key] };
|
|
337
369
|
if (key.length === 1) {
|
|
338
370
|
const ch = key;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
else if (/^[0-9]$/.test(ch)) { code = `Digit${ch}`; vk = ch.charCodeAt(0); }
|
|
342
|
-
else { code = ch; vk = ch.charCodeAt(0); }
|
|
343
|
-
return { key: ch, code, windowsVirtualKeyCode: vk, text: ch };
|
|
371
|
+
const layout = usKeyLayoutForChar(ch);
|
|
372
|
+
return { key: ch, code: layout.code, windowsVirtualKeyCode: layout.keyCode, text: ch };
|
|
344
373
|
}
|
|
345
374
|
return { key, code: key, windowsVirtualKeyCode: 0, text: "" };
|
|
346
375
|
}
|
|
@@ -937,9 +966,27 @@ async function getTabByParams(params) {
|
|
|
937
966
|
if ((tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) {
|
|
938
967
|
throw new Error(`Chrome blocks extension automation on protected URL: ${tab.url}`);
|
|
939
968
|
}
|
|
969
|
+
// Tabs Pi interacts with (page.* actions) join this session's group so the user can see exactly
|
|
970
|
+
// which tabs Pi is driving. We only adopt *ungrouped* tabs — never hijack a tab the user (or
|
|
971
|
+
// another Pi session) already grouped, since groupTab would otherwise rename that group.
|
|
972
|
+
if (params.joinSessionGroup && params.sessionGroupTitle) {
|
|
973
|
+
await joinSessionGroup(tab, params.sessionGroupTitle);
|
|
974
|
+
}
|
|
940
975
|
return tab;
|
|
941
976
|
}
|
|
942
977
|
|
|
978
|
+
// Add an ungrouped tab to the session's tab group (reusing it by title, else creating it).
|
|
979
|
+
// No-op when the tab is already grouped or tabGroups is unavailable.
|
|
980
|
+
async function joinSessionGroup(tab, title) {
|
|
981
|
+
if (!chrome.tabGroups || typeof tab.id !== "number") return;
|
|
982
|
+
if (typeof tab.groupId === "number" && tab.groupId >= 0) return;
|
|
983
|
+
try {
|
|
984
|
+
await groupTab(tab, title);
|
|
985
|
+
} catch {
|
|
986
|
+
// Grouping is best-effort; never block the actual page action on a grouping failure.
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
943
990
|
// Helper sources that get concatenated into the injected MAIN-world script. Kept as separate
|
|
944
991
|
// functions so callers below can reference them by `.toString()`. The helpers do not perform any
|
|
945
992
|
// eval themselves — they're plain function declarations.
|
|
@@ -961,6 +1008,7 @@ const HELPER_FUNCS = [
|
|
|
961
1008
|
dispatchPointerLikeEvent,
|
|
962
1009
|
humanMoveTo,
|
|
963
1010
|
humanClickPoint,
|
|
1011
|
+
usKeyLayoutForChar,
|
|
964
1012
|
printableKeyCode,
|
|
965
1013
|
dispatchKeyEvent,
|
|
966
1014
|
typeCharacter,
|
|
@@ -1974,19 +2022,13 @@ function setNativeValue(element, value) {
|
|
|
1974
2022
|
}
|
|
1975
2023
|
|
|
1976
2024
|
function printableKeyCode(ch) {
|
|
1977
|
-
|
|
1978
|
-
const upper = ch.toUpperCase();
|
|
1979
|
-
if (/^[A-Z]$/.test(upper)) return upper.charCodeAt(0);
|
|
1980
|
-
if (/^[0-9]$/.test(ch)) return ch.charCodeAt(0);
|
|
1981
|
-
return ch.charCodeAt(0) || 0;
|
|
2025
|
+
return ch.length === 1 ? usKeyLayoutForChar(ch).keyCode : 0;
|
|
1982
2026
|
}
|
|
1983
2027
|
|
|
1984
2028
|
function dispatchKeyEvent(element, type, key, mods = {}) {
|
|
1985
|
-
const code = key.length === 1 && /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` :
|
|
1986
|
-
key.length === 1 && /^[0-9]$/.test(key) ? `Digit${key}` :
|
|
1987
|
-
key === " " ? "Space" : key;
|
|
1988
2029
|
const SPECIAL = { Enter: 13, Tab: 9, Backspace: 8, Delete: 46, Escape: 27,
|
|
1989
2030
|
ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, " ": 32, Shift: 16, Control: 17, Alt: 18, Meta: 91 };
|
|
2031
|
+
const code = key.length === 1 ? usKeyLayoutForChar(key).code : (key === " " ? "Space" : key);
|
|
1990
2032
|
const keyCode = key.length === 1 ? printableKeyCode(key) : (SPECIAL[key] ?? 0);
|
|
1991
2033
|
const ev = new KeyboardEvent(type, {
|
|
1992
2034
|
key,
|
|
@@ -55,6 +55,10 @@ function readPiChromeVersion(): string {
|
|
|
55
55
|
}
|
|
56
56
|
const PI_CHROME_VERSION = readPiChromeVersion();
|
|
57
57
|
const PI_CHROME_GLOBAL_KEY = "__piChromeProfileBridgeLoaded__";
|
|
58
|
+
// Authorization is kept on globalThis (separate from the singleton flag, which is cleared on
|
|
59
|
+
// reload) so a /reload — which tears down and re-evaluates the module — does not silently drop
|
|
60
|
+
// an active /chrome authorize grant.
|
|
61
|
+
const PI_CHROME_AUTH_KEY = "__piChromeProfileBridgeAuth__";
|
|
58
62
|
const DEFAULT_HOST = process.env.PI_CHROME_BRIDGE_HOST ?? "127.0.0.1";
|
|
59
63
|
const DEFAULT_PORT = Number(process.env.PI_CHROME_BRIDGE_PORT ?? "17318");
|
|
60
64
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -535,6 +539,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
535
539
|
const currentRoot = extensionRoot();
|
|
536
540
|
const globalState = globalThis as typeof globalThis & {
|
|
537
541
|
[PI_CHROME_GLOBAL_KEY]?: { version: string; root: string; token?: symbol };
|
|
542
|
+
[PI_CHROME_AUTH_KEY]?: { until: number | "indefinite" };
|
|
538
543
|
};
|
|
539
544
|
const alreadyLoaded = globalState[PI_CHROME_GLOBAL_KEY];
|
|
540
545
|
if (alreadyLoaded?.token || (alreadyLoaded && alreadyLoaded.root !== currentRoot)) {
|
|
@@ -551,8 +556,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
551
556
|
const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
|
|
552
557
|
let backgroundDefault = true;
|
|
553
558
|
let chromeAuthorizedUntil: number | "indefinite" | undefined;
|
|
559
|
+
// Restore an authorization that survived a /reload. Drop it if it already expired.
|
|
560
|
+
const persistedAuth = globalState[PI_CHROME_AUTH_KEY];
|
|
561
|
+
if (persistedAuth) {
|
|
562
|
+
if (persistedAuth.until === "indefinite" || persistedAuth.until > Date.now()) {
|
|
563
|
+
chromeAuthorizedUntil = persistedAuth.until;
|
|
564
|
+
} else {
|
|
565
|
+
delete globalState[PI_CHROME_AUTH_KEY];
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const persistAuth = (): void => {
|
|
569
|
+
if (chromeAuthorizedUntil === undefined) delete globalState[PI_CHROME_AUTH_KEY];
|
|
570
|
+
else globalState[PI_CHROME_AUTH_KEY] = { until: chromeAuthorizedUntil };
|
|
571
|
+
};
|
|
554
572
|
let chromeToolsRegistered = false;
|
|
555
573
|
let authExpiryTimer: NodeJS.Timeout | undefined;
|
|
574
|
+
// Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
|
|
575
|
+
let sessionCtx: ExtensionContext | undefined;
|
|
556
576
|
|
|
557
577
|
const clearAuthExpiryTimer = (): void => {
|
|
558
578
|
if (!authExpiryTimer) return;
|
|
@@ -574,6 +594,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
574
594
|
const lockChromeControl = (): void => {
|
|
575
595
|
clearAuthExpiryTimer();
|
|
576
596
|
chromeAuthorizedUntil = undefined;
|
|
597
|
+
persistAuth();
|
|
577
598
|
deactivateChromeTools();
|
|
578
599
|
};
|
|
579
600
|
|
|
@@ -632,7 +653,15 @@ export default function (pi: ExtensionAPI): void {
|
|
|
632
653
|
|
|
633
654
|
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
|
|
634
655
|
requireChromeControlAuthorized();
|
|
635
|
-
|
|
656
|
+
// Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the
|
|
657
|
+
// auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull
|
|
658
|
+
// the resolved (e.g. active) tab into the session group on the service-worker side. We skip
|
|
659
|
+
// tab.* actions: tab.new/group group explicitly, and activate/close/ungroup/list must not.
|
|
660
|
+
const shouldJoinGroup = action.startsWith("page.") && sessionCtx !== undefined && params.sessionGroupTitle === undefined;
|
|
661
|
+
const wireParams = shouldJoinGroup
|
|
662
|
+
? { ...params, sessionGroupTitle: sessionGroupTitle(sessionCtx as ExtensionContext), joinSessionGroup: true }
|
|
663
|
+
: params;
|
|
664
|
+
return bridge.send(action, wireParams, timeoutMs, signal);
|
|
636
665
|
};
|
|
637
666
|
|
|
638
667
|
// Translate the public `background` parameter (default on = silent/background) into the
|
|
@@ -650,7 +679,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
650
679
|
};
|
|
651
680
|
|
|
652
681
|
pi.on("session_start", async (_event, ctx) => {
|
|
682
|
+
sessionCtx = ctx;
|
|
653
683
|
await bridge.start();
|
|
684
|
+
// Reestablish in-memory state after a /reload restored chromeAuthorizedUntil from globalThis.
|
|
685
|
+
if (chromeControlAuthorized()) {
|
|
686
|
+
activateChromeTools();
|
|
687
|
+
if (typeof chromeAuthorizedUntil === "number") scheduleAuthExpiry(ctx, chromeAuthorizedUntil);
|
|
688
|
+
}
|
|
654
689
|
updateChromeStatus(ctx);
|
|
655
690
|
});
|
|
656
691
|
|
|
@@ -790,6 +825,7 @@ Usage rules:
|
|
|
790
825
|
return;
|
|
791
826
|
}
|
|
792
827
|
chromeAuthorizedUntil = until;
|
|
828
|
+
persistAuth();
|
|
793
829
|
activateChromeTools();
|
|
794
830
|
scheduleAuthExpiry(ctx, until);
|
|
795
831
|
ctx.ui.notify(`Chrome control authorized for ${label}.`, "info");
|
package/package.json
CHANGED
|
@@ -164,6 +164,27 @@ async function run() {
|
|
|
164
164
|
"waitFor: missing selector times out",
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
+
// ===== usKeyLayoutForChar / cdpKeyInfo: US-layout key codes =====
|
|
168
|
+
// Regression: punctuation must NOT use charCodeAt() (".":46 collides with VK_DELETE,
|
|
169
|
+
// "-":45 with VK_INSERT), which made apps drop the char on keydown.
|
|
170
|
+
const { usKeyLayoutForChar, cdpKeyInfo } = sandbox;
|
|
171
|
+
const period = usKeyLayoutForChar(".");
|
|
172
|
+
ok(period.code === "Period" && period.keyCode === 190 && !period.needShift, "keylayout: '.' -> Period/190 (not 46)");
|
|
173
|
+
const dash = usKeyLayoutForChar("-");
|
|
174
|
+
ok(dash.code === "Minus" && dash.keyCode === 189, "keylayout: '-' -> Minus/189 (not 45)");
|
|
175
|
+
const slash = usKeyLayoutForChar("/");
|
|
176
|
+
ok(slash.code === "Slash" && slash.keyCode === 191, "keylayout: '/' -> Slash/191");
|
|
177
|
+
const at = usKeyLayoutForChar("@");
|
|
178
|
+
ok(at.code === "Digit2" && at.keyCode === 50 && at.needShift, "keylayout: '@' -> Digit2/50 + shift");
|
|
179
|
+
const A = usKeyLayoutForChar("A");
|
|
180
|
+
ok(A.code === "KeyA" && A.keyCode === 65 && A.needShift, "keylayout: 'A' -> KeyA/65 + shift");
|
|
181
|
+
const a = usKeyLayoutForChar("a");
|
|
182
|
+
ok(a.code === "KeyA" && a.keyCode === 65 && !a.needShift, "keylayout: 'a' -> KeyA/65 no shift");
|
|
183
|
+
const dot = cdpKeyInfo(".");
|
|
184
|
+
ok(dot.code === "Period" && dot.windowsVirtualKeyCode === 190 && dot.text === ".", "cdpKeyInfo: '.' -> Period/190 with text");
|
|
185
|
+
const ent = cdpKeyInfo("Enter");
|
|
186
|
+
ok(ent.code === "Enter" && ent.windowsVirtualKeyCode === 13, "cdpKeyInfo: named key 'Enter' unaffected");
|
|
187
|
+
|
|
167
188
|
console.log(`\n${passes} passed, ${failures} failed`);
|
|
168
189
|
if (failures) process.exit(1);
|
|
169
190
|
}
|