fix(desktop): dedupe optimistic user turns for all wire references, not only images
This commit is contained in:
committed by
kshitij
parent
b9b0505cfc
commit
3060beea07
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { WIRE_REFERENCE_KINDS } from '@/components/assistant-ui/reference-kinds'
|
||||
import type { ChatMessage } from '@/lib/chat-messages'
|
||||
import { $approvalModes, approvalModeForProfile } from '@/store/approval-mode'
|
||||
import { $desktopOnboarding } from '@/store/onboarding'
|
||||
@@ -575,6 +576,46 @@ describe('preserveLocalPendingTurnMessages', () => {
|
||||
expect(preserveLocalPendingTurnMessages(next, previous)).toBe(next)
|
||||
})
|
||||
|
||||
it('does not duplicate the optimistic file turn when the persisted turn carries @file refs', () => {
|
||||
const previous = [
|
||||
msg('1-user', 'user', 'first'),
|
||||
msg('2-assistant', 'assistant', 'first answer'),
|
||||
msg('user-optimistic', 'user', 'text', {
|
||||
attachmentRefs: ['@file:X']
|
||||
})
|
||||
]
|
||||
|
||||
const next = [
|
||||
msg('1-user-stored', 'user', 'first'),
|
||||
msg('2-assistant-stored', 'assistant', 'first answer'),
|
||||
msg('3-user-stored', 'user', '@file:X\n\ntext')
|
||||
]
|
||||
|
||||
expect(preserveLocalPendingTurnMessages(next, previous)).toBe(next)
|
||||
})
|
||||
|
||||
it.each(WIRE_REFERENCE_KINDS.filter(kind => kind !== 'file' && kind !== 'image'))(
|
||||
'does not duplicate the optimistic %s turn when the persisted turn carries its directive',
|
||||
kind => {
|
||||
const ref = `@${kind}:X`
|
||||
const previous = [
|
||||
msg('1-user', 'user', 'first'),
|
||||
msg('2-assistant', 'assistant', 'first answer'),
|
||||
msg('user-optimistic', 'user', 'text', {
|
||||
attachmentRefs: [ref]
|
||||
})
|
||||
]
|
||||
|
||||
const next = [
|
||||
msg('1-user-stored', 'user', 'first'),
|
||||
msg('2-assistant-stored', 'assistant', 'first answer'),
|
||||
msg('3-user-stored', 'user', `${ref}\n\ntext`)
|
||||
]
|
||||
|
||||
expect(preserveLocalPendingTurnMessages(next, previous)).toBe(next)
|
||||
}
|
||||
)
|
||||
|
||||
it('still keeps a genuinely uncommitted optimistic image turn when the persisted text differs', () => {
|
||||
const previous = [
|
||||
msg('1-user', 'user', 'first'),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getSession } from '@/hermes'
|
||||
import { textWithoutReferenceLines } from '@/components/assistant-ui/reference-kinds'
|
||||
import { assistantTextPart, type ChatMessage, chatMessageText, textPart } from '@/lib/chat-messages'
|
||||
import { normalizePersonalityValue } from '@/lib/chat-runtime'
|
||||
import { embeddedImageUrls, textWithoutEmbeddedImages, textWithoutImageRefs } from '@/lib/embedded-images'
|
||||
import { embeddedImageUrls, textWithoutEmbeddedImages } from '@/lib/embedded-images'
|
||||
import { reconcileApprovalModeForProfile } from '@/store/approval-mode'
|
||||
import { requestDesktopOnboardingForCredentialWarning } from '@/store/onboarding'
|
||||
import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/profile'
|
||||
@@ -403,7 +404,8 @@ export function preserveLocalPendingTurnMessages(
|
||||
if (
|
||||
isOptimisticUser &&
|
||||
latestAuthoritativeUser &&
|
||||
textWithoutImageRefs(chatMessageText(latestAuthoritativeUser)) === textWithoutImageRefs(chatMessageText(message))
|
||||
textWithoutReferenceLines(chatMessageText(latestAuthoritativeUser)) ===
|
||||
textWithoutReferenceLines(chatMessageText(message))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -415,7 +417,9 @@ export function preserveLocalPendingTurnMessages(
|
||||
continue
|
||||
}
|
||||
|
||||
if (textWithoutImageRefs(chatMessageText(authoritative)) === textWithoutImageRefs(chatMessageText(message))) {
|
||||
if (
|
||||
textWithoutReferenceLines(chatMessageText(authoritative)) === textWithoutReferenceLines(chatMessageText(message))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -484,7 +488,9 @@ export function appendLiveSessionProjection(
|
||||
}
|
||||
|
||||
const persistedInLatestRun = (text: string): boolean =>
|
||||
latestUserRun.some(message => textWithoutImageRefs(chatMessageText(message)) === textWithoutImageRefs(text))
|
||||
latestUserRun.some(
|
||||
message => textWithoutReferenceLines(chatMessageText(message)) === textWithoutReferenceLines(text)
|
||||
)
|
||||
|
||||
const inflightUserAlreadyPersisted = Boolean(inflightUser) && persistedInLatestRun(inflightUser)
|
||||
|
||||
|
||||
@@ -157,3 +157,20 @@ const REFERENCE_PATTERN = /@(file|folder|url|image|tool|line|terminal|session):(
|
||||
export function referenceRe(): RegExp {
|
||||
return new RegExp(REFERENCE_PATTERN.source, 'g')
|
||||
}
|
||||
|
||||
/** Remove reference-only lines when comparing visible message text. */
|
||||
export function textWithoutReferenceLines(text: string): string {
|
||||
const matcher = referenceRe()
|
||||
|
||||
return text
|
||||
.split('\n')
|
||||
.filter(line => {
|
||||
const candidate = line.trimEnd()
|
||||
matcher.lastIndex = 0
|
||||
const match = matcher.exec(candidate)
|
||||
|
||||
return !match || match.index !== 0 || match[0] !== candidate
|
||||
})
|
||||
.join('\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user