From eef49d33953624b0e5884f71120081834d6c1059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 28 Jul 2026 22:09:48 +0800 Subject: [PATCH] fix(desktop): keep image-only messages in turn order #1095 --- desktop/src/stores/chatStore.test.ts | 50 ++++++++++++++++++++++++++++ desktop/src/stores/chatStore.ts | 14 +++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index 15d97b1b..ae246f11 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -5559,6 +5559,56 @@ describe('chatStore history mapping', () => { expect(userMessages).toHaveLength(1) }) + it('keeps an image-only replay before the assistant response', () => { + useChatStore.setState({ + sessions: { + [TEST_SESSION_ID]: makeSession({ + messages: [ + { + id: 'live-user', + type: 'user_text', + content: '', + attachments: [{ + type: 'image', + name: 'screenshot.png', + data: 'data:image/png;base64,AAAA', + mimeType: 'image/png', + }], + timestamp: 1, + }, + { + id: 'assistant-answer', + type: 'assistant_text', + content: 'The screenshot shows a repository list.', + timestamp: 2, + }, + ], + chatState: 'thinking', + }), + }, + }) + + useChatStore.getState().handleServerMessage(TEST_SESSION_ID, { + type: 'user_message_replay', + content: [ + 'Please analyze the attached image.', + '[Image source: /Users/me/.claude/uploads/session/screenshot.png]', + ].join('\n'), + }) + + expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([ + { + type: 'user_text', + content: '', + attachments: [{ type: 'image', name: 'screenshot.png' }], + }, + { + type: 'assistant_text', + content: 'The screenshot shows a repository list.', + }, + ]) + }) + it('dedupes a path-backed image after the server inlines it into replay content', () => { const prompt = '检查这张截图' const imagePath = 'C:\\Users\\tester\\Desktop\\screen.png' diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index c4e0f114..2c0638e0 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -3749,6 +3749,7 @@ function extractRestoredUserDisplay(text: string): RestoredUserDisplay { // ConversationService stores data-only files as `${randomUUID()}-${sanitizedName}` // and omits successfully inlined images from the replayed text content. const MATERIALIZED_UPLOAD_NAME_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}-(.+)$/i +const IMAGE_ONLY_REPLAY_FALLBACK = 'Please analyze the attached image.' function isLikelyInlineImageAttachment(attachment: UIAttachment): boolean { if (attachment.type === 'image') return true @@ -3800,12 +3801,23 @@ function replayMatchesCurrentUserMessage( const currentModelContent = (message.modelContent ?? message.content).trim() if (currentModelContent === replayModelContent) return true + const currentAttachments = message.attachments ?? [] + if ( + message.content.trim() === '' && + replayDisplay.content.trim() === IMAGE_ONLY_REPLAY_FALLBACK && + !replayDisplay.attachments?.length && + currentAttachments.length > 0 && + currentAttachments.every(isLikelyInlineImageAttachment) + ) { + return true + } + const currentDisplay = extractRestoredUserDisplay(currentModelContent) if (currentDisplay.content.trim() !== replayDisplay.content.trim()) return false return replayAttachmentsMatchCurrent( replayDisplay.attachments ?? [], - message.attachments ?? [], + currentAttachments, ) }