Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### Pasting into a Bot's browser works on a layout that does not write Latin letters

While somebody drives a Bot's browser, Ctrl+V or Cmd+V is left to the local page so its paste event
can send the clipboard text across. The shortcut was recognised by the character the key writes,
and on a Russian or Greek layout the V key writes "м" or "ω", so the keystroke went to the Bot's
browser instead and nothing was pasted. The V is now read the way the app's own shortcuts read it
since Shift+N was fixed for the same layouts: from the physical key when the layout writes a
character outside ASCII there.
### Reading a large file from Google Drive no longer downloads all of it

`read_file_content` shows a Bot at most the first 20,000 characters of a file, but it downloaded the
Expand Down
10 changes: 8 additions & 2 deletions app/src/components/computer/live-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { keyOf } from "@/lib/hotkeys/hotkeys";
import { socketUrl } from "@/lib/socket-url";
import { currentPageVisible } from "./preview-visibility";
import { pageCoordinates } from "./take-the-wheel";
Expand Down Expand Up @@ -33,9 +34,14 @@ function modifierBits(event: {
);
}

/** Let the local browser create a paste event, whose clipboard text is forwarded separately. */
/**
* Let the local browser create a paste event, whose clipboard text is forwarded separately.
*
* The V is read the way a shortcut is (`keyOf`), so a layout that writes another script still has
* one: Ctrl and the V key report "м" on Russian and "ω" on Greek.
*/
function isPasteShortcut(event: KeyboardEvent): boolean {
return (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v";
return (event.ctrlKey || event.metaKey) && keyOf(event) === "v";
}

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/hotkeys/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const isMac =
* Latin N) on Greek, so a shortcut read from `key` alone never fired there. When `key` is a single
* character outside ASCII, the physical key in `code` is the only N there is.
*/
function keyOf(event: KeyboardEvent): string {
export function keyOf(event: KeyboardEvent): string {
const written = event.key.toLowerCase();
if (written.length !== 1 || written.charCodeAt(0) < 0x80) return written;
const physical = /^(?:Key|Digit)([A-Z0-9])$/.exec(event.code);
Expand Down
75 changes: 75 additions & 0 deletions app/tests/live-screen-keyboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,81 @@ for (const [name, modifier] of [
});
}

/*
* A layout that writes another script has no key that writes a V. Ctrl and the V key report "м" on
* Russian and "ω" on Greek, with `code` still `KeyV`, so reading `key` alone sent the shortcut to the
* remote browser and stopped the local paste event that carries the clipboard text.
*/
for (const [layout, written] of [
["Russian", "м"],
["Greek", "ω"],
] as const) {
test(`Ctrl+V on a ${layout} layout stays in the local page too`, async () => {
const socket = await liveSocket();
const shortcut = new KeyboardEvent("keydown", {
key: written,
code: "KeyV",
keyCode: 86,
ctrlKey: true,
bubbles: true,
cancelable: true,
});

window.dispatchEvent(shortcut);
window.dispatchEvent(
new KeyboardEvent("keyup", {
key: written,
code: "KeyV",
keyCode: 86,
bubbles: true,
cancelable: true,
}),
);

expect(socket.sent).toEqual([]);
expect(shortcut.defaultPrevented).toBe(false);
});
}

test("Ctrl on a key that writes V on Dvorak is still paste, and Ctrl on the QWERTY V key is not", async () => {
const socket = await liveSocket();
// On Dvorak the key that writes V is the one QWERTY calls Period.
const paste = new KeyboardEvent("keydown", {
key: "v",
code: "Period",
keyCode: 86,
ctrlKey: true,
bubbles: true,
cancelable: true,
});
// And the key QWERTY calls V writes K.
const other = new KeyboardEvent("keydown", {
key: "k",
code: "KeyV",
keyCode: 75,
ctrlKey: true,
bubbles: true,
cancelable: true,
});

window.dispatchEvent(paste);
window.dispatchEvent(other);

expect(paste.defaultPrevented).toBe(false);
expect(other.defaultPrevented).toBe(true);
expect(socket.sent).toEqual([
{
type: "key",
event: "down",
key: "k",
code: "KeyV",
text: "k",
windowsVirtualKeyCode: 75,
modifiers: 2,
},
]);
});

test("a paste keyup stays local when the modifier was released first", async () => {
const socket = await liveSocket();
window.dispatchEvent(
Expand Down
Loading