From 3ac26ba6e8eb1c85fbd9ca4c0ab3d937f58e1a76 Mon Sep 17 00:00:00 2001 From: suraj-markup Date: Mon, 2 Mar 2026 00:13:34 +0530 Subject: [PATCH] fix(terminal): preserve text selection while terminal receives output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buffer incoming WebSocket data while the user is selecting text (mousedown→mouseup). Without this, terminal.write() clears the xterm.js selection mid-drag or immediately on mouseup. The buffer is flushed 150ms after mouseup so the selection stays visible long enough to copy with Cmd+C. Co-Authored-By: Claude Opus 4.6 --- .../web/src/components/DirectTerminal.tsx | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx index a4c1221c3..762268e42 100644 --- a/packages/web/src/components/DirectTerminal.tsx +++ b/packages/web/src/components/DirectTerminal.tsx @@ -201,10 +201,40 @@ export function DirectTerminal({ ); }; + // Buffer incoming data while the user is selecting text so that + // terminal.write() doesn't clear the xterm.js selection mid-drag. + let selecting = false; + const writeBuffer: string[] = []; + + const flushBuffer = () => { + if (writeBuffer.length > 0) { + terminal.write(writeBuffer.join("")); + writeBuffer.length = 0; + } + }; + + const onMouseDown = () => { + selecting = true; + }; + const onMouseUp = () => { + selecting = false; + // Small delay so the selection is still visible when the user + // releases the mouse and can hit Cmd+C before the flush clears it. + setTimeout(flushBuffer, 150); + }; + + const el = terminalRef.current!; + el.addEventListener("mousedown", onMouseDown); + el.addEventListener("mouseup", onMouseUp); + websocket.onmessage = (event) => { const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data); - terminal.write(data); + if (selecting) { + writeBuffer.push(data); + } else { + terminal.write(data); + } }; websocket.onerror = (event) => { @@ -284,6 +314,8 @@ export function DirectTerminal({ // Store cleanup function to be called from useEffect cleanup cleanup = () => { + el.removeEventListener("mousedown", onMouseDown); + el.removeEventListener("mouseup", onMouseUp); window.removeEventListener("resize", handleResize); disposable.dispose(); websocket.close();