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, ) }