diff --git a/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts b/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts index 5084448ea4a..4447e3511ee 100644 --- a/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts +++ b/mobile/src/session/mobile-native-chat-draft-reconcile.test.ts @@ -73,6 +73,19 @@ describe('mobile native chat image preview reconciliation', () => { expect(findLandedImagePreviewEchoes(messages, [preview])).toEqual([]) }) + it('does not bind a glued image echo to an ordinary row with the same prefix', () => { + const messages = [ + userText('ordinary', 'look at this later'), + userText('source', '[Image: source: /tmp/a.png]'), + userText('prompt', 'look at this[Image #1] is it still working?') + ] + const preview = { ...pending('pending', ['file:///a.jpg']), text: 'look at this' } + + expect(findLandedImagePreviewEchoes(messages, [preview])).toEqual([ + { pendingId: 'pending', messageId: 'prompt', images: ['file:///a.jpg'] } + ]) + }) + it('reconciles a middle-marker echo without changing its rendered whitespace', () => { const messages = [ userText('source', '[Image: source: /tmp/a.png]'), diff --git a/mobile/src/session/mobile-native-chat-draft-reconcile.ts b/mobile/src/session/mobile-native-chat-draft-reconcile.ts index 17fcb343198..6c2c8c0286d 100644 --- a/mobile/src/session/mobile-native-chat-draft-reconcile.ts +++ b/mobile/src/session/mobile-native-chat-draft-reconcile.ts @@ -153,6 +153,20 @@ export function findLandedImagePreviewEchoes( ): LandedImagePreviewEcho[] { const normalized = normalizeImageTranscriptMessages(messages) const messageIndexById = new Map(normalized.map((message, index) => [message.id, index])) + // Keep provenance from the raw transcript: normalization removes image markers, + // so a plain text row must not become a candidate merely because it shares a + // caption prefix with a glued image send. + const imageMessageIds = new Set( + messages + .filter( + (message) => + message.role === 'user' && + (isImageSourceUserTurn(message) || + hasImagePromptMarker(message) || + message.blocks.some(isImageRefBlock)) + ) + .map((message) => message.id) + ) const claimedMessageIds = new Set() const landed: LandedImagePreviewEcho[] = [] @@ -175,7 +189,9 @@ export function findLandedImagePreviewEchoes( // text-only send lands in a row whose text is the concatenation. Requiring the // whole row to equal this echo left it unmatched, and since both other // retirement paths skip image echoes, nothing could ever retire it. - return text === targetText || text.startsWith(targetText) + return ( + text === targetText || (imageMessageIds.has(message.id) && text.startsWith(targetText)) + ) } const imageCount = message.blocks.filter(isImageRefBlock).length return message.blocks.length === 0 || imageCount >= entry.images!.length