From 6f4bd5b6c7023fc2155e9de0c9ff2dd47c99d6d9 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:14:23 -0700 Subject: [PATCH] fix(mobile): isolate question answers from composer drafts --- .../src/session/MobileNativeChatOverlay.tsx | 2 +- .../use-mobile-native-chat-controller.test.ts | 13 ++++++++++ .../use-mobile-native-chat-controller.ts | 25 +++++++++++++------ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/mobile/src/session/MobileNativeChatOverlay.tsx b/mobile/src/session/MobileNativeChatOverlay.tsx index b1fcfd4c368..6277ed1a69b 100644 --- a/mobile/src/session/MobileNativeChatOverlay.tsx +++ b/mobile/src/session/MobileNativeChatOverlay.tsx @@ -48,7 +48,7 @@ export function MobileNativeChatOverlay({ onAnswerAsk={controller.handleNativeChatAnswerAsk} onCancelAsk={controller.handleNativeChatCancelAsk} question={controller.nativeChatQuestion} - onAnswerQuestion={controller.handleNativeChatSend} + onAnswerQuestion={controller.handleNativeChatQuestionAnswer} permission={controller.nativeChatPermission} onRespondPermission={controller.handleNativeChatRespondPermission} onOpenFile={controller.handleNativeChatOpenFile} diff --git a/mobile/src/session/use-mobile-native-chat-controller.test.ts b/mobile/src/session/use-mobile-native-chat-controller.test.ts index 487e0a4b2bb..012070fece0 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.test.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.test.ts @@ -149,4 +149,17 @@ describe('useMobileNativeChatController handleNativeChatSend', () => { // A definite rejection puts the optimistically-cleared text back. expect(restoreRejectedDraft).toHaveBeenCalledWith(ORIGIN, 'look') }) + + it('does not restore a rejected question answer into the composer', async () => { + sendWithOutcome.mockResolvedValue('rejected') + let accepted = true + await act(async () => { + accepted = await controller!.handleNativeChatQuestionAnswer('1') + }) + + expect(accepted).toBe(false) + expect(clearDraftForSend).not.toHaveBeenCalled() + expect(restoreRejectedDraft).not.toHaveBeenCalled() + expect(onSendError).toHaveBeenCalledWith('Message not sent') + }) }) diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index f3a963bb3da..7d9959dc42f 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -58,6 +58,7 @@ export type MobileNativeChatController = { handleNativeChatStop: () => void nativeChatFilePaths: string[] loadNativeChatFiles: (query: string) => void + handleNativeChatQuestionAnswer: (text: string) => Promise handleNativeChatSend: (text: string, images?: string[]) => Promise } @@ -226,18 +227,17 @@ export function useMobileNativeChatController(args: { worktreeId }) - const handleNativeChatSend = useCallback( - async (text: string, images?: string[]): Promise => { + const sendNativeChatMessage = useCallback( + async (text: string, images: string[] | undefined, syncComposer: boolean): Promise => { const handle = activeHandleRef.current const origin = captureSendOrigin(text) if (!client || !handle || !origin || !nativeChatInputLeaseReady) { onSendError('Message not sent (disconnected)') return false } - // Why: empty the composer at send time, not on the ack — over relay the - // round trip is visible, and a lost ack must not strand the sent prompt - // in the box. Only a definite rejection puts the text back. - clearDraftForSend(origin, text) + if (syncComposer) { + clearDraftForSend(origin, text) + } const outcome = await sendMobileNativeChatMessageWithOutcome({ client, terminal: handle, @@ -255,7 +255,9 @@ export function useMobileNativeChatController(args: { return true } if (outcome === 'rejected') { - restoreRejectedDraft(origin, text) + if (syncComposer) { + restoreRejectedDraft(origin, text) + } onSendError('Message not sent') return false } @@ -277,6 +279,14 @@ export function useMobileNativeChatController(args: { restoreRejectedDraft ] ) + const handleNativeChatQuestionAnswer = useCallback( + (text: string) => sendNativeChatMessage(text, undefined, false), + [sendNativeChatMessage] + ) + const handleNativeChatSend = useCallback( + (text: string, images?: string[]) => sendNativeChatMessage(text, images, true), + [sendNativeChatMessage] + ) return { isTabChatView, @@ -300,6 +310,7 @@ export function useMobileNativeChatController(args: { handleNativeChatStop, nativeChatFilePaths, loadNativeChatFiles, + handleNativeChatQuestionAnswer, handleNativeChatSend } }