Skip to content

Commit e2b011f

Browse files
authored
Merge pull request #22 from devswha/fix/browser-panel-keys
fix(browser): forward editing and navigation keys through the remote keyboard
2 parents 5d21c8c + da686a0 commit e2b011f

3 files changed

Lines changed: 78 additions & 16 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import assert from 'node:assert/strict';
2+
import test from 'node:test';
3+
4+
import { toPuppeteerKeyInput } from './browser-sidecar.js';
5+
6+
test('maps editing and navigation keys to Puppeteer key inputs', () => {
7+
for (const key of [
8+
'Backspace', 'Delete', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown',
9+
'Home', 'End', 'PageUp', 'PageDown', 'Tab', 'Enter', 'Escape', 'Shift',
10+
'Control', 'Alt', 'Meta',
11+
]) {
12+
assert.equal(toPuppeteerKeyInput(key), key);
13+
}
14+
});
15+
16+
test('maps DOM key aliases and printable characters to Puppeteer key inputs', () => {
17+
assert.equal(toPuppeteerKeyInput('OS'), 'Meta');
18+
assert.equal(toPuppeteerKeyInput('Esc'), 'Escape');
19+
assert.equal(toPuppeteerKeyInput(' '), 'Space');
20+
assert.equal(toPuppeteerKeyInput('a'), 'a');
21+
assert.equal(toPuppeteerKeyInput('7'), '7');
22+
});
23+
24+
test('maps function keys and rejects IME keys', () => {
25+
for (let index = 1; index <= 12; index += 1) {
26+
assert.equal(toPuppeteerKeyInput(`F${index}`), `F${index}`);
27+
}
28+
for (const key of ['Dead', 'Unidentified', 'Process']) {
29+
assert.equal(toPuppeteerKeyInput(key), null);
30+
}
31+
});

server/modules/automation/browser-sidecar.ts

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { mkdir } from 'node:fs/promises';
66
import { homedir } from 'node:os';
77
import { join } from 'node:path';
88
import readline from 'node:readline';
9+
import { pathToFileURL } from 'node:url';
910

1011
import {
1112
Browser as BrowserBinary,
@@ -46,6 +47,24 @@ import {
4647
} from './browser-protocol.js';
4748
import { normalizeAutomationUrl } from './automation-url.js';
4849

50+
const PUPPETEER_KEY_NAMES = new Set([
51+
'Backspace', 'Tab', 'Enter', 'Escape', 'Shift', 'Control', 'Alt', 'Meta',
52+
'CapsLock', 'Delete', 'Insert', 'Home', 'End', 'PageUp', 'PageDown',
53+
'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', 'NumLock', 'ScrollLock',
54+
'Pause', 'PrintScreen', 'ContextMenu',
55+
]);
56+
57+
export function toPuppeteerKeyInput(key: string, code?: string): string | null {
58+
void code;
59+
if (key === 'OS') return 'Meta';
60+
if (key === 'Scroll') return 'ScrollLock';
61+
if (key === 'Esc') return 'Escape';
62+
if (key === 'Del') return 'Delete';
63+
if (key === ' ') return 'Space';
64+
if (PUPPETEER_KEY_NAMES.has(key) || /^F(?:[1-9]|1[0-2])$/.test(key) || key.length === 1) return key;
65+
return null;
66+
}
67+
4968
type Tab = {
5069
id: string;
5170
page: Page;
@@ -335,12 +354,18 @@ class BrowserRuntime {
335354
} else if (input.kind === 'text') {
336355
await cdp.send('Input.insertText', { text: input.text });
337356
} else {
338-
await cdp.send('Input.dispatchKeyEvent', {
339-
type: input.event === 'down' ? 'keyDown' : 'keyUp',
340-
key: input.key,
341-
code: input.code ?? input.key,
342-
modifiers: input.modifiers ?? 0,
343-
});
357+
const keyInput = toPuppeteerKeyInput(input.key, input.code);
358+
if (keyInput) {
359+
if (input.event === 'down') await tab.page.keyboard.down(keyInput as KeyInput);
360+
else await tab.page.keyboard.up(keyInput as KeyInput);
361+
} else {
362+
await cdp.send('Input.dispatchKeyEvent', {
363+
type: input.event === 'down' ? 'keyDown' : 'keyUp',
364+
key: input.key,
365+
code: input.code ?? input.key,
366+
modifiers: input.modifiers ?? 0,
367+
});
368+
}
344369
}
345370
return { accepted: true };
346371
}
@@ -770,15 +795,19 @@ function enqueue(frame: BrowserRequestFrame): void {
770795
globalRequestQueue = globalRequestQueue.then(() => handle(frame)).catch(reportQueueError);
771796
}
772797

773-
readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', (line) => {
774-
try {
775-
for (const frame of decoder.push(`${line}\n`)) {
776-
if (frame.kind !== 'request') throw new Error('Only request frames are accepted.');
777-
enqueue(frame);
798+
function runBrowserSidecarEntrypoint(): void {
799+
readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', (line) => {
800+
try {
801+
for (const frame of decoder.push(`${line}\n`)) {
802+
if (frame.kind !== 'request') throw new Error('Only request frames are accepted.');
803+
enqueue(frame);
804+
}
805+
} catch (error) {
806+
reportQueueError(error);
778807
}
779-
} catch (error) {
780-
reportQueueError(error);
781-
}
782-
});
808+
});
809+
810+
emit('ready', { protocolVersion: BROWSER_PROTOCOL_VERSION });
811+
}
783812

784-
emit('ready', { protocolVersion: BROWSER_PROTOCOL_VERSION });
813+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) runBrowserSidecarEntrypoint();

src/components/workspace/view/BrowserPanel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ export default function BrowserPanel({ sessionId, navigationRequest, onNavigatio
314314
};
315315

316316
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
317+
if (event.nativeEvent.isComposing) return;
317318
if (event.metaKey || event.ctrlKey || event.altKey || event.key.length !== 1) {
318319
event.preventDefault();
319320
sendInput({ kind: 'key', event: 'down', key: event.key, code: event.code, modifiers: (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0) });
@@ -469,6 +470,7 @@ export default function BrowserPanel({ sessionId, navigationRequest, onNavigatio
469470
});
470471
}}
471472
onPointerDown={(event) => {
473+
surfaceRef.current?.focus();
472474
const point = framePoint(event);
473475
if (!point) return;
474476
event.currentTarget.setPointerCapture(event.pointerId);

0 commit comments

Comments
 (0)