diff --git a/packages/web/src/components/DirectTerminal.tsx b/packages/web/src/components/DirectTerminal.tsx index 29b57fe9f..f62eecf45 100644 --- a/packages/web/src/components/DirectTerminal.tsx +++ b/packages/web/src/components/DirectTerminal.tsx @@ -189,47 +189,13 @@ export function DirectTerminal({ const port = process.env.NEXT_PUBLIC_DIRECT_TERMINAL_PORT ?? "14801"; const wsUrl = `${protocol}//${hostname}:${port}/ws?session=${encodeURIComponent(sessionId)}`; - // ── Selection-aware write buffer ────────────────────────────── - // xterm.js clears the selection on every terminal.write(). To keep - // the selection visible while the terminal receives output, we - // buffer incoming data while a selection is active and flush once - // the selection is cleared (click, keypress, etc.). - const writeBuffer: string[] = []; - let selecting = false; // true while mouse button is down - let hasSelection = false; // true while a selection exists - let flushTimer: ReturnType | null = null; - const MAX_BUFFER_MS = 5_000; // safety cap - - const flushBuffer = () => { - if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } - if (writeBuffer.length > 0) { - terminal.write(writeBuffer.join("")); - writeBuffer.length = 0; - } - }; - - const shouldBuffer = () => selecting || hasSelection; - - const termEl = terminalRef.current!; - const onMouseDown = () => { selecting = true; }; - const onMouseUp = () => { selecting = false; }; - termEl.addEventListener("mousedown", onMouseDown); - termEl.addEventListener("mouseup", onMouseUp); - // Auto-copy selection to clipboard whenever the user selects text. - // Also track selection state for the write buffer. + // xterm.js clears the selection on incoming terminal.write(), so we + // copy eagerly — the text is in the clipboard even if the highlight + // disappears before the user presses Cmd+C. const selectionDisposable = terminal.onSelectionChange(() => { if (terminal.hasSelection()) { - hasSelection = true; navigator.clipboard?.writeText(terminal.getSelection()).catch(() => {}); - // Safety: flush buffer after MAX_BUFFER_MS even if selection persists - if (!flushTimer) { - flushTimer = setTimeout(flushBuffer, MAX_BUFFER_MS); - } - } else { - hasSelection = false; - // Selection was cleared — flush buffered output - flushBuffer(); } }); @@ -265,6 +231,7 @@ export function DirectTerminal({ // Handle paste via the DOM "paste" event — this provides clipboard // content through ClipboardEvent.clipboardData without needing the // Clipboard API "clipboard-read" permission. + const termEl = terminalRef.current!; const handlePaste = (e: ClipboardEvent) => { const text = e.clipboardData?.getData("text/plain"); if (text) { @@ -325,11 +292,7 @@ export function DirectTerminal({ websocket.onmessage = (event) => { const data = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data); - if (shouldBuffer()) { - writeBuffer.push(data); - } else { - terminal.write(data); - } + terminal.write(data); }; websocket.onerror = (event) => { @@ -367,10 +330,7 @@ export function DirectTerminal({ // Store cleanup function to be called from useEffect cleanup cleanup = () => { selectionDisposable.dispose(); - termEl.removeEventListener("mousedown", onMouseDown); - termEl.removeEventListener("mouseup", onMouseUp); termEl.removeEventListener("paste", handlePaste as EventListener); - if (flushTimer) clearTimeout(flushTimer); window.removeEventListener("resize", handleResize); inputDisposable?.dispose(); inputDisposable = null;