Ctrl+V in the terminal inserted the clipboard text twice. Right-click →
Paste inserted it once.
`_handleImagePaste()` appends a hidden contenteditable div, focuses it, and
reads the clipboard out of the paste event that lands there. Two separate
routes deliver that event for a single keypress. The function issues
`document.execCommand('paste')` itself, which in Firefox dispatches a
trusted paste event and then returns false, because the trap cancels the
event and the command never completes; Chromium and WebKit refuse that
command and dispatch nothing. The keydown's own default action delivers the
other, because xterm calls the custom key handler before its own `cancel()`,
so returning false never calls preventDefault. Firefox therefore ran the
trap's listener twice and both runs reached `terminal.paste()`. The
context-menu paste involves no keydown at all, which is why that path stayed
correct.
The trap now accepts the first paste event and cancels every later one, so
how many paste events a browser delivers no longer changes what the PTY
sees. Measured on a live install, one Ctrl+V each: Firefox two events and
two writes before this change, Chromium and WebKit one and one, and every
engine one write after it.
The `execCommand('paste')` call stays. Stripping it out also ends the
doubling, and all three engines still deliver one event without it, since
`trap.focus()` has already run when the key's default action resolves. It is
kept because the trap technique arrived in #84 for plain HTTP and for
mobile, and a desktop measurement says nothing about real iOS Safari or
Android Chrome: where a browser aims the default action at the element
focused when the keydown began, the command is the only route into the trap,
and the trap is the only place clipboard image blobs are read.
test/image-paste-trap.test.ts loads image-input.js into a `node:vm` context
with a fake document and fires two paste events at the trap. It covers text
and images, and fails on the old code with the text pasted twice and the
image uploaded twice.
Docs: the invariant goes into docs/architecture-invariants.md as a Terminal
paste section and into CLAUDE.md as a Frontend entry, both recording the
measured event counts and why the redundant call is still there. README.md
and the Keyboard Shortcuts and Input and Voice wiki pages gain a Ctrl+V row,
which all three tables were missing while listing every other clipboard
binding.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3.9 KiB
Keyboard Shortcuts
Every binding, and how to change them. Ctrl also accepts Cmd on macOS.
Press Ctrl+? in the app for the same list in a floating overlay.
Sessions and tabs
| Shortcut | Action |
|---|---|
Ctrl+K (also Cmd+K, Alt+K) |
Find an open session or start a new one. |
Ctrl+W |
Kill the active session. |
Ctrl+Tab |
Next session. |
Alt+[ / Alt+] |
Previous / next tab. |
Alt+1 to Alt+9 |
Switch to tab N. Physical keys, so macOS Option layouts work. |
Ctrl+Shift+{ / Ctrl+Shift+} |
Move the active tab left / right. |
Alt+B |
Collapse / expand the session sidebar, when that layout is on. |
Terminal
| Shortcut | Action |
|---|---|
Enter |
Send. |
Shift+Enter |
Insert a newline without sending. |
Ctrl+Enter |
Same. |
Ctrl+C |
Copy the selection, or interrupt when nothing is selected. |
Ctrl+Shift+C |
Copy the selection. Never interrupts. |
Ctrl+V |
Paste. An image on the clipboard uploads and pastes its file path instead. |
Ctrl+L |
Clear the terminal. |
Ctrl+Shift+R |
Restore terminal size. |
Ctrl + / Ctrl - |
Font size. |
Shift+Wheel |
Scroll the local buffer, even where the wheel is forwarded to the CLI. |
Everything else
| Shortcut | Action |
|---|---|
Ctrl+Shift+V |
Toggle voice input. |
Ctrl+? |
Shortcut reference overlay. |
Escape |
Close panels and modals. |
Rebinding
App Settings → Shortcuts. Bindings live in a registry with per-user overrides, so a rebind is stored as an override on top of the default rather than replacing the table.
Two things are deliberately not rebindable:
Ctrl+Csmart copy. The generic dispatch loop callspreventDefault()on every shortcut it handles, and doing that toCtrl+Cwould swallow the interrupt when nothing is selected. It is handled separately for that reason.Escape, which closes whatever is open.
Why some chords behave oddly
The terminal sees keystrokes before the app does. Any chord the app claims has to also be
swallowed at the terminal layer, or xterm writes the control byte into the session as well
as triggering the action. If you rebind something to a chord the terminal cares about
(Ctrl+D, say), expect the CLI to see it too.
Alt+1 through Alt+9 are matched on physical key position rather than the character
produced, so macOS Option layouts that produce ¡™£ still switch tabs.
On phones
There is no physical keyboard, so the equivalents live in the keyboard accessory bar: Esc,
Ctrl as a one-shot modifier, Tab, arrows, and quick actions. See
Mobile Guide.
Read next
- The Dashboard - what the shortcuts are navigating.
- Settings Reference - where the overrides are stored.