mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): anchor marker stripping to a contiguous image-source run
Re-home the landed literal-text counts onto the draft reconciler after the #14665 revert removed the module they lived in, and cover the anchoring boundary: a marker the user typed must not absorb a later send's image.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
findLandedImagePreviewEchoes,
|
||||
findLandedUnconfirmedSends,
|
||||
migrateImagePreviewMessageIds,
|
||||
userTextOccurrenceCounts,
|
||||
type PendingImagePreviewEcho,
|
||||
type UnconfirmedSend
|
||||
} from './mobile-native-chat-draft-reconcile'
|
||||
@@ -234,3 +235,22 @@ describe('mobile native chat image preview reconciliation', () => {
|
||||
).toEqual({ [sessionKey]: { prompt: ['file:///a.jpg'] } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('userTextOccurrenceCounts', () => {
|
||||
it('counts a literal marker turn under its verbatim text', () => {
|
||||
expect(userTextOccurrenceCounts([userText('u1', 'keep [Image #1] literal')])).toEqual(
|
||||
new Map([['keep [Image #1] literal', 1]])
|
||||
)
|
||||
})
|
||||
|
||||
// An attached-image turn must not retire a send that carried no images.
|
||||
it('excludes a folded image turn from the literal landed counts', () => {
|
||||
const counts = userTextOccurrenceCounts([
|
||||
userText('s1', '[Image: source: /tmp/a.png]'),
|
||||
userText('p1', '[Image #1] look')
|
||||
])
|
||||
|
||||
expect(counts.get('look')).toBeUndefined()
|
||||
expect(counts.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,6 +49,24 @@ export function countUserTextOccurrences(
|
||||
return count
|
||||
}
|
||||
|
||||
/** Landed literal-text turns counted under the same key a markerless send uses.
|
||||
* Image turns are excluded so they cannot retire a send that carried none. */
|
||||
export function userTextOccurrenceCounts(
|
||||
messages: readonly NativeChatMessage[]
|
||||
): Map<string, number> {
|
||||
const counts = new Map<string, number>()
|
||||
for (const message of normalizeImageTranscriptMessages(messages)) {
|
||||
if (message.blocks.some(isImageRefBlock)) {
|
||||
continue
|
||||
}
|
||||
const text = nativeChatUserMessageMatchText(message)
|
||||
if (text) {
|
||||
counts.set(text, (counts.get(text) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
return counts
|
||||
}
|
||||
|
||||
/** Number of `[Image: source: …]` echo turns strictly after `tailId` (or the
|
||||
* whole transcript when the tail was paginated out). An image-only send has no
|
||||
* caption to match, so it reconciles by ordinal against this count — counting
|
||||
|
||||
@@ -217,6 +217,48 @@ describe('normalizeImageTranscriptMessages', () => {
|
||||
expect(out[2]).toBe(after)
|
||||
})
|
||||
|
||||
it('keeps a later literal marker when earlier turns separate it from a real image send', () => {
|
||||
const out = normalizeImageTranscriptMessages([
|
||||
userText('s1', '[Image: source: /tmp/a.jpg]'),
|
||||
userText('p1', '[Image #1] what is this?'),
|
||||
userText('u1', 'thanks'),
|
||||
userText('u2', 'in my notes I wrote [Image #2] as plain text')
|
||||
])
|
||||
|
||||
expect(out.at(-1)?.blocks).toEqual([
|
||||
{ type: 'text', text: 'in my notes I wrote [Image #2] as plain text' }
|
||||
])
|
||||
})
|
||||
|
||||
it('breaks the anchor when a non-image user turn separates the source run from the prompt', () => {
|
||||
const out = normalizeImageTranscriptMessages([
|
||||
userText('s1', '[Image: source: /tmp/a.jpg]'),
|
||||
userText('u1', 'here is the picture'),
|
||||
userText('u2', 'please preserve [Image #1] literally')
|
||||
])
|
||||
|
||||
expect(out.map((message) => message.blocks)).toEqual([
|
||||
[{ type: 'image-ref', path: '/tmp/a.jpg' }],
|
||||
[{ type: 'text', text: 'here is the picture' }],
|
||||
[{ type: 'text', text: 'please preserve [Image #1] literally' }]
|
||||
])
|
||||
})
|
||||
|
||||
// A marker the user typed must not absorb the image of the send that follows it.
|
||||
it('does not let a literal marker turn claim a later send’s image source', () => {
|
||||
const out = normalizeImageTranscriptMessages([
|
||||
userText('u1', 'remember I typed [Image #1] by hand'),
|
||||
userText('s1', '[Image: source: /tmp/a.jpg]'),
|
||||
userText('p1', '[Image #1] what is this?')
|
||||
])
|
||||
|
||||
expect(out[0]?.blocks).toEqual([{ type: 'text', text: 'remember I typed [Image #1] by hand' }])
|
||||
expect(out.at(-1)?.blocks).toEqual([
|
||||
{ type: 'image-ref', path: '/tmp/a.jpg' },
|
||||
{ type: 'text', text: 'what is this?' }
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves assistant messages untouched', () => {
|
||||
const assistant: NativeChatMessage = {
|
||||
id: 'a',
|
||||
|
||||
Reference in New Issue
Block a user