From b8b719de1ca402cc6e3a18227540085676ba2ae6 Mon Sep 17 00:00:00 2001 From: Mayank Debnath Date: Sun, 12 Jul 2026 11:25:51 +0000 Subject: [PATCH] fix(shell): paste through terminal.paste() and stop swallowing native paste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both shell paste paths (the Ctrl/Cmd+V handler and the mobile shortcuts-panel Paste button) sent raw clipboard text as a socket input message, bypassing xterm's bracketed-paste wrapping (DECSET 2004) and newline normalization — multi-line pastes submitted or executed line-by-line in Claude Code's CLI, shells, and vim. Route both through terminal.paste(), whose onData output flows to the pty via the existing socket input subscription. The key handler also unconditionally cancelled the paste chord even when navigator.clipboard.readText was unavailable (plain-HTTP deployments have no navigator.clipboard), pasting nothing while suppressing the browser's native paste event that would have reached xterm's hidden textarea. Only intercept the chord when the async clipboard API is actually usable; otherwise let the native path handle it. Fixes #1006 --- .../shell/hooks/useShellTerminal.ts | 31 ++++++++++++------- .../subcomponents/TerminalShortcutsPanel.tsx | 8 +++-- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/components/shell/hooks/useShellTerminal.ts b/src/components/shell/hooks/useShellTerminal.ts index 8b34381194..bf99982402 100644 --- a/src/components/shell/hooks/useShellTerminal.ts +++ b/src/components/shell/hooks/useShellTerminal.ts @@ -216,21 +216,30 @@ export function useShellTerminal({ (event.ctrlKey || event.metaKey) && event.key?.toLowerCase() === 'v' ) { + // Without the async clipboard API (common on self-hosted plain-http + // deployments, where navigator.clipboard doesn't exist) there is + // nothing to read here — let the event through so the browser's native + // paste event can still reach xterm's hidden textarea, instead of + // swallowing the keystroke and pasting nothing. + if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) { + return true; + } + // Block native paste so data is only injected after clipboard-read resolves. event.preventDefault(); event.stopPropagation(); - if (typeof navigator !== 'undefined' && navigator.clipboard?.readText) { - navigator.clipboard - .readText() - .then((text) => { - sendSocketMessage(wsRef.current, { - type: 'input', - data: text, - }); - }) - .catch(() => {}); - } + navigator.clipboard + .readText() + .then((text) => { + // terminal.paste() applies bracketed-paste wrapping (DECSET 2004) + // and \n -> \r normalization, then emits onData, which flows to + // the pty through the existing socket input subscription. Sending + // the raw text as an input message bypassed bracketed paste, so + // multi-line pastes executed line-by-line in shells and CLIs. + nextTerminal.paste(text); + }) + .catch(() => {}); return false; } diff --git a/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx index 88c1ee9ecc..034c29eb85 100644 --- a/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx +++ b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx @@ -81,12 +81,16 @@ export default function TerminalShortcutsPanel({ try { const text = await navigator.clipboard.readText(); if (text.length > 0) { - sendInput(text); + // terminal.paste() applies bracketed-paste wrapping and \n -> \r + // normalization before the text reaches the pty via onData; sending + // raw text as an input message made multi-line pastes execute + // line-by-line. + terminalRef.current?.paste(text); } } catch { // Ignore clipboard permission errors. } - }, [sendInput]); + }, [terminalRef]); const handleKeyPress = useCallback( (seq: string) => {