From 110c4696ad60cfe2f5e1a3ba193952c04ebffe45 Mon Sep 17 00:00:00 2001 From: DodgyBadger <4453217+DodgyBadger@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:09:01 +0000 Subject: [PATCH] fix(mobile): swallow blank-space long presses --- src/web/public/terminal-ui.js | 20 +++++++++----------- test/terminal-touch-tap.test.ts | 15 ++++++++++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index c1eacb7b..9538b5ce 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -1934,24 +1934,22 @@ Object.assign(CodemanApp.prototype, { this.terminal?.select?.(index % cols, Math.floor(index / cols), length); }, - /** Long-press fired: select the word under the finger and arm drag-to-extend. */ + /** Long-press fired: swallow the platform gesture, then select a word if one exists. */ _beginTouchSelection(clientX, clientY) { + // Reaching the 350ms threshold makes this a long press even when the finger + // landed on blank space. Arm every guard before looking for a word so Chrome + // cannot focus xterm's hidden textarea, and leave _touchSelecting set so the + // touchend branch preventDefaults the compatibility mouse sequence. + this._blurMobileTerminalInput(); + this._suppressTrustedTapMouseEvents(); + this._armTouchSelectionFocusGuard(); + this._touchSelecting = true; const cell = this._touchSelectionCellAt(clientX, clientY); if (!cell) return false; const word = this._touchSelectionWordAt(cell); if (!word) return false; - // The keyboard must not sit on top of the thing being selected, and the - // composer would eat the selection on its next keystroke anyway. - this._blurMobileTerminalInput(); this._touchSelectionAnchor = word; - this._touchSelecting = true; this._touchSelectionActive = true; - // From here until the gesture ends, no trusted mouse event may reach xterm — - // see _endTouchSelectionGesture for why — and the terminal input may not take - // focus. Both are re-armed as the gesture continues, since their windows are - // short and a press can be held for much longer. - this._suppressTrustedTapMouseEvents(); - this._armTouchSelectionFocusGuard(); this._applyTouchSelection(word.index, word.length); // Android answers; iOS ignores it silently. Both are fine. try { diff --git a/test/terminal-touch-tap.test.ts b/test/terminal-touch-tap.test.ts index 28705687..13bba1dc 100644 --- a/test/terminal-touch-tap.test.ts +++ b/test/terminal-touch-tap.test.ts @@ -987,11 +987,24 @@ describe('terminal touch selection', () => { expect(select).toHaveBeenCalledWith(PATH_AT, 0, '/tmp/out/chart.png'.length); }); - it('selects nothing when the press lands on blank space', () => { + it('swallows a blank-space long press without creating a selection', () => { const { app, select } = selectionHarness(); + app._blurMobileTerminalInput = vi.fn(); + app._suppressTrustedTapMouseEvents = vi.fn(); + app._armTouchSelectionFocusGuard = vi.fn(); + app._showTouchSelectionBar = vi.fn(); expect(press(app, LINE.length + 10)).toBe(false); + expect(app._blurMobileTerminalInput).toHaveBeenCalledOnce(); + expect(app._suppressTrustedTapMouseEvents).toHaveBeenCalledOnce(); + expect(app._armTouchSelectionFocusGuard).toHaveBeenCalledOnce(); + // touchend keys off this state to preventDefault the compatibility mouse + // sequence even though blank space produced no selection to keep alive. + expect(app._touchSelecting).toBe(true); + expect(app._touchSelectionActive).toBeFalsy(); + expect(app._touchSelectionAnchor).toBeUndefined(); expect(select).not.toHaveBeenCalled(); + expect(app._showTouchSelectionBar).not.toHaveBeenCalled(); }); it('grows the selection as the finger drags past the anchor word', () => {