mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
co-authored by
lianshuang-photo
parent
b9abfa9116
commit
13d8ca87dd
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user