fix(mobile): isolate question answers from composer drafts

This commit is contained in:
Brennan Benson
2026-07-23 17:14:23 -07:00
parent 5289b1c772
commit 6f4bd5b6c7
3 changed files with 32 additions and 8 deletions
@@ -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}
@@ -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')
})
})
@@ -58,6 +58,7 @@ export type MobileNativeChatController = {
handleNativeChatStop: () => void
nativeChatFilePaths: string[]
loadNativeChatFiles: (query: string) => void
handleNativeChatQuestionAnswer: (text: string) => Promise<boolean>
handleNativeChatSend: (text: string, images?: string[]) => Promise<boolean>
}
@@ -226,18 +227,17 @@ export function useMobileNativeChatController(args: {
worktreeId
})
const handleNativeChatSend = useCallback(
async (text: string, images?: string[]): Promise<boolean> => {
const sendNativeChatMessage = useCallback(
async (text: string, images: string[] | undefined, syncComposer: boolean): Promise<boolean> => {
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
}
}