pi-chrome 0.15.32 → 0.15.34
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 +9 -0
- package/README.md +3 -3
- 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 +33 -22
- package/package.json +1 -1
- package/test-suite/unit/csp-eval.test.mjs +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.15.33 — 2026-05-31
|
|
11
|
+
|
|
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.
|
|
13
|
+
|
|
5
14
|
## 0.15.32 — 2026-05-31
|
|
6
15
|
|
|
7
16
|
- **Session tab-group naming tweak.** Per-session groups are now titled `Pi Session: <name-or-id>` (added a colon separator).
|
package/README.md
CHANGED
|
@@ -176,15 +176,15 @@ This protects your signed-in Chrome profile from accidental agent use. The loopb
|
|
|
176
176
|
|
|
177
177
|
### Run in background / watch modes
|
|
178
178
|
|
|
179
|
-
By default, every `chrome_*` call
|
|
179
|
+
By default, every `chrome_*` call runs in the **background** — Chrome won't pop up or steal focus. Switch to foreground when you want to **watch the agent work** (demos, debugging, first-time confidence).
|
|
180
180
|
|
|
181
181
|
```text
|
|
182
182
|
/chrome background # toggle for the whole session
|
|
183
|
-
/chrome background on # run in background
|
|
183
|
+
/chrome background on # run in background (default)
|
|
184
184
|
/chrome background off # bring Chrome forward so you can watch
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
Per-call `background:
|
|
187
|
+
Per-call `background: false` wins over the session setting (and `background: true` forces background).
|
|
188
188
|
|
|
189
189
|
### Diagnostics
|
|
190
190
|
|
|
@@ -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,
|
|
@@ -549,10 +549,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
549
549
|
globalState[PI_CHROME_GLOBAL_KEY] = { version: PI_CHROME_VERSION, root: currentRoot, token: instanceToken };
|
|
550
550
|
|
|
551
551
|
const bridge = new ChromeProfileBridge(DEFAULT_HOST, DEFAULT_PORT);
|
|
552
|
-
let backgroundDefault =
|
|
552
|
+
let backgroundDefault = true;
|
|
553
553
|
let chromeAuthorizedUntil: number | "indefinite" | undefined;
|
|
554
554
|
let chromeToolsRegistered = false;
|
|
555
555
|
let authExpiryTimer: NodeJS.Timeout | undefined;
|
|
556
|
+
// Remembered so bridge sends can tag tabs with this session's group even when ctx isn't handy.
|
|
557
|
+
let sessionCtx: ExtensionContext | undefined;
|
|
556
558
|
|
|
557
559
|
const clearAuthExpiryTimer = (): void => {
|
|
558
560
|
if (!authExpiryTimer) return;
|
|
@@ -632,10 +634,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
632
634
|
|
|
633
635
|
const authorizedBridgeSend = (action: string, params: Record<string, unknown>, timeoutMs = DEFAULT_TIMEOUT_MS, signal?: AbortSignal): Promise<unknown> => {
|
|
634
636
|
requireChromeControlAuthorized();
|
|
635
|
-
|
|
637
|
+
// Any tab Pi *uses* (page.* interactions) should join this session's group, mirroring the
|
|
638
|
+
// auto-grouping that tab.new already does. Tagging the wire params lets getTabByParams pull
|
|
639
|
+
// the resolved (e.g. active) tab into the session group on the service-worker side. We skip
|
|
640
|
+
// tab.* actions: tab.new/group group explicitly, and activate/close/ungroup/list must not.
|
|
641
|
+
const shouldJoinGroup = action.startsWith("page.") && sessionCtx !== undefined && params.sessionGroupTitle === undefined;
|
|
642
|
+
const wireParams = shouldJoinGroup
|
|
643
|
+
? { ...params, sessionGroupTitle: sessionGroupTitle(sessionCtx as ExtensionContext), joinSessionGroup: true }
|
|
644
|
+
: params;
|
|
645
|
+
return bridge.send(action, wireParams, timeoutMs, signal);
|
|
636
646
|
};
|
|
637
647
|
|
|
638
|
-
// Translate the public `background` parameter (default
|
|
648
|
+
// Translate the public `background` parameter (default on = silent/background) into the
|
|
639
649
|
// service worker's wire-level `foreground` flag, accepting legacy `foreground` as a fallback.
|
|
640
650
|
const withBackground = <T extends Record<string, unknown>>(params: T): T => {
|
|
641
651
|
const typed = params as { background?: boolean; foreground?: boolean };
|
|
@@ -650,6 +660,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
650
660
|
};
|
|
651
661
|
|
|
652
662
|
pi.on("session_start", async (_event, ctx) => {
|
|
663
|
+
sessionCtx = ctx;
|
|
653
664
|
await bridge.start();
|
|
654
665
|
updateChromeStatus(ctx);
|
|
655
666
|
});
|
|
@@ -682,7 +693,7 @@ Usage rules:
|
|
|
682
693
|
3. \`includeSnapshot=true\` on click/type/fill/key to verify in one round trip.
|
|
683
694
|
4. If \`chrome_evaluate\` returns null when you expected a value, the expression evaluated to null/undefined in the page; surface the value via \`JSON.stringify\` to confirm.
|
|
684
695
|
5. \`chrome_navigate\` supports an optional \`initScript\` that runs at document_start in MAIN world for the next navigation (good for seeding localStorage or stubbing Date.now).
|
|
685
|
-
6. By default chrome_* tools
|
|
696
|
+
6. By default chrome_* tools run in the background without focusing Chrome; pass \`background=false\` or run /chrome background off when the user wants to watch Chrome work.
|
|
686
697
|
7. If you hit a native file-picker or privileged browser prompt gate, tell the user; generic clicks/typing/CSP gates are handled by Chrome input.
|
|
687
698
|
8. Run /chrome doctor when in doubt about connectivity or capabilities.
|
|
688
699
|
</chrome-profile-bridge>`;
|
|
@@ -953,8 +964,8 @@ Usage rules:
|
|
|
953
964
|
];
|
|
954
965
|
} else if (path[0] === "background" && path.length === 1) {
|
|
955
966
|
candidates = [
|
|
956
|
-
{ fullValue: "background on", label: "on", description: "Run in background. Chrome stays in the background. Your editor keeps focus." },
|
|
957
|
-
{ fullValue: "background off", label: "off", description: "Bring Chrome to the front so you can watch
|
|
967
|
+
{ fullValue: "background on", label: "on", description: "Run in background. Chrome stays in the background. Your editor keeps focus. (default)" },
|
|
968
|
+
{ fullValue: "background off", label: "off", description: "Bring Chrome to the front so you can watch." },
|
|
958
969
|
{ fullValue: "background toggle", label: "toggle", description: "Flip whichever way it's currently set." },
|
|
959
970
|
{ fullValue: "background status", label: "status", description: "Show the current setting." },
|
|
960
971
|
];
|
|
@@ -1073,7 +1084,7 @@ Usage rules:
|
|
|
1073
1084
|
name: "chrome_snapshot",
|
|
1074
1085
|
label: "Chrome Snapshot",
|
|
1075
1086
|
description:
|
|
1076
|
-
"Inspect a page in the user's existing Chrome profile: title, URL, visible body text, viewport, and clickable/focusable elements with stable uids plus CSS selectors.
|
|
1087
|
+
"Inspect a page in the user's existing Chrome profile: title, URL, visible body text, viewport, and clickable/focusable elements with stable uids plus CSS selectors. Runs in the background by default; pass background=false to bring Chrome to the foreground so the user can watch.",
|
|
1077
1088
|
promptSnippet: "Inspect the current Chrome page and get CSS selectors for browser automation.",
|
|
1078
1089
|
parameters: Type.Object({
|
|
1079
1090
|
targetId: Type.Optional(Type.String()),
|
|
@@ -1084,7 +1095,7 @@ Usage rules:
|
|
|
1084
1095
|
roleFilter: Type.Optional(Type.String({ description: "Only return elements matching this ARIA role or tag name (case-insensitive). e.g. 'button', 'link', 'textbox'." })),
|
|
1085
1096
|
nearUid: Type.Optional(Type.String({ description: "Sort elements by proximity to this snapshot uid. Useful for finding controls near a known anchor." })),
|
|
1086
1097
|
background: Type.Optional(
|
|
1087
|
-
Type.Boolean({ description: "If true, run silently in the background without focusing Chrome
|
|
1098
|
+
Type.Boolean({ description: "If true (the default), run silently in the background without focusing Chrome; pass false so Chrome focuses + the tab activates and the user can watch." }),
|
|
1088
1099
|
),
|
|
1089
1100
|
host: Type.Optional(Type.String()),
|
|
1090
1101
|
port: Type.Optional(Type.Number()),
|
|
@@ -1104,7 +1115,7 @@ Usage rules:
|
|
|
1104
1115
|
name: "chrome_navigate",
|
|
1105
1116
|
label: "Chrome Navigate",
|
|
1106
1117
|
description:
|
|
1107
|
-
"Navigate an existing Chrome tab to a URL via the companion extension.
|
|
1118
|
+
"Navigate an existing Chrome tab to a URL via the companion extension. Runs in the background by default; pass background=false to focus Chrome and activate the tab so the user can watch. Optionally waits for load completion.",
|
|
1108
1119
|
promptSnippet: "Navigate a Chrome tab in the user's existing profile.",
|
|
1109
1120
|
parameters: Type.Object({
|
|
1110
1121
|
url: Type.String(),
|
|
@@ -1115,7 +1126,7 @@ Usage rules:
|
|
|
1115
1126
|
timeoutMs: Type.Optional(Type.Number({ default: 15_000 })),
|
|
1116
1127
|
initScript: Type.Optional(Type.String({ description: "Optional JavaScript source to run in MAIN world at document_start of the next navigation. Useful for seeding localStorage, stubbing Date.now(), or defining navigator.webdriver=undefined. Requires the companion extension's webNavigation permission." })),
|
|
1117
1128
|
background: Type.Optional(
|
|
1118
|
-
Type.Boolean({ description: "If true, navigate silently without focusing Chrome.
|
|
1129
|
+
Type.Boolean({ description: "If true, navigate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1119
1130
|
),
|
|
1120
1131
|
host: Type.Optional(Type.String()),
|
|
1121
1132
|
port: Type.Optional(Type.Number()),
|
|
@@ -1130,7 +1141,7 @@ Usage rules:
|
|
|
1130
1141
|
name: "chrome_evaluate",
|
|
1131
1142
|
label: "Chrome Evaluate",
|
|
1132
1143
|
description:
|
|
1133
|
-
"Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context and returns JSON-serializable values when possible.
|
|
1144
|
+
"Evaluate JavaScript in an existing Chrome tab through the companion extension. Runs in the page context and returns JSON-serializable values when possible. Runs in the background by default; pass background=false to focus Chrome and activate the tab.",
|
|
1134
1145
|
promptSnippet: "Evaluate JavaScript in the active Chrome tab through the companion extension.",
|
|
1135
1146
|
parameters: Type.Object({
|
|
1136
1147
|
expression: Type.String(),
|
|
@@ -1139,7 +1150,7 @@ Usage rules:
|
|
|
1139
1150
|
urlIncludes: Type.Optional(Type.String()),
|
|
1140
1151
|
titleIncludes: Type.Optional(Type.String()),
|
|
1141
1152
|
background: Type.Optional(
|
|
1142
|
-
Type.Boolean({ description: "If true, evaluate silently without focusing Chrome.
|
|
1153
|
+
Type.Boolean({ description: "If true, evaluate silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1143
1154
|
),
|
|
1144
1155
|
host: Type.Optional(Type.String()),
|
|
1145
1156
|
port: Type.Optional(Type.Number()),
|
|
@@ -1172,7 +1183,7 @@ Usage rules:
|
|
|
1172
1183
|
urlIncludes: Type.Optional(Type.String()),
|
|
1173
1184
|
titleIncludes: Type.Optional(Type.String()),
|
|
1174
1185
|
background: Type.Optional(
|
|
1175
|
-
Type.Boolean({ description: "If true, click silently without focusing Chrome.
|
|
1186
|
+
Type.Boolean({ description: "If true, click silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1176
1187
|
),
|
|
1177
1188
|
host: Type.Optional(Type.String()),
|
|
1178
1189
|
port: Type.Optional(Type.Number()),
|
|
@@ -1204,7 +1215,7 @@ Usage rules:
|
|
|
1204
1215
|
urlIncludes: Type.Optional(Type.String()),
|
|
1205
1216
|
titleIncludes: Type.Optional(Type.String()),
|
|
1206
1217
|
background: Type.Optional(
|
|
1207
|
-
Type.Boolean({ description: "If true, type silently without focusing Chrome.
|
|
1218
|
+
Type.Boolean({ description: "If true, type silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1208
1219
|
),
|
|
1209
1220
|
host: Type.Optional(Type.String()),
|
|
1210
1221
|
port: Type.Optional(Type.Number()),
|
|
@@ -1236,7 +1247,7 @@ Usage rules:
|
|
|
1236
1247
|
urlIncludes: Type.Optional(Type.String()),
|
|
1237
1248
|
titleIncludes: Type.Optional(Type.String()),
|
|
1238
1249
|
background: Type.Optional(
|
|
1239
|
-
Type.Boolean({ description: "If true, fill silently without focusing Chrome.
|
|
1250
|
+
Type.Boolean({ description: "If true, fill silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1240
1251
|
),
|
|
1241
1252
|
host: Type.Optional(Type.String()),
|
|
1242
1253
|
port: Type.Optional(Type.Number()),
|
|
@@ -1255,7 +1266,7 @@ Usage rules:
|
|
|
1255
1266
|
name: "chrome_key",
|
|
1256
1267
|
label: "Chrome Key",
|
|
1257
1268
|
description:
|
|
1258
|
-
"Send a keyboard key to an existing Chrome tab (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right, or one character).
|
|
1269
|
+
"Send a keyboard key to an existing Chrome tab (Enter, Escape, Tab, Backspace, Delete, ArrowUp/Down/Left/Right, or one character). Runs in the background by default; pass background=false to focus Chrome and activate the tab so the user can watch. Pass includeSnapshot=true to verify after the keypress.",
|
|
1259
1270
|
promptSnippet: "Press keys in Chrome through the companion extension.",
|
|
1260
1271
|
parameters: Type.Object({
|
|
1261
1272
|
key: Type.String(),
|
|
@@ -1271,7 +1282,7 @@ Usage rules:
|
|
|
1271
1282
|
urlIncludes: Type.Optional(Type.String()),
|
|
1272
1283
|
titleIncludes: Type.Optional(Type.String()),
|
|
1273
1284
|
background: Type.Optional(
|
|
1274
|
-
Type.Boolean({ description: "If true, send the key silently without focusing Chrome.
|
|
1285
|
+
Type.Boolean({ description: "If true, send the key silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." }),
|
|
1275
1286
|
),
|
|
1276
1287
|
host: Type.Optional(Type.String()),
|
|
1277
1288
|
port: Type.Optional(Type.Number()),
|
|
@@ -1318,7 +1329,7 @@ Usage rules:
|
|
|
1318
1329
|
targetId: Type.Optional(Type.String()),
|
|
1319
1330
|
urlIncludes: Type.Optional(Type.String()),
|
|
1320
1331
|
titleIncludes: Type.Optional(Type.String()),
|
|
1321
|
-
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome.
|
|
1332
|
+
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })),
|
|
1322
1333
|
host: Type.Optional(Type.String()),
|
|
1323
1334
|
port: Type.Optional(Type.Number()),
|
|
1324
1335
|
}),
|
|
@@ -1340,7 +1351,7 @@ Usage rules:
|
|
|
1340
1351
|
targetId: Type.Optional(Type.String()),
|
|
1341
1352
|
urlIncludes: Type.Optional(Type.String()),
|
|
1342
1353
|
titleIncludes: Type.Optional(Type.String()),
|
|
1343
|
-
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome.
|
|
1354
|
+
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })),
|
|
1344
1355
|
host: Type.Optional(Type.String()),
|
|
1345
1356
|
port: Type.Optional(Type.Number()),
|
|
1346
1357
|
}),
|
|
@@ -1360,7 +1371,7 @@ Usage rules:
|
|
|
1360
1371
|
targetId: Type.Optional(Type.String()),
|
|
1361
1372
|
urlIncludes: Type.Optional(Type.String()),
|
|
1362
1373
|
titleIncludes: Type.Optional(Type.String()),
|
|
1363
|
-
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome.
|
|
1374
|
+
background: Type.Optional(Type.Boolean({ description: "If true, run silently without focusing Chrome. Defaults to on (the session background setting); pass false to focus Chrome so the user can watch." })),
|
|
1364
1375
|
host: Type.Optional(Type.String()),
|
|
1365
1376
|
port: Type.Optional(Type.Number()),
|
|
1366
1377
|
}),
|
|
@@ -1374,7 +1385,7 @@ Usage rules:
|
|
|
1374
1385
|
name: "chrome_screenshot",
|
|
1375
1386
|
label: "Chrome Screenshot",
|
|
1376
1387
|
description:
|
|
1377
|
-
"Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk. Chrome's extension screenshot API requires the target tab to be the active tab in its window.
|
|
1388
|
+
"Capture a screenshot of an existing Chrome tab via the companion extension and save it to disk. Chrome's extension screenshot API requires the target tab to be the active tab in its window. Runs in the background by default (the tab is briefly activated within its window for the capture, then the previous active tab is restored); pass background=false to focus Chrome so the user can watch.",
|
|
1378
1389
|
promptSnippet: "Capture Chrome screenshots and save them under .pi/chrome-screenshots by default.",
|
|
1379
1390
|
parameters: Type.Object({
|
|
1380
1391
|
path: Type.Optional(Type.String({ description: "Output path. Defaults to .pi/chrome-screenshots/<timestamp>.<format>." })),
|
|
@@ -1385,7 +1396,7 @@ Usage rules:
|
|
|
1385
1396
|
urlIncludes: Type.Optional(Type.String()),
|
|
1386
1397
|
titleIncludes: Type.Optional(Type.String()),
|
|
1387
1398
|
background: Type.Optional(
|
|
1388
|
-
Type.Boolean({ description: "If true, capture silently without focusing the Chrome window (the target tab is briefly activated within its window for the capture, then restored)
|
|
1399
|
+
Type.Boolean({ description: "If true (the default), capture silently without focusing the Chrome window (the target tab is briefly activated within its window for the capture, then restored); pass false to focus Chrome." }),
|
|
1389
1400
|
),
|
|
1390
1401
|
host: Type.Optional(Type.String()),
|
|
1391
1402
|
port: Type.Optional(Type.Number()),
|
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
|
}
|