aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTianren Dong <tianrendong.dtr@gmail.com>2026-06-01 21:40:07 -0400
committerTianren Dong <tianrendong.dtr@gmail.com>2026-06-01 21:40:07 -0400
commit1c7cb9934275731de8f2fe950a0aa461637e5450 (patch)
treed59c70c90f2561777fc507b23402bff8ae9aa086
parentf7f6e2a7c6faf5da532a7486e820f45bdddce85d (diff)
0.15.34: tabs Pi uses join session group; fix punctuation dropped by chrome_type/chrome_key (US-layout keymap)
-rw-r--r--CHANGELOG.md5
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/manifest.json2
-rw-r--r--extensions/chrome-profile-bridge/browser-extension/service_worker.js68
-rw-r--r--extensions/chrome-profile-bridge/index.ts13
-rw-r--r--package.json2
-rw-r--r--test-suite/unit/csp-eval.test.mjs21
6 files changed, 95 insertions, 16 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc41221..60e185e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable user-facing changes to `pi-chrome`.
+## 0.15.34 — 2026-06-01
+
+- **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.
+- **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.
+
## 0.15.33 — 2026-05-31
- **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.
diff --git a/extensions/chrome-profile-bridge/browser-extension/manifest.json b/extensions/chrome-profile-bridge/browser-extension/manifest.json
index 71e872f..580b25f 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 Chrome Connector",
- "version": "0.15.33",
+ "version": "0.15.34",
"description": "Lets Pi control tabs in Chrome via a local connector at 127.0.0.1.",
"permissions": [
"tabs",
diff --git a/extensions/chrome-profile-bridge/browser-extension/service_worker.js b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
index eb54dce..9a51ad7 100644
--- a/extensions/chrome-profile-bridge/browser-extension/service_worker.js
+++ b/extensions/chrome-profile-bridge/browser-extension/service_worker.js
@@ -315,6 +315,38 @@ function cdpModifiersFor(mods) {
return m;
}
+// Resolve a single printable character to { code, keyCode, needShift } on a US layout.
+// Self-contained (maps defined inline) so it can be serialized into the page via
+// HELPER_FUNCS for the DOM-event fallback as well as used by the CDP path.
+// Using charCodeAt() for punctuation is wrong: e.g. "." is charCode 46 which collides
+// with VK_DELETE, "-" is 45 (VK_INSERT), so app keydown handlers misfire and drop input.
+function usKeyLayoutForChar(ch) {
+ const PUNCT = {
+ "`": { code: "Backquote", keyCode: 192 }, "~": { code: "Backquote", keyCode: 192, shift: true },
+ "-": { code: "Minus", keyCode: 189 }, "_": { code: "Minus", keyCode: 189, shift: true },
+ "=": { code: "Equal", keyCode: 187 }, "+": { code: "Equal", keyCode: 187, shift: true },
+ "[": { code: "BracketLeft", keyCode: 219 }, "{": { code: "BracketLeft", keyCode: 219, shift: true },
+ "]": { code: "BracketRight", keyCode: 221 }, "}": { code: "BracketRight", keyCode: 221, shift: true },
+ "\\": { code: "Backslash", keyCode: 220 }, "|": { code: "Backslash", keyCode: 220, shift: true },
+ ";": { code: "Semicolon", keyCode: 186 }, ":": { code: "Semicolon", keyCode: 186, shift: true },
+ "'": { code: "Quote", keyCode: 222 }, "\"": { code: "Quote", keyCode: 222, shift: true },
+ ",": { code: "Comma", keyCode: 188 }, "<": { code: "Comma", keyCode: 188, shift: true },
+ ".": { code: "Period", keyCode: 190 }, ">": { code: "Period", keyCode: 190, shift: true },
+ "/": { code: "Slash", keyCode: 191 }, "?": { code: "Slash", keyCode: 191, shift: true },
+ " ": { code: "Space", keyCode: 32 },
+ };
+ // Shifted digit symbols share the digit's physical code + keyCode.
+ const SHIFT_DIGIT = { ")": "0", "!": "1", "@": "2", "#": "3", "$": "4", "%": "5", "^": "6", "&": "7", "*": "8", "(": "9" };
+ if (/^[a-z]$/.test(ch)) return { code: `Key${ch.toUpperCase()}`, keyCode: ch.toUpperCase().charCodeAt(0), needShift: false };
+ if (/^[A-Z]$/.test(ch)) return { code: `Key${ch}`, keyCode: ch.charCodeAt(0), needShift: true };
+ if (/^[0-9]$/.test(ch)) return { code: `Digit${ch}`, keyCode: ch.charCodeAt(0), needShift: false };
+ if (SHIFT_DIGIT[ch]) { const d = SHIFT_DIGIT[ch]; return { code: `Digit${d}`, keyCode: d.charCodeAt(0), needShift: true }; }
+ const p = PUNCT[ch];
+ if (p) return { code: p.code, keyCode: p.keyCode, needShift: !!p.shift };
+ // Unknown char (e.g. unicode): keep text-driven insertion, avoid bogus keyCode collisions.
+ return { code: ch, keyCode: 0, needShift: false };
+}
+
function cdpKeyInfo(key, shifted) {
// Map common keys to CDP key event init fields. Returns { code, key, windowsVirtualKeyCode, text }.
const SPECIAL = {
@@ -336,11 +368,8 @@ function cdpKeyInfo(key, shifted) {
if (SPECIAL[key]) return { key, ...SPECIAL[key] };
if (key.length === 1) {
const ch = key;
- let code, vk;
- if (/^[a-zA-Z]$/.test(ch)) { code = `Key${ch.toUpperCase()}`; vk = ch.toUpperCase().charCodeAt(0); }
- else if (/^[0-9]$/.test(ch)) { code = `Digit${ch}`; vk = ch.charCodeAt(0); }
- else { code = ch; vk = ch.charCodeAt(0); }
- return { key: ch, code, windowsVirtualKeyCode: vk, text: ch };
+ const layout = usKeyLayoutForChar(ch);
+ return { key: ch, code: layout.code, windowsVirtualKeyCode: layout.keyCode, text: ch };
}
return { key, code: key, windowsVirtualKeyCode: 0, text: "" };
}
@@ -937,9 +966,27 @@ async function getTabByParams(params) {
if ((tab.url || "").startsWith("chrome://") || (tab.url || "").startsWith("chrome-extension://")) {
throw new Error(`Chrome blocks extension automation on protected URL: ${tab.url}`);
}
+ // Tabs Pi interacts with (page.* actions) join this session's group so the user can see exactly
+ // which tabs Pi is driving. We only adopt *ungrouped* tabs — never hijack a tab the user (or
+ // another Pi session) already grouped, since groupTab would otherwise rename that group.
+ if (params.joinSessionGroup && params.sessionGroupTitle) {
+ await joinSessionGroup(tab, params.sessionGroupTitle);
+ }
return tab;
}
+// Add an ungrouped tab to the session's tab group (reusing it by title, else creating it).
+// No-op when the tab is already grouped or tabGroups is unavailable.
+async function joinSessionGroup(tab, title) {
+ if (!chrome.tabGroups || typeof tab.id !== "number") return;
+ if (typeof tab.groupId === "number" && tab.groupId >= 0) return;
+ try {
+ await groupTab(tab, title);
+ } catch {
+ // Grouping is best-effort; never block the actual page action on a grouping failure.
+ }
+}
+
// Helper sources that get concatenated into the injected MAIN-world script. Kept as separate
// functions so callers below can reference them by `.toString()`. The helpers do not perform any
// eval themselves — they're plain function declarations.
@@ -961,6 +1008,7 @@ const HELPER_FUNCS = [
dispatchPointerLikeEvent,
humanMoveTo,
humanClickPoint,
+ usKeyLayoutForChar,
printableKeyCode,
dispatchKeyEvent,
typeCharacter,
@@ -1974,19 +2022,13 @@ function setNativeValue(element, value) {
}
function printableKeyCode(ch) {
- if (ch === " ") return 32;
- const upper = ch.toUpperCase();
- if (/^[A-Z]$/.test(upper)) return upper.charCodeAt(0);
- if (/^[0-9]$/.test(ch)) return ch.charCodeAt(0);
- return ch.charCodeAt(0) || 0;
+ return ch.length === 1 ? usKeyLayoutForChar(ch).keyCode : 0;
}
function dispatchKeyEvent(element, type, key, mods = {}) {
- const code = key.length === 1 && /^[a-z]$/i.test(key) ? `Key${key.toUpperCase()}` :
- key.length === 1 && /^[0-9]$/.test(key) ? `Digit${key}` :
- key === " " ? "Space" : key;
const SPECIAL = { Enter: 13, Tab: 9, Backspace: 8, Delete: 46, Escape: 27,
ArrowLeft: 37, ArrowUp: 38, ArrowRight: 39, ArrowDown: 40, " ": 32, Shift: 16, Control: 17, Alt: 18, Meta: 91 };
+ const code = key.length === 1 ? usKeyLayoutForChar(key).code : (key === " " ? "Space" : key);
const keyCode = key.length === 1 ? printableKeyCode(key) : (SPECIAL[key] ?? 0);
const ev = new KeyboardEvent(type, {
key,
diff --git a/extensions/chrome-profile-bridge/index.ts b/extensions/chrome-profile-bridge/index.ts
index ad8e495..b29a8e4 100644
--- a/extensions/chrome-profile-bridge/index.ts
+++ b/extensions/chrome-profile-bridge/index.ts
@@ -553,6 +553,8 @@ export default function (pi: ExtensionAPI): void {
let chromeAuthorizedUntil: number | "indefinite" | undefined;
let chromeToolsRegistered = false;
let authExpiryTimer: NodeJS.Timeout | undefined;
+ // Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
+ let sessionCtx: ExtensionContext | undefined;
const clearAuthExpiryTimer = (): void => {
if (!authExpiryTimer) return;
@@ -632,7 +634,15 @@ export default function (pi: ExtensionAPI): void {
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
requireChromeControlAuthorized();
- return bridge.send(action, params, timeoutMs, signal);
+ // Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the
+ // auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull
+ // the resolved (e.g. active) tab into the session group on the service-worker side. We skip
+ // tab.* actions: tab.new/group group explicitly, and activate/close/ungroup/list must not.
+ const shouldJoinGroup = action.startsWith("page.") && sessionCtx !== undefined && params.sessionGroupTitle === undefined;
+ const wireParams = shouldJoinGroup
+ ? { ...params, sessionGroupTitle: sessionGroupTitle(sessionCtx as ExtensionContext), joinSessionGroup: true }
+ : params;
+ return bridge.send(action, wireParams, timeoutMs, signal);
};
// Translate the public `background` parameter (default on = silent/background) into the
@@ -650,6 +660,7 @@ export default function (pi: ExtensionAPI): void {
};
pi.on("session_start", async (_event, ctx) => {
+ sessionCtx = ctx;
await bridge.start();
updateChromeStatus(ctx);
});
diff --git a/package.json b/package.json
index bd4d517..830208a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pi-chrome",
- "version": "0.15.33",
+ "version": "0.15.34",
"scripts": {
"test": "node test-suite/unit/csp-eval.test.mjs",
"version": "node scripts/sync-manifest-version.js",
diff --git a/test-suite/unit/csp-eval.test.mjs b/test-suite/unit/csp-eval.test.mjs
index 24101fb..69f7943 100644
--- a/test-suite/unit/csp-eval.test.mjs
+++ b/test-suite/unit/csp-eval.test.mjs
@@ -164,6 +164,27 @@ async function run() {
"waitFor: missing selector times out",
);
+ // ===== usKeyLayoutForChar / cdpKeyInfo: US-layout key codes =====
+ // Regression: punctuation must NOT use charCodeAt() (".":46 collides with VK_DELETE,
+ // "-":45 with VK_INSERT), which made apps drop the char on keydown.
+ const { usKeyLayoutForChar, cdpKeyInfo } = sandbox;
+ const period = usKeyLayoutForChar(".");
+ ok(period.code === "Period" && period.keyCode === 190 && !period.needShift, "keylayout: '.' -> Period/190 (not 46)");
+ const dash = usKeyLayoutForChar("-");
+ ok(dash.code === "Minus" && dash.keyCode === 189, "keylayout: '-' -> Minus/189 (not 45)");
+ const slash = usKeyLayoutForChar("/");
+ ok(slash.code === "Slash" && slash.keyCode === 191, "keylayout: '/' -> Slash/191");
+ const at = usKeyLayoutForChar("@");
+ ok(at.code === "Digit2" && at.keyCode === 50 && at.needShift, "keylayout: '@' -> Digit2/50 + shift");
+ const A = usKeyLayoutForChar("A");
+ ok(A.code === "KeyA" && A.keyCode === 65 && A.needShift, "keylayout: 'A' -> KeyA/65 + shift");
+ const a = usKeyLayoutForChar("a");
+ ok(a.code === "KeyA" && a.keyCode === 65 && !a.needShift, "keylayout: 'a' -> KeyA/65 no shift");
+ const dot = cdpKeyInfo(".");
+ ok(dot.code === "Period" && dot.windowsVirtualKeyCode === 190 && dot.text === ".", "cdpKeyInfo: '.' -> Period/190 with text");
+ const ent = cdpKeyInfo("Enter");
+ ok(ent.code === "Enter" && ent.windowsVirtualKeyCode === 13, "cdpKeyInfo: named key 'Enter' unaffected");
+
console.log(`\n${passes} passed, ${failures} failed`);
if (failures) process.exit(1);
}