From db58121b57eed6e76312b3ae87afe6c0b05046d2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:56:00 -0700 Subject: [PATCH] fix(mobile): stop the chat pre-clear from wiping a just-pasted image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text write set clearInputFirst unconditionally. On the image path that Ctrl+U lands AFTER pasteMobileNativeChatImagePaths already pasted the image, so the agent receives the text alone while acceptSend still renders the thumbnail on the sent bubble — silent image loss. Desktop's image path clears exactly once, before the paste, and never again; mobile now matches: pre-clear only when nothing was deliberately pasted first. The image paste already leads with its own Ctrl+U, so a launch-draft prefill parked on the input line still cannot glue onto the message. Pinned at both levels: the controller test drives the real send hook and asserts clearInputFirst per branch, and the send module asserts the wire text carries no leading \x15. The image-attachments test injects its own baseSend, so it structurally could not observe this. --- .../session/mobile-native-chat-send.test.ts | 21 +++++++++++++++++++ .../use-mobile-native-chat-controller.test.ts | 21 +++++++++++++++++++ .../use-mobile-native-chat-message-send.ts | 11 ++++++---- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/mobile/src/session/mobile-native-chat-send.test.ts b/mobile/src/session/mobile-native-chat-send.test.ts index eaaa7aaef25..1f3d57985ac 100644 --- a/mobile/src/session/mobile-native-chat-send.test.ts +++ b/mobile/src/session/mobile-native-chat-send.test.ts @@ -194,6 +194,27 @@ describe('sendMobileNativeChatMessage', () => { ) }) + it('sends the text verbatim when clearInputFirst is not set', async () => { + // An image send pastes the image (behind its own leading Ctrl+U) before this + // text write; a clear byte here would kill the pasted image off the input line. + const client = clientWithResponse({ + id: 'request', + ok: true, + result: { send: { accepted: true } }, + _meta: { runtimeId: 'runtime' } + }) + + await sendMobileNativeChatMessage({ + client, + terminal: 'term', + text: 'what is this', + clearInputFirst: false + }) + const sent = vi.mocked(client.sendRequest).mock.calls[0]?.[1] as { text: string } + expect(sent.text).toBe('what is this') + expect(sent.text.startsWith('\x15')).toBe(false) + }) + it('sends a single non-submitting Escape for prompt cancellation', async () => { const client = clientWithResponse({ id: 'request', 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 7118d23321b..1569fc9fc30 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.test.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.test.ts @@ -213,6 +213,27 @@ describe('useMobileNativeChatController handleNativeChatSend', () => { expect(restoreRejectedDraft).not.toHaveBeenCalled() }) + it('pre-clears the input line for a text-only send but never for an image send', async () => { + // The image path pastes the image behind its OWN leading Ctrl+U and then calls + // this send; a second clear here wipes the image off the input line and the + // agent receives text alone while the echo bubble still shows the thumbnail. + sendWithOutcome.mockResolvedValue('accepted') + + await act(async () => { + await controller!.handleNativeChatSend('answer') + }) + expect(sendWithOutcome).toHaveBeenLastCalledWith( + expect.objectContaining({ text: 'answer', clearInputFirst: true }) + ) + + await act(async () => { + await controller!.handleNativeChatSend('look', ['file:///a.jpg']) + }) + expect(sendWithOutcome).toHaveBeenLastCalledWith( + expect.objectContaining({ text: 'look', clearInputFirst: false }) + ) + }) + it('holds an unknown-outcome send without posting the optimistic echo', async () => { sendWithOutcome.mockResolvedValue('unknown') let accepted = false diff --git a/mobile/src/session/use-mobile-native-chat-message-send.ts b/mobile/src/session/use-mobile-native-chat-message-send.ts index 49b537213ed..366dd426570 100644 --- a/mobile/src/session/use-mobile-native-chat-message-send.ts +++ b/mobile/src/session/use-mobile-native-chat-message-send.ts @@ -99,10 +99,13 @@ export function useMobileNativeChatMessageSend(args: { client, terminal: handle, text, - // Why: desktop-parity pre-clear. The stale-input heal above only fires for - // terminals a mobile image paste marked, so a desktop launch-draft prefill - // sitting on the input line would otherwise glue onto this message. - clearInputFirst: true, + // Why: pre-clear only when nothing was deliberately pasted first. The heal + // above fires only for terminals a mobile image paste marked, so a desktop + // launch-draft prefill parked on the input line would otherwise glue onto + // this message. An image send already led its own paste with Ctrl+U, and a + // second one here would wipe the image it just pasted (desktop's image path + // likewise clears once, before the paste, and never again). + clearInputFirst: !images?.length, deadline, ...(deviceTokenRef.current ? { mobileClient: { id: deviceTokenRef.current, type: 'mobile' } }