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) => {