From 13d8ca87ddb775b8796b777f84910656fab512df Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 9 Oct 2026 19:28:37 -0700 Subject: [PATCH] fix(native-chat): reconcile attachment paths with pending messages (#27095) Co-authored-by: lianshuang-photo <188543100+lianshuang-photo@users.noreply.github.com> --- .../native-chat-pending-occurrence.ts | 29 ++++++++++++---- .../native-chat/native-chat-pending.test.ts | 33 +++++++++++++++++++ .../native-chat/native-chat-pending.ts | 10 +++--- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts b/src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts index bdabded3051..3a998b9f37f 100644 --- a/src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts +++ b/src/renderer/src/components/native-chat/native-chat-pending-occurrence.ts @@ -29,11 +29,26 @@ export function nativeChatPendingContentKey( return imagePaths.length > 0 ? `images:${JSON.stringify(imagePaths)}` : 'empty' } -function nativeChatUserMessageContentKey(message: NativeChatMessage): string | null { +function nativeChatUserMessageContentKey( + message: NativeChatMessage, + sentImagePaths?: readonly string[] +): string | null { if (message.role !== 'user') { return null } - const text = normalizedNativeChatUserMessageText(message) ?? '' + let text = normalizedNativeChatUserMessageText(message) ?? '' + // Some CLIs retain pasted attachment paths as prompt text instead of image markers. + const paths = sentImagePaths?.filter(Boolean) ?? [] + const normalizedPaths = paths.map(normalizeNativeChatPendingText) + for (const prefix of [normalizedPaths.join(' '), normalizedPaths.join('')]) { + if (prefix && (text === prefix || text.startsWith(`${prefix} `))) { + text = text.slice(prefix.length).trimStart() + if (!text) { + return nativeChatPendingContentKey({ text: '', imagePaths: paths }) + } + break + } + } if (text) { return `text:${text}` } @@ -46,11 +61,12 @@ function nativeChatUserMessageContentKey(message: NativeChatMessage): string | n } export function matchingNativeChatUserContentCounts( - messages: readonly NativeChatMessage[] + messages: readonly NativeChatMessage[], + sentImagePaths?: readonly string[] ): Map { const counts = new Map() for (const message of messages) { - const key = nativeChatUserMessageContentKey(message) + const key = nativeChatUserMessageContentKey(message, sentImagePaths) if (key) { counts.set(key, (counts.get(key) ?? 0) + 1) } @@ -59,13 +75,14 @@ export function matchingNativeChatUserContentCounts( } export function advancedNativeChatUserContentCounts( - messages: readonly NativeChatMessage[] + messages: readonly NativeChatMessage[], + sentImagePaths?: readonly string[] ): Map { const advanced = new Map() const waiting = new Map() for (const message of messages) { if (message.role === 'user') { - const key = nativeChatUserMessageContentKey(message) + const key = nativeChatUserMessageContentKey(message, sentImagePaths) if (key) { waiting.set(key, (waiting.get(key) ?? 0) + 1) } diff --git a/src/renderer/src/components/native-chat/native-chat-pending.test.ts b/src/renderer/src/components/native-chat/native-chat-pending.test.ts index 496b4304fb8..df9fd93c251 100644 --- a/src/renderer/src/components/native-chat/native-chat-pending.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-pending.test.ts @@ -56,6 +56,39 @@ function imageMessage(id: string, ...paths: string[]): NativeChatMessage { const pendingOf = (id: string, text: string): NativeChatPendingSend => ({ id, text, sentAt: 100 }) describe('prunePendingSends', () => { + it.each(['/remote/.orca/drops/Screenshot 2026-09-20 at 18.25.43.png', '/tmp/local photo.png'])( + 'reconciles a pasted attachment path recorded as text: %s', + (path) => { + const pending = [ + { ...pendingOf('pending', '看一下'), imagePaths: [path], afterMessageId: null } + ] + const user = userMessage('real', `${path} 看一下`, 101) + expect(pendingSendsAsMessages(pending, [user])).toEqual([]) + expect(prunePendingSends(pending, [user])).toEqual(pending) + expect(prunePendingSends(pending, [user, assistantMessage('reply', '好的', 102)])).toEqual([]) + expect( + prunePendingSends(pending, [ + userMessage('unrelated', `${path}.other 看一下`, 101), + assistantMessage('reply', 'ok', 102) + ]) + ).toEqual(pending) + } + ) + + it('reconciles image-only and multiple attachments without consuming a prior turn', () => { + const paths = ['/remote/a.png', '/remote/b.png'] + const pending = [{ ...pendingOf('pending', ''), imagePaths: paths, afterMessageId: 'old' }] + const old = userMessage('old', paths.join(' '), 99) + expect(prunePendingSends(pending, [old, assistantMessage('old-reply', 'old', 100)])).toEqual( + pending + ) + const messages = [ + old, + userMessage('new', paths.join(''), 101), + assistantMessage('reply', 'ok', 102) + ] + expect(prunePendingSends(pending, messages)).toEqual([]) + }) it('returns the same reference when there is nothing pending', () => { const pending: NativeChatPendingSend[] = [] expect(prunePendingSends(pending, [userMessage('m1', 'hi')])).toBe(pending) diff --git a/src/renderer/src/components/native-chat/native-chat-pending.ts b/src/renderer/src/components/native-chat/native-chat-pending.ts index ee492316f52..9702954b563 100644 --- a/src/renderer/src/components/native-chat/native-chat-pending.ts +++ b/src/renderer/src/components/native-chat/native-chat-pending.ts @@ -191,9 +191,10 @@ export function prunePendingSends( const contentKey = nativeChatPendingContentKey(entry) const key = nativeChatPendingMatchKey(entry) const available = - advancedNativeChatUserContentCounts(messagesAfterPendingBoundary(messages, entry)).get( - contentKey - ) ?? 0 + advancedNativeChatUserContentCounts( + messagesAfterPendingBoundary(messages, entry), + entry.imagePaths + ).get(contentKey) ?? 0 const used = consumed.get(key) ?? 0 const occurrence = nativeChatPendingOccurrence(entry, used) consumed.set(key, Math.max(used, occurrence)) @@ -236,7 +237,8 @@ export function pendingSendsAsMessages( const key = nativeChatPendingMatchKey(entry) const represented = matchingNativeChatUserContentCounts( - messagesAfterPendingBoundary(existingMessages, entry) + messagesAfterPendingBoundary(existingMessages, entry), + entry.imagePaths ).get(contentKey) ?? 0 const used = consumed.get(key) ?? 0 const occurrence = nativeChatPendingOccurrence(entry, used)