fix(coding-agent): keep ctrl+v as a paste key on Windows and WSL (#11780)

On Windows and WSL, `app.clipboard.pasteImage` was bound only to
`alt+v`. Most Windows terminals handle Ctrl+V themselves and send a
bracketed paste. Some terminals have no Ctrl+V paste action and pass
Ctrl+V to the program as the raw `0x16` key instead; Tern's embedded
terminal is one. In those terminals nothing in pi was bound to `ctrl+v`,
so pressing it did nothing and showed no message.

This change makes the Windows/WSL default `["alt+v", "ctrl+v"]`. `alt+v`
keeps working as before. In terminals that handle Ctrl+V themselves, pi
never receives the key, so the extra binding has no effect there. No
other default binding uses `ctrl+v`. `docs/keybindings.md` is updated to
match.

Testing:
- Added a test to `test/keybindings.test.ts`. It sets `WSL_DISTRO_NAME`,
re-imports the keybindings module, and checks that the paste defaults
are `["alt+v", "ctrl+v"]`. It fails without the change and passes with
it.
- Updated the existing defaults assertion in the same file to match the
new value.
- `node ../../node_modules/vitest/dist/cli.js --run
test/keybindings.test.ts`: 6 passed.
- `npm run check`: passes.

Fixes #11765

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-11-04 09:33:31 +01:00 • committed by GitHub
parent ea0f49a365
commit 37b38e9f65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 19 additions and 4 deletions

View file

@ -126,7 +126,7 @@ In fullscreen mode, these actions control the transcript and take precedence ove
| `app.exit` | `ctrl+d` | Exit (when editor empty) |
| `app.suspend` | `ctrl+z` (None on Windows) | Suspend to background |
| `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) |
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |
| `app.clipboard.pasteImage` | `ctrl+v` (`alt+v`, `ctrl+v` on Windows and WSL) | Paste files on macOS, images, or text from clipboard |
On native Windows, `app.suspend` has no default because Windows terminals do not support Unix job control. If you assign it manually, Pi shows a status message instead of suspending. WSL uses the normal `ctrl+z` and `fg` behavior.

View file

@ -73,7 +73,7 @@ const APP_KEYBINDINGS = {
description: "Restore queued messages",
},
"app.clipboard.pasteImage": {
defaultKeys: windowsKeybindings ? "alt+v" : "ctrl+v",
defaultKeys: windowsKeybindings ? ["alt+v", "ctrl+v"] : "ctrl+v",
description: "Paste files on macOS, images, or text from clipboard",
},
"app.session.new": { defaultKeys: [], description: "Start a new session" },

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { KEYBINDINGS, useWindowsKeybindings } from "../src/core/keybindings.ts";
describe("Windows keybinding defaults", () => {
@ -20,11 +20,26 @@ describe("Windows keybinding defaults", () => {
expect(useWindowsKeybindings("darwin", {})).toBe(false);
});
it("keeps ctrl+v as a paste key on WSL", async () => {
if (process.platform !== "linux") return;
vi.stubEnv("WSL_DISTRO_NAME", "Ubuntu");
vi.resetModules();
try {
const { KEYBINDINGS: wslKeybindings } = await import("../src/core/keybindings.ts");
expect(wslKeybindings["app.clipboard.pasteImage"].defaultKeys).toEqual(["alt+v", "ctrl+v"]);
} finally {
vi.unstubAllEnvs();
vi.resetModules();
}
});
it("applies the detected defaults consistently", () => {
const windowsKeybindings = useWindowsKeybindings();
const nativeWindows = process.platform === "win32";
expect(KEYBINDINGS["app.clipboard.pasteImage"].defaultKeys).toBe(windowsKeybindings ? "alt+v" : "ctrl+v");
expect(KEYBINDINGS["app.clipboard.pasteImage"].defaultKeys).toEqual(
windowsKeybindings ? ["alt+v", "ctrl+v"] : "ctrl+v",
);
expect(KEYBINDINGS["tui.altScreen.search"].defaultKeys).toBe(windowsKeybindings ? "ctrl+f" : "ctrl+shift+f");
expect(KEYBINDINGS["app.message.followUp"].defaultKeys).toBe(windowsKeybindings ? "ctrl+q" : "alt+enter");
expect(KEYBINDINGS["app.model.cycleBackward"].defaultKeys).toBe(windowsKeybindings ? "alt+p" : "shift+ctrl+p");