fix: use _term.paste() for Ctrl+Shift+V — enables bracketed paste mode

Raw WebSocket send bypassed xterm.js's paste pipeline, so multiline
text was sent without bracketed paste markers (ESC[200~...ESC[201~).
The shell treated each newline as Enter, executing lines separately.
Fix: _term.paste(text) goes through the proper pipeline — converts
CR/LF, wraps in bracketed paste markers, then fires onData → WebSocket.
This commit is contained in:
Brian Krabach
2026-04-06 19:09:32 -07:00
parent ad3c961770
commit 085379981a
2 changed files with 33 additions and 2 deletions
+6 -2
View File
@@ -375,8 +375,12 @@ function openTerminal(sessionName, remoteId) {
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) {
_ws.send(_encodePayload(0x30, text));
if (text && _term) {
// Use xterm.js paste() pipeline: converts \r\n→\r, wraps in bracketed
// paste markers (ESC[200~...ESC[201~) so the shell treats multiline text
// as a paste block rather than executing each line on Enter.
// Fires onData → our WebSocket handler automatically.
_term.paste(text);
}
}).catch(function() {});
}
+27
View File
@@ -1012,6 +1012,33 @@ test('terminal.js Ctrl+Shift+V handler calls preventDefault to suppress double p
);
});
// --- Multiline paste (bracketed paste mode) ---
test('terminal.js Ctrl+Shift+V uses _term.paste() for bracketed paste mode', () => {
// Regression: the handler sent raw bytes via _ws.send(_encodePayload(0x30, text)),
// bypassing xterm.js's paste pipeline. Multiline text had no bracketed paste markers
// (ESC[200~...ESC[201~), so the shell treated each \n as Enter — executing each line
// separately instead of pasting as a block.
// Fix: use _term.paste(text) which goes through the proper pipeline.
const source = fs.readFileSync(new URL('../terminal.js', import.meta.url), 'utf8');
// Locate the Ctrl+Shift+V 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('_term.paste'),
'Ctrl+Shift+V must use _term.paste() for proper bracketed paste mode support',
);
assert.ok(
!vBlock.includes('_encodePayload') && !vBlock.includes('_ws.send'),
'Ctrl+Shift+V must NOT send raw bytes via WebSocket (bypasses bracketed paste wrapping)',
);
});
// --- Federation reconnect routing ---
test('terminal.js reconnect uses federation connect path for remote sessions', () => {