Merge #799: cancel the Ctrl+V keydown once the paste trap has the clipboard (Firefox double paste)

This commit is contained in:
Codeman maintainer committed 2026-11-11 10:21:44 +01:00
commit 1d7288dc4f
7 files changed
+126 -26

No files matched your search

+17 -1
View File
@@ -196,11 +196,13 @@ Object.assign(CodemanApp.prototype, {
// own. Images upload to THAT session. Plain text goes to that pane's session
// as a Codeman-built bracketed-paste frame: the terminal is only refocused
// and receives input(framed). onData delivers the frame; it is not written
// into the xterm buffer.
// into the xterm buffer. `target.keyEvent` is the Ctrl+V keydown itself,
// which the trap cancels when execCommand('paste') has already delivered.
_handleImagePaste(target = {}) {
const self = this;
const terminal = target.terminal || this.terminal;
const sessionId = target.sessionId || this.activeSessionId;
const keyEvent = target.keyEvent;
// Create a hidden contenteditable div to receive the paste
const trap = document.createElement('div');
@@ -267,6 +269,20 @@ Object.assign(CodemanApp.prototype, {
// Trigger the browser's native paste via execCommand
// (this fires the paste event on our focused trap element)
document.execCommand('paste');
// Firefox dispatches the execCommand paste synchronously, inside the call
// above, and then delivers the key's own paste as a SEPARATE task about
// 10 ms later. That second event races the setTimeout above that removes
// the trap and refocuses the terminal. When the timer wins, the second
// paste lands on xterm's textarea, xterm's own paste handler sends the
// clipboard again, and the one-shot flag never sees it. That race is why
// Ctrl+V still pasted twice only sometimes. Once execCommand has delivered
// a paste, cancel the keydown so the browser never issues its own.
// Chromium and WebKit refuse the command and dispatch nothing here, so
// they keep the default action, which delivers their only paste event.
if (pasteConsumed && keyEvent && typeof keyEvent.preventDefault === 'function') {
keyEvent.preventDefault();
}
},
/** What a pick or a paste may upload: 'image', 'video' for the camera roll's
+1 -1
View File
@@ -349,7 +349,7 @@
// input(framed), which onData delivers. Mirrors terminal-ui.js's own
// Ctrl+V gate; without it xterm's default only ever pasted text.
if ((ev.ctrlKey || ev.metaKey) && ev.key === 'v' && ev.type === 'keydown') {
global.app?._handleImagePaste?.({ terminal: this.terminal, sessionId: this.sessionId });
global.app?._handleImagePaste?.({ terminal: this.terminal, sessionId: this.sessionId, keyEvent: ev });
return false;
}
// Ctrl+Z (SIGTSTP/job-control suspend), as terminal-ui.js swallows it:
+3 -1
View File
@@ -924,7 +924,9 @@ Object.assign(CodemanApp.prototype, {
// Route through our paste trap which handles both images and text.
if ((ev.ctrlKey || ev.metaKey) && ev.key === 'v' && ev.type === 'keydown') {
if (this.activeSessionId && this._handleImagePaste) {
this._handleImagePaste();
// The trap cancels this keydown itself when execCommand('paste')
// already delivered the clipboard (Firefox), so pass the event in.
this._handleImagePaste({ keyEvent: ev });
}
return false;
}