a73x

3f348dbb

fix: let an explicit copy revoke a queued osc 52 write

a73x   2026-08-18 12:27

Commit message
fix: let an explicit copy revoke a queued osc 52 write

web/mux.js
Old New
@@ -643,6 +643,15 @@ class Tile {
643 const selection = this.selection; 643 const selection = this.selection;
644 if (!this.zoomed || !selection || selection.text === null) return; 644 if (!this.zoomed || !selection || selection.text === null) return;
645 this.clearSelectionFeedback(); 645 this.clearSelectionFeedback();
646 // An explicit copy is the newest statement of what the human wants on
647 // the clipboard, so it revokes the automatic lane rather than racing
648 // it. A write already handed to the browser genuinely cannot be
649 // cancelled — but the RE-DISPATCH that write performs when it settles
650 // is ours to start, and starting it would put terminal text on the
651 // clipboard after the gesture that asked for something else. Retiring
652 // the version and the queued value together is what suppresses it.
653 this.clipboardVersion++;
654 this.pendingClipboard = null;
646 const version = ++this.selectionCopyVersion; 655 const version = ++this.selectionCopyVersion;
647 this.renderCopyUi('selection', 'copy-request', 'Copy'); 656 this.renderCopyUi('selection', 'copy-request', 'Copy');
648 let succeeded = false; 657 let succeeded = false;
web/verify.js
Old New
@@ -2066,6 +2066,37 @@ async function verifySelectionShell(shell, html) {
2066 check('older OSC52 settlement cannot overwrite selection-copy success UI', `${clipboardRace.tile.copyButton.className}|${clipboardRace.tile.copyButton.textContent}`, 'copy-request on|Copied'); 2066 check('older OSC52 settlement cannot overwrite selection-copy success UI', `${clipboardRace.tile.copyButton.className}|${clipboardRace.tile.copyButton.textContent}`, 'copy-request on|Copied');
2067 check('older OSC52 still completes its serialized pending state', clipboardRace.tile.pendingClipboard, null); 2067 check('older OSC52 still completes its serialized pending state', clipboardRace.tile.pendingClipboard, null);
2068 2068
2069 // The re-dispatch at the end of tryClipboardWrite is ours to start, and
2070 // an explicit user copy is the newest statement of intent: a queued OSC 52
2071 // must not reach the system clipboard after the gesture that asked for
2072 // something else.
2073 const revoked = makeTile();
2074 revoked.tile.pendingClipboard = 'first OSC52';
2075 revoked.tile.clipboardVersion = 1;
2076 revoked.tile.copyUiOwner = 'clipboard';
2077 const firstOsc = deferred();
2078 const revokedCalls = [];
2079 h.navigator.clipboard = { writeText(text) {
2080 revokedCalls.push(text);
2081 return text === 'first OSC52' ? firstOsc.promise : Promise.resolve();
2082 } };
2083 const revokedRun = revoked.tile.tryClipboardWrite(1, true);
2084 // A second OSC 52 lands while the first write is still with the browser,
2085 // so it is queued in the one pending slot rather than started.
2086 revoked.tile.pendingClipboard = 'second OSC52';
2087 revoked.tile.clipboardVersion++;
2088 check('queued OSC52 is not yet handed to the browser', revokedCalls.join('|'), 'first OSC52');
2089 revoked.tile.selection = authoritativeSelection('what the user asked for');
2090 h.setZoomedTile(revoked.tile);
2091 h.document.dispatchEvent('keydown', keyEvent({ key: 'c', ctrlKey: true }));
2092 await flushPromises();
2093 firstOsc.resolve();
2094 await revokedRun;
2095 await flushPromises();
2096 check('explicit copy revokes the queued OSC52 write', revokedCalls.join('|'), 'first OSC52|what the user asked for');
2097 check('explicit copy drops the queued OSC52 text', revoked.tile.pendingClipboard, null);
2098 check('revoked OSC52 leaves the copy control to the selection', revoked.tile.copyUiOwner, 'selection');
2099
2069 const oldTimerRace = makeTile(); 2100 const oldTimerRace = makeTile();
2070 oldTimerRace.tile.pendingClipboard = 'prior copied OSC52'; 2101 oldTimerRace.tile.pendingClipboard = 'prior copied OSC52';
2071 oldTimerRace.tile.clipboardVersion = 1; 2102 oldTimerRace.tile.clipboardVersion = 1;