From 78601e3bcea874612ce7fc06f30e0a921cac5789 Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Tue, 10 Mar 2026 21:36:35 +0300 Subject: [PATCH] fix(shell): copy terminal selections from xterm buffer The shell was delegating Cmd/Ctrl+C to document.execCommand('copy'), which copied the rendered DOM selection instead of xterm's logical buffer text. Wrapped values like login URLs could pick up row whitespace or line breaks and break when pasted. Route keyboard copy through terminal.getSelection() and the shared clipboard helper. Also intercept native copy events on the terminal container so mouse selection and browser copy actions use the same normalized terminal text. Remove the copy listener during teardown to avoid leaking handlers across terminal reinitialization. --- .../shell/hooks/useShellTerminal.ts | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/shell/hooks/useShellTerminal.ts b/src/components/shell/hooks/useShellTerminal.ts index cf7afc5..011e7ee 100644 --- a/src/components/shell/hooks/useShellTerminal.ts +++ b/src/components/shell/hooks/useShellTerminal.ts @@ -11,6 +11,7 @@ import { TERMINAL_OPTIONS, TERMINAL_RESIZE_DELAY_MS, } from '../constants/constants'; +import { copyTextToClipboard } from '../../../utils/clipboard'; import { isCodexLoginCommand } from '../utils/auth'; import { sendSocketMessage } from '../utils/socket'; import { ensureXtermFocusStyles } from '../utils/terminalStyles'; @@ -103,6 +104,37 @@ export function useShellTerminal({ nextTerminal.open(terminalContainerRef.current); + const copyTerminalSelection = async () => { + const selection = nextTerminal.getSelection(); + if (!selection) { + return false; + } + + return copyTextToClipboard(selection); + }; + + const handleTerminalCopy = (event: ClipboardEvent) => { + if (!nextTerminal.hasSelection()) { + return; + } + + const selection = nextTerminal.getSelection(); + if (!selection) { + return; + } + + event.preventDefault(); + + if (event.clipboardData) { + event.clipboardData.setData('text/plain', selection); + return; + } + + void copyTextToClipboard(selection); + }; + + terminalContainerRef.current.addEventListener('copy', handleTerminalCopy); + nextTerminal.attachCustomKeyEventHandler((event) => { const activeAuthUrl = isCodexLoginCommand(initialCommandRef.current) ? CODEX_DEVICE_AUTH_URL @@ -132,7 +164,7 @@ export function useShellTerminal({ ) { event.preventDefault(); event.stopPropagation(); - document.execCommand('copy'); + void copyTerminalSelection(); return false; } @@ -211,6 +243,7 @@ export function useShellTerminal({ resizeObserver.observe(terminalContainerRef.current); return () => { + terminalContainerRef.current?.removeEventListener('copy', handleTerminalCopy); resizeObserver.disconnect(); if (resizeTimeoutRef.current !== null) { window.clearTimeout(resizeTimeoutRef.current);