From 89158241c0f215477f0a9ba6147659a5ff789432 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 6 Apr 2026 07:53:56 -0700 Subject: [PATCH] fix: prevent double paste on Ctrl+Shift+V Our custom handler sent clipboard text via WebSocket, but the browser's native paste event also fired on xterm.js's hidden textarea, causing xterm's built-in paste handler to send the same text again. Fix: e.preventDefault() suppresses the browser paste event. --- muxplex/frontend/terminal.js | 1 + muxplex/frontend/tests/test_terminal.mjs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/muxplex/frontend/terminal.js b/muxplex/frontend/terminal.js index e9a305f..e285e57 100644 --- a/muxplex/frontend/terminal.js +++ b/muxplex/frontend/terminal.js @@ -364,6 +364,7 @@ function openTerminal(sessionName, remoteId) { // Ctrl+Shift+V → paste from clipboard into terminal if (e.ctrlKey && e.shiftKey && (e.key === 'V' || e.code === 'KeyV')) { + e.preventDefault(); // suppress browser native paste event (prevents double-paste via xterm textarea) if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then(function(text) { if (text && _ws && _ws.readyState === WebSocket.OPEN) { diff --git a/muxplex/frontend/tests/test_terminal.mjs b/muxplex/frontend/tests/test_terminal.mjs index e436473..bd628bc 100644 --- a/muxplex/frontend/tests/test_terminal.mjs +++ b/muxplex/frontend/tests/test_terminal.mjs @@ -991,4 +991,25 @@ test('terminal.js loads xterm-addon-image for inline graphics', () => { assert.ok(source.includes('ImageAddon'), 'must reference ImageAddon'); }); +// --- Clipboard double-paste bug fix --- + +test('terminal.js Ctrl+Shift+V handler calls preventDefault to suppress double paste', () => { + // Regression: without e.preventDefault(), the browser fires a native paste event on + // xterm.js's hidden textarea after our handler sends via WebSocket, causing double paste. + const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8'); + + // Locate the Ctrl+Shift+V block by finding the key check, then extracting the block + const vIdx = source.indexOf("e.key === 'V'"); + assert.ok(vIdx !== -1, "must have a Ctrl+Shift+V key check"); + const blockStart = source.lastIndexOf('if (', vIdx); + const blockEnd = source.indexOf('return false', vIdx); + const vBlock = source.substring(blockStart, blockEnd); + + assert.ok( + vBlock.includes('preventDefault'), + 'Ctrl+Shift+V handler must call e.preventDefault() before the async clipboard read ' + + 'to suppress the browser native paste event (which causes double-paste via xterm textarea)', + ); +}); +