a73x

1120988e

fix: revoke expired selection replies

a73x   2026-08-18 12:27

Commit message
fix: revoke expired selection replies

web/mux.js
Old New
@@ -149,6 +149,7 @@ class Tile {
149 this.nextSelectionId = 0; 149 this.nextSelectionId = 0;
150 this.selectionRequestTimer = null; 150 this.selectionRequestTimer = null;
151 this.selectionRequestVersion = 0; 151 this.selectionRequestVersion = 0;
152 this.activeSelectionRequest = null; // {selection, id, generation}
152 this.selectionFeedbackTimer = null; 153 this.selectionFeedbackTimer = null;
153 this.selectionFeedbackVersion = 0; 154 this.selectionFeedbackVersion = 0;
154 this.drag = null; 155 this.drag = null;
@@ -672,9 +673,12 @@ class Tile {
672 } 673 }
673 674
674 onSelectionReply() { 675 onSelectionReply() {
675 if (!this.selection) return; 676 const active = this.activeSelectionRequest;
677 if (!this.selection || !active || active.selection !== this.selection ||
678 active.id !== this.selection.requestId ||
679 active.generation !== this.selectionRequestVersion) return;
676 // A Zig u32 result reaches JavaScript as a signed WASM i32. 680 // A Zig u32 result reaches JavaScript as a signed WASM i32.
677 if ((this.core.mux_selection_id() >>> 0) !== this.selection.requestId) return; 681 if ((this.core.mux_selection_id() >>> 0) !== active.id) return;
678 this.invalidateSelectionRequest(); 682 this.invalidateSelectionRequest();
679 if (this.core.mux_selection_status() !== 0) { 683 if (this.core.mux_selection_status() !== 0) {
680 this.selection.text = null; 684 this.selection.text = null;
@@ -728,22 +732,29 @@ class Tile {
728 invalidateSelectionRequest() { 732 invalidateSelectionRequest() {
729 if (this.selectionRequestTimer !== null) clearTimeout(this.selectionRequestTimer); 733 if (this.selectionRequestTimer !== null) clearTimeout(this.selectionRequestTimer);
730 this.selectionRequestTimer = null; 734 this.selectionRequestTimer = null;
735 this.activeSelectionRequest = null;
731 this.selectionRequestVersion++; 736 this.selectionRequestVersion++;
732 } 737 }
733 738
734 startSelectionRequestTimeout(selection, reservedVersion = null) { 739 activateSelectionRequest(selection) {
735 if (reservedVersion === null) { 740 this.invalidateSelectionRequest();
736 this.invalidateSelectionRequest(); 741 const request = {
737 reservedVersion = this.selectionRequestVersion; 742 selection,
738 } 743 id: selection.requestId,
739 const version = reservedVersion; 744 generation: this.selectionRequestVersion,
740 const requestId = selection.requestId; 745 };
746 this.activeSelectionRequest = request;
747 return request;
748 }
749
750 startSelectionRequestTimeout(request) {
751 const selection = request.selection;
741 const timer = setTimeout(() => { 752 const timer = setTimeout(() => {
742 if (this.selectionRequestTimer !== timer || 753 if (this.selectionRequestTimer !== timer ||
743 this.selectionRequestVersion !== version || 754 this.activeSelectionRequest !== request ||
744 this.selection !== selection || selection.requestId !== requestId) return; 755 this.selectionRequestVersion !== request.generation ||
745 this.selectionRequestTimer = null; 756 this.selection !== selection || selection.requestId !== request.id) return;
746 this.selectionRequestVersion++; 757 this.invalidateSelectionRequest();
747 selection.text = null; 758 selection.text = null;
748 this.showSelectionUnavailable(); 759 this.showSelectionUnavailable();
749 }, SELECTION_REQUEST_TIMEOUT_MS); 760 }, SELECTION_REQUEST_TIMEOUT_MS);
@@ -835,16 +846,16 @@ class Tile {
835 ); 846 );
836 if (n === 16) { 847 if (n === 16) {
837 const selection = this.selection; 848 const selection = this.selection;
838 this.invalidateSelectionRequest(); 849 // Establish reply authority before send: a host is allowed to deliver
839 const requestVersion = this.selectionRequestVersion; 850 // a matching semantic reply synchronously from its send hook.
851 const request = this.activateSelectionRequest(selection);
840 if (!this.sendFrame(MSG.selection_req, this.outBytes())) { 852 if (!this.sendFrame(MSG.selection_req, this.outBytes())) {
841 this.clearSelection(); 853 this.clearSelection();
842 return; 854 return;
843 } 855 }
844 if (endpointChanged) this.reflow(); 856 if (endpointChanged) this.reflow();
845 if (this.selection === selection && 857 if (this.activeSelectionRequest === request)
846 this.selectionRequestVersion === requestVersion) 858 this.startSelectionRequestTimeout(request);
847 this.startSelectionRequestTimeout(selection, requestVersion);
848 } 859 }
849 else this.clearSelection(); 860 else this.clearSelection();
850 } 861 }
web/verify.js
Old New
@@ -730,6 +730,7 @@ async function verifySelectionShell(shell, html) {
730 && tile.scrollRequestTimer === null 730 && tile.scrollRequestTimer === null
731 && tile.selectionRequestTimer === null 731 && tile.selectionRequestTimer === null
732 && tile.selectionRequestVersion === 0 732 && tile.selectionRequestVersion === 0
733 && tile.activeSelectionRequest === null
733 && tile.selectionFeedbackTimer === null 734 && tile.selectionFeedbackTimer === null
734 && tile.selectionFeedbackVersion === 0 735 && tile.selectionFeedbackVersion === 0
735 && tile.selectionCopyVersion === 0; 736 && tile.selectionCopyVersion === 0;
@@ -859,6 +860,8 @@ async function verifySelectionShell(shell, html) {
859 envelope.set(payload, 6); 860 envelope.set(payload, 6);
860 return envelope; 861 return envelope;
861 }; 862 };
863 const authorizeSelectionReply = (fixture) =>
864 fixture.tile.activateSelectionRequest(fixture.tile.selection);
862 865
863 const transport = makeTile(); 866 const transport = makeTile();
864 transport.tile.sendFrame = h.Tile.prototype.sendFrame.bind(transport.tile); 867 transport.tile.sendFrame = h.Tile.prototype.sendFrame.bind(transport.tile);
@@ -997,7 +1000,9 @@ async function verifySelectionShell(shell, html) {
997 snapshotHistory.tile.selection = { 1000 snapshotHistory.tile.selection = {
998 anchor: { row: 21, col: 1 }, active: { row: 24, col: 3 }, requestId: 90, text: 'old grid', 1001 anchor: { row: 21, col: 1 }, active: { row: 24, col: 3 }, requestId: 90, text: 'old grid',
999 }; 1002 };
1000 snapshotHistory.tile.startSelectionRequestTimeout(snapshotHistory.tile.selection); 1003 snapshotHistory.tile.startSelectionRequestTimeout(
1004 snapshotHistory.tile.activateSelectionRequest(snapshotHistory.tile.selection),
1005 );
1001 const snapshotSelectionHandle = snapshotHistory.tile.selectionRequestTimer; 1006 const snapshotSelectionHandle = snapshotHistory.tile.selectionRequestTimer;
1002 const snapshotSelectionTimer = snapshotSelectionHandle === null 1007 const snapshotSelectionTimer = snapshotSelectionHandle === null
1003 ? undefined : h.timers[snapshotSelectionHandle - 1]; 1008 ? undefined : h.timers[snapshotSelectionHandle - 1];
@@ -1431,6 +1436,31 @@ async function verifySelectionShell(shell, html) {
1431 check('selection response timeout clears timer identity', selectionTimeout.tile.selectionRequestTimer, null); 1436 check('selection response timeout clears timer identity', selectionTimeout.tile.selectionRequestTimer, null);
1432 check('selection response timeout retains no authoritative text', selectionTimeout.tile.selection?.text, null); 1437 check('selection response timeout retains no authoritative text', selectionTimeout.tile.selection?.text, null);
1433 check('selection response timeout reports transient unavailability', `${selectionTimeout.tile.copyButton.className}|${selectionTimeout.tile.copyButton.textContent}`, 'copy-request on error|Selection unavailable'); 1438 check('selection response timeout reports transient unavailability', `${selectionTimeout.tile.copyButton.className}|${selectionTimeout.tile.copyButton.textContent}`, 'copy-request on error|Selection unavailable');
1439 check('selection response timeout revokes active request capability', selectionTimeout.tile.activeSelectionRequest, null);
1440
1441 const lateAfterTimeout = makeTile();
1442 lateAfterTimeout.tile.pendingClipboard = 'OSC fallback after timeout';
1443 lateAfterTimeout.tile.clipboardVersion = 1;
1444 lateAfterTimeout.tile.copyUiOwner = 'clipboard';
1445 lateAfterTimeout.tile.canvas.dispatchEvent('pointerdown', pointer(41, 1, 1));
1446 lateAfterTimeout.tile.canvas.dispatchEvent('pointerup', pointer(41, 4, 2));
1447 const lateLeaseHandle = lateAfterTimeout.tile.selectionRequestTimer;
1448 const lateLeaseTimer = lateLeaseHandle === null
1449 ? undefined : h.timers[lateLeaseHandle - 1];
1450 lateLeaseTimer?.fn();
1451 const lateFeedbackHandle = lateAfterTimeout.tile.selectionFeedbackTimer;
1452 const lateFeedbackTimer = lateFeedbackHandle === null
1453 ? undefined : h.timers[lateFeedbackHandle - 1];
1454 lateFeedbackTimer?.fn();
1455 check('expired selection feedback restores OSC fallback before late reply', lateAfterTimeout.tile.copyUiOwner, 'clipboard');
1456 lateAfterTimeout.setResult(1, 0, Buffer.from('must stay expired'));
1457 lateAfterTimeout.tile.onSelectionReply();
1458 check('late same-id success cannot restore expired authoritative text', lateAfterTimeout.tile.selection?.text, null);
1459 check('late same-id success cannot reclaim shared copy UI', lateAfterTimeout.tile.copyUiOwner, 'clipboard');
1460 lateAfterTimeout.setResult(1, 2, []);
1461 lateAfterTimeout.tile.onSelectionReply();
1462 check('late same-id failure cannot reclaim shared copy UI', lateAfterTimeout.tile.copyUiOwner, 'clipboard');
1463 check('late same-id failure cannot start new unavailable feedback', lateAfterTimeout.tile.selectionFeedbackTimer, null);
1434 1464
1435 const replyBeforeTimeout = makeTile(); 1465 const replyBeforeTimeout = makeTile();
1436 replyBeforeTimeout.tile.canvas.dispatchEvent('pointerdown', pointer(33, 1, 1)); 1466 replyBeforeTimeout.tile.canvas.dispatchEvent('pointerdown', pointer(33, 1, 1));
@@ -1443,6 +1473,7 @@ async function verifySelectionShell(shell, html) {
1443 check('matching selection reply clears response timeout', replyTimeoutTask?.cleared, true); 1473 check('matching selection reply clears response timeout', replyTimeoutTask?.cleared, true);
1444 check('matching selection reply clears timeout identity', replyBeforeTimeout.tile.selectionRequestTimer, null); 1474 check('matching selection reply clears timeout identity', replyBeforeTimeout.tile.selectionRequestTimer, null);
1445 check('matching selection reply retains authoritative text', replyBeforeTimeout.tile.selection?.text, 'reply before timeout'); 1475 check('matching selection reply retains authoritative text', replyBeforeTimeout.tile.selection?.text, 'reply before timeout');
1476 check('matching selection reply consumes active request capability', replyBeforeTimeout.tile.activeSelectionRequest, null);
1446 1477
1447 const synchronousReply = makeTile(); 1478 const synchronousReply = makeTile();
1448 synchronousReply.tile.sendFrame = () => { 1479 synchronousReply.tile.sendFrame = () => {
@@ -1454,6 +1485,7 @@ async function verifySelectionShell(shell, html) {
1454 synchronousReply.tile.canvas.dispatchEvent('pointerup', pointer(40, 4, 2)); 1485 synchronousReply.tile.canvas.dispatchEvent('pointerup', pointer(40, 4, 2));
1455 check('synchronous host reply retains authoritative selection', synchronousReply.tile.selection?.text, 'synchronous host reply'); 1486 check('synchronous host reply retains authoritative selection', synchronousReply.tile.selection?.text, 'synchronous host reply');
1456 check('synchronous host reply cannot acquire a stale response timeout', synchronousReply.tile.selectionRequestTimer, null); 1487 check('synchronous host reply cannot acquire a stale response timeout', synchronousReply.tile.selectionRequestTimer, null);
1488 check('synchronous host reply consumes pre-send active capability', synchronousReply.tile.activeSelectionRequest, null);
1457 1489
1458 const failureBeforeTimeout = makeTile(); 1490 const failureBeforeTimeout = makeTile();
1459 failureBeforeTimeout.tile.canvas.dispatchEvent('pointerdown', pointer(34, 1, 1)); 1491 failureBeforeTimeout.tile.canvas.dispatchEvent('pointerdown', pointer(34, 1, 1));
@@ -1481,6 +1513,23 @@ async function verifySelectionShell(shell, html) {
1481 check('stale selection timeout cannot clear newer response timer', staleSelectionTimeout.tile.selectionRequestTimer, newerSelectionTimer); 1513 check('stale selection timeout cannot clear newer response timer', staleSelectionTimeout.tile.selectionRequestTimer, newerSelectionTimer);
1482 check('stale selection timeout cannot show unavailable for newer request', staleSelectionTimeout.tile.copyUiOwner, null); 1514 check('stale selection timeout cannot show unavailable for newer request', staleSelectionTimeout.tile.copyUiOwner, null);
1483 1515
1516 const staleSelectionReply = makeTile();
1517 staleSelectionReply.tile.canvas.dispatchEvent('pointerdown', pointer(42, 1, 1));
1518 staleSelectionReply.tile.canvas.dispatchEvent('pointerup', pointer(42, 4, 2));
1519 staleSelectionReply.tile.canvas.dispatchEvent('pointerdown', pointer(43, 2, 2));
1520 staleSelectionReply.tile.canvas.dispatchEvent('pointerup', pointer(43, 5, 3));
1521 const activeNewerRequest = staleSelectionReply.tile.activeSelectionRequest;
1522 check('newer selection establishes active pre-send capability', activeNewerRequest !== null, true);
1523 check('newer selection active capability carries request id', activeNewerRequest?.id, 2);
1524 staleSelectionReply.setResult(1, 0, Buffer.from('stale older reply'));
1525 staleSelectionReply.tile.onSelectionReply();
1526 check('older reply cannot consume newer active request capability', staleSelectionReply.tile.activeSelectionRequest, activeNewerRequest);
1527 check('older reply cannot bind text to newer selection', staleSelectionReply.tile.selection?.text, null);
1528 staleSelectionReply.setResult(2, 0, Buffer.from('matching newer reply'));
1529 staleSelectionReply.tile.onSelectionReply();
1530 check('matching newer reply binds after stale older reply', staleSelectionReply.tile.selection?.text, 'matching newer reply');
1531 check('matching newer reply consumes active capability', staleSelectionReply.tile.activeSelectionRequest, null);
1532
1484 const selectionLifecycle = makeTile(); 1533 const selectionLifecycle = makeTile();
1485 selectionLifecycle.tile.canvas.dispatchEvent('pointerdown', pointer(37, 1, 1)); 1534 selectionLifecycle.tile.canvas.dispatchEvent('pointerdown', pointer(37, 1, 1));
1486 selectionLifecycle.tile.canvas.dispatchEvent('pointerup', pointer(37, 4, 2)); 1535 selectionLifecycle.tile.canvas.dispatchEvent('pointerup', pointer(37, 4, 2));
@@ -1585,6 +1634,7 @@ async function verifySelectionShell(shell, html) {
1585 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: highId, text: null, 1634 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: highId, text: null,
1586 }; 1635 };
1587 const authoritative = Buffer.from('daemon ☃', 'utf8'); 1636 const authoritative = Buffer.from('daemon ☃', 'utf8');
1637 authorizeSelectionReply(replies);
1588 replies.setResult(highId, 0, authoritative); 1638 replies.setResult(highId, 0, authoritative);
1589 replies.tile.onSelectionReply(); 1639 replies.tile.onSelectionReply();
1590 new Uint8Array(replies.memory.buffer).fill(0, 96, 96 + authoritative.length); 1640 new Uint8Array(replies.memory.buffer).fill(0, 96, 96 + authoritative.length);
@@ -1595,12 +1645,14 @@ async function verifySelectionShell(shell, html) {
1595 replies.tile.onSelectionReply(); 1645 replies.tile.onSelectionReply();
1596 check('stale semantic reply cannot replace retained text', replies.tile.selection.text, 'keep'); 1646 check('stale semantic reply cannot replace retained text', replies.tile.selection.text, 'keep');
1597 1647
1648 authorizeSelectionReply(replies);
1598 replies.setResult(highId, 2, []); 1649 replies.setResult(highId, 2, []);
1599 replies.tile.onSelectionReply(); 1650 replies.tile.onSelectionReply();
1600 check('failed matching reply clears authoritative text', replies.tile.selection.text, null); 1651 check('failed matching reply clears authoritative text', replies.tile.selection.text, null);
1601 check('failed matching reply reports selection unavailable', `${replies.tile.copyButton.className}|${replies.tile.copyButton.textContent}`, 'copy-request on error|Selection unavailable'); 1652 check('failed matching reply reports selection unavailable', `${replies.tile.copyButton.className}|${replies.tile.copyButton.textContent}`, 'copy-request on error|Selection unavailable');
1602 1653
1603 replies.tile.selection.text = 'previous'; 1654 replies.tile.selection.text = 'previous';
1655 authorizeSelectionReply(replies);
1604 replies.setResult(highId, 0, [0xff]); 1656 replies.setResult(highId, 0, [0xff]);
1605 replies.tile.onSelectionReply(); 1657 replies.tile.onSelectionReply();
1606 check('browser rejects invalid UTF-8 selection text defensively', replies.tile.selection.text, null); 1658 check('browser rejects invalid UTF-8 selection text defensively', replies.tile.selection.text, null);
@@ -1615,6 +1667,7 @@ async function verifySelectionShell(shell, html) {
1615 routed.tile.selection = { 1667 routed.tile.selection = {
1616 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 77, text: null, 1668 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 77, text: null,
1617 }; 1669 };
1670 authorizeSelectionReply(routed);
1618 routed.setResult(77, 0, Buffer.from('routed')); 1671 routed.setResult(77, 0, Buffer.from('routed'));
1619 const replyPayload = Uint8Array.from([77, 0, 0, 0, 0, ...Buffer.from('routed')]); 1672 const replyPayload = Uint8Array.from([77, 0, 0, 0, 0, ...Buffer.from('routed')]);
1620 const replyEnvelope = new Uint8Array(6 + replyPayload.length); 1673 const replyEnvelope = new Uint8Array(6 + replyPayload.length);
@@ -1632,6 +1685,7 @@ async function verifySelectionShell(shell, html) {
1632 blockedCopy.tile.selection = { 1685 blockedCopy.tile.selection = {
1633 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 71, text: null, 1686 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 71, text: null,
1634 }; 1687 };
1688 authorizeSelectionReply(blockedCopy);
1635 blockedCopy.setResult(71, 3, []); 1689 blockedCopy.setResult(71, 3, []);
1636 blockedCopy.tile.onSelectionReply(); 1690 blockedCopy.tile.onSelectionReply();
1637 const unavailableFeedbackHandle = blockedCopy.tile.selectionFeedbackTimer; 1691 const unavailableFeedbackHandle = blockedCopy.tile.selectionFeedbackTimer;
@@ -1653,6 +1707,7 @@ async function verifySelectionShell(shell, html) {
1653 ownerChangedFeedback.tile.selection = { 1707 ownerChangedFeedback.tile.selection = {
1654 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 78, text: null, 1708 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 78, text: null,
1655 }; 1709 };
1710 authorizeSelectionReply(ownerChangedFeedback);
1656 ownerChangedFeedback.setResult(78, 3, []); 1711 ownerChangedFeedback.setResult(78, 3, []);
1657 ownerChangedFeedback.tile.onSelectionReply(); 1712 ownerChangedFeedback.tile.onSelectionReply();
1658 const supersededFeedbackHandle = ownerChangedFeedback.tile.selectionFeedbackTimer; 1713 const supersededFeedbackHandle = ownerChangedFeedback.tile.selectionFeedbackTimer;
@@ -1673,6 +1728,7 @@ async function verifySelectionShell(shell, html) {
1673 copySupersedesFeedback.tile.selection = { 1728 copySupersedesFeedback.tile.selection = {
1674 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 79, text: null, 1729 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 79, text: null,
1675 }; 1730 };
1731 authorizeSelectionReply(copySupersedesFeedback);
1676 copySupersedesFeedback.setResult(79, 3, []); 1732 copySupersedesFeedback.setResult(79, 3, []);
1677 copySupersedesFeedback.tile.onSelectionReply(); 1733 copySupersedesFeedback.tile.onSelectionReply();
1678 const preCopyFeedbackHandle = copySupersedesFeedback.tile.selectionFeedbackTimer; 1734 const preCopyFeedbackHandle = copySupersedesFeedback.tile.selectionFeedbackTimer;
@@ -1689,6 +1745,7 @@ async function verifySelectionShell(shell, html) {
1689 unzoomFeedback.tile.selection = { 1745 unzoomFeedback.tile.selection = {
1690 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 81, text: null, 1746 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 81, text: null,
1691 }; 1747 };
1748 authorizeSelectionReply(unzoomFeedback);
1692 unzoomFeedback.setResult(81, 3, []); 1749 unzoomFeedback.setResult(81, 3, []);
1693 unzoomFeedback.tile.onSelectionReply(); 1750 unzoomFeedback.tile.onSelectionReply();
1694 const unzoomFeedbackHandle = unzoomFeedback.tile.selectionFeedbackTimer; 1751 const unzoomFeedbackHandle = unzoomFeedback.tile.selectionFeedbackTimer;
@@ -1710,6 +1767,7 @@ async function verifySelectionShell(shell, html) {
1710 inFlightUi.tile.selection = { 1767 inFlightUi.tile.selection = {
1711 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 72, text: null, 1768 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 72, text: null,
1712 }; 1769 };
1770 authorizeSelectionReply(inFlightUi);
1713 inFlightUi.setResult(72, 2, []); 1771 inFlightUi.setResult(72, 2, []);
1714 inFlightUi.tile.onSelectionReply(); 1772 inFlightUi.tile.onSelectionReply();
1715 inFlightWrite.resolve(); 1773 inFlightWrite.resolve();
@@ -1730,6 +1788,7 @@ async function verifySelectionShell(shell, html) {
1730 timerUi.tile.selection = { 1788 timerUi.tile.selection = {
1731 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 73, text: null, 1789 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 73, text: null,
1732 }; 1790 };
1791 authorizeSelectionReply(timerUi);
1733 timerUi.setResult(73, 1, []); 1792 timerUi.setResult(73, 1, []);
1734 timerUi.tile.onSelectionReply(); 1793 timerUi.tile.onSelectionReply();
1735 oldClipboardTimer.fn(); 1794 oldClipboardTimer.fn();
@@ -1743,6 +1802,7 @@ async function verifySelectionShell(shell, html) {
1743 clearedUi.tile.selection = { 1802 clearedUi.tile.selection = {
1744 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 74, text: null, 1803 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 74, text: null,
1745 }; 1804 };
1805 authorizeSelectionReply(clearedUi);
1746 clearedUi.setResult(74, 3, []); 1806 clearedUi.setResult(74, 3, []);
1747 clearedUi.tile.onSelectionReply(); 1807 clearedUi.tile.onSelectionReply();
1748 clearedUi.tile.clearSelection(false); 1808 clearedUi.tile.clearSelection(false);
@@ -1752,8 +1812,10 @@ async function verifySelectionShell(shell, html) {
1752 successfulUi.tile.selection = { 1812 successfulUi.tile.selection = {
1753 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 75, text: null, 1813 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 75, text: null,
1754 }; 1814 };
1815 authorizeSelectionReply(successfulUi);
1755 successfulUi.setResult(75, 3, []); 1816 successfulUi.setResult(75, 3, []);
1756 successfulUi.tile.onSelectionReply(); 1817 successfulUi.tile.onSelectionReply();
1818 authorizeSelectionReply(successfulUi);
1757 successfulUi.setResult(75, 0, Buffer.from('available')); 1819 successfulUi.setResult(75, 0, Buffer.from('available'));
1758 successfulUi.tile.onSelectionReply(); 1820 successfulUi.tile.onSelectionReply();
1759 check('successful selection removes stale unavailable UI', `${successfulUi.tile.copyButton.className}|${successfulUi.tile.copyButton.textContent}`, 'copy-request|Copy'); 1821 check('successful selection removes stale unavailable UI', `${successfulUi.tile.copyButton.className}|${successfulUi.tile.copyButton.textContent}`, 'copy-request|Copy');
@@ -2061,6 +2123,7 @@ async function verifySelectionShell(shell, html) {
2061 newSelectionUi.tile.selection = { 2123 newSelectionUi.tile.selection = {
2062 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 76, text: null, 2124 anchor: { row: 0, col: 0 }, active: { row: 0, col: 1 }, requestId: 76, text: null,
2063 }; 2125 };
2126 authorizeSelectionReply(newSelectionUi);
2064 newSelectionUi.setResult(76, 3, []); 2127 newSelectionUi.setResult(76, 3, []);
2065 newSelectionUi.tile.onSelectionReply(); 2128 newSelectionUi.tile.onSelectionReply();
2066 const oldUnavailableHandle = newSelectionUi.tile.selectionFeedbackTimer; 2129 const oldUnavailableHandle = newSelectionUi.tile.selectionFeedbackTimer;