diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index a94109a0273..b1180f4c81d 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -3416,11 +3416,19 @@ export default function SessionScreen() { }, []) const startAccessoryRepeat = useCallback( (input: ReturnType) => { + // Why: the controller queue sits outside RPC, so fence pre-outage taps from a later connection. + const targetClient = clientRef.current + const targetConnectedAt = targetClient?.getLastConnectedAt() ?? null accessoryRepeatRef.current.start( input, createTerminalAccessoryRepeatSender( activeHandleRef.current, - () => activeHandleRef.current, + (targetHandle) => + activeHandleRef.current === targetHandle && + targetClient !== null && + clientRef.current === targetClient && + connStateRef.current === 'connected' && + targetClient.getLastConnectedAt() === targetConnectedAt, (nextInput, targetHandle) => handleAccessoryKeyRef.current(nextInput, targetHandle) ) ) diff --git a/mobile/src/terminal/terminal-accessory-repeat.test.ts b/mobile/src/terminal/terminal-accessory-repeat.test.ts index 3492aebfb6b..efc3364c035 100644 --- a/mobile/src/terminal/terminal-accessory-repeat.test.ts +++ b/mobile/src/terminal/terminal-accessory-repeat.test.ts @@ -155,7 +155,7 @@ describe('terminal accessory repeat', () => { const sendToTerminal = vi.fn(async () => true) const send = createTerminalAccessoryRepeatSender( activeTerminal, - () => activeTerminal, + (targetHandle) => activeTerminal === targetHandle, sendToTerminal ) @@ -166,4 +166,35 @@ describe('terminal accessory repeat', () => { expect(sendToTerminal).toHaveBeenCalledTimes(1) expect(sendToTerminal).toHaveBeenCalledWith('down', 'terminal-a') }) + + it('drops a queued send when its press-time connection is no longer current', async () => { + vi.useFakeTimers() + let connectionGeneration = 1 + const first = deferred() + const sent: string[] = [] + const sendToTerminal = vi.fn((input: string) => { + sent.push(input) + return input === 'down' ? first.promise.then(() => true) : Promise.resolve(true) + }) + const createSender = () => { + const pressedConnectionGeneration = connectionGeneration + return createTerminalAccessoryRepeatSender( + 'terminal-a', + () => connectionGeneration === pressedConnectionGeneration, + sendToTerminal + ) + } + const repeat = createTerminalAccessoryRepeatController() + + repeat.start('down', createSender()) + repeat.stop() + repeat.start('up', createSender()) + repeat.stop() + + connectionGeneration = 2 + first.resolve() + await vi.runAllTimersAsync() + + expect(sent).toEqual(['down']) + }) }) diff --git a/mobile/src/terminal/terminal-accessory-repeat.ts b/mobile/src/terminal/terminal-accessory-repeat.ts index 76529bda36c..9ed10af12d2 100644 --- a/mobile/src/terminal/terminal-accessory-repeat.ts +++ b/mobile/src/terminal/terminal-accessory-repeat.ts @@ -5,11 +5,11 @@ type TerminalAccessoryRepeatSender = (input: TInput) => Promise export function createTerminalAccessoryRepeatSender( targetHandle: string | null, - getActiveHandle: () => string | null, + isDeliveryTargetCurrent: (targetHandle: string) => boolean, sendToTerminal: (input: TInput, targetHandle: string) => Promise ): TerminalAccessoryRepeatSender { return (input) => { - if (!targetHandle || getActiveHandle() !== targetHandle) { + if (!targetHandle || !isDeliveryTargetCurrent(targetHandle)) { return Promise.resolve(false) } return sendToTerminal(input, targetHandle)