fix(native-chat): reconcile attachment paths with pending messages (#27095)

Co-authored-by: lianshuang-photo <188543100+lianshuang-photo@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-09 19:28:37 -07:00
committed by GitHub
co-authored by lianshuang-photo
parent b9abfa9116
commit 13d8ca87dd
3 changed files with 62 additions and 10 deletions
@@ -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<string, number> {
const counts = new Map<string, number>()
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<string, number> {
const advanced = new Map<string, number>()
const waiting = new Map<string, number>()
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)
}
@@ -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)
@@ -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)