From f705721fede780d3871dc7bc854c6d500cb1cc3d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:52:14 -0700 Subject: [PATCH] fix(mobile): serialize accessory key presses --- .../terminal-accessory-repeat.test.ts | 22 ++++++++++++++++ .../src/terminal/terminal-accessory-repeat.ts | 26 +++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/mobile/src/terminal/terminal-accessory-repeat.test.ts b/mobile/src/terminal/terminal-accessory-repeat.test.ts index eac77cf5439..8dec420ff48 100644 --- a/mobile/src/terminal/terminal-accessory-repeat.test.ts +++ b/mobile/src/terminal/terminal-accessory-repeat.test.ts @@ -60,6 +60,28 @@ describe('terminal accessory repeat', () => { expect(send).toHaveBeenCalledTimes(1) }) + it('serializes a new press behind the previous press send', async () => { + vi.useFakeTimers() + const first = deferred() + const sent: string[] = [] + const send = vi.fn((input: string) => { + sent.push(input) + return input === 'down' ? first.promise.then(() => true) : Promise.resolve(true) + }) + const repeat = createTerminalAccessoryRepeatController() + + repeat.start('down', send) + repeat.stop() + repeat.start('up', send) + + expect(sent).toEqual(['down']) + + first.resolve() + await vi.advanceTimersByTimeAsync(0) + expect(sent).toEqual(['down', 'up']) + repeat.stop() + }) + it('stops repeating when the transport rejects a send', async () => { vi.useFakeTimers() const send = vi.fn(() => Promise.reject(new Error('disconnected'))) diff --git a/mobile/src/terminal/terminal-accessory-repeat.ts b/mobile/src/terminal/terminal-accessory-repeat.ts index 8dc571ffe89..15071f4e035 100644 --- a/mobile/src/terminal/terminal-accessory-repeat.ts +++ b/mobile/src/terminal/terminal-accessory-repeat.ts @@ -19,6 +19,28 @@ export function createTerminalAccessoryRepeatSender( export function createTerminalAccessoryRepeatController() { let generation = 0 let timer: ReturnType | null = null + let dispatchTail: Promise | null = null + + const dispatch = ( + input: TInput, + send: TerminalAccessoryRepeatSender, + activeGeneration: number + ) => { + const sendIfCurrent = () => + generation === activeGeneration ? send(input) : Promise.resolve(false) + const result = dispatchTail ? dispatchTail.then(sendIfCurrent) : sendIfCurrent() + const settled = result.then( + () => undefined, + () => undefined + ) + dispatchTail = settled + void settled.then(() => { + if (dispatchTail === settled) { + dispatchTail = null + } + }) + return result + } const stop = () => { generation += 1 @@ -39,7 +61,7 @@ export function createTerminalAccessoryRepeatController() { if (generation !== activeGeneration) { return } - void send(input).then( + void dispatch(input, send, activeGeneration).then( (sent) => { if (sent && generation === activeGeneration) { schedule(TERMINAL_ACCESSORY_REPEAT_INTERVAL_MS) @@ -50,7 +72,7 @@ export function createTerminalAccessoryRepeatController() { }, delayMs) } - void send(input).then( + void dispatch(input, send, activeGeneration).then( (sent) => { if (sent && generation === activeGeneration) { schedule(