fix(mobile-chat): require image provenance for glued prefix matches

This commit is contained in:
Merge Sim
2026-08-31 21:30:15 -07:00
parent 36d798e5bb
commit 2f555ff263
2 changed files with 30 additions and 1 deletions
@@ -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]'),
@@ -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<string>()
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