mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(native-chat): require complete image echo evidence
(cherry picked from commit 94f1cc66ee)
This commit is contained in:
@@ -37,6 +37,7 @@ describe('mobile native chat image preview reconciliation', () => {
|
||||
pendingKey: 'pending-key',
|
||||
text: 'look at this',
|
||||
normalizedText: 'look at this',
|
||||
imageCount: 1,
|
||||
baselineTailMessageId: null,
|
||||
deadline: null
|
||||
}
|
||||
@@ -58,6 +59,7 @@ describe('mobile native chat image preview reconciliation', () => {
|
||||
pendingKey: 'pending-key',
|
||||
text: 'look here',
|
||||
normalizedText: 'look here',
|
||||
imageCount: 1,
|
||||
baselineTailMessageId: null,
|
||||
deadline: null
|
||||
}
|
||||
|
||||
@@ -3,15 +3,12 @@ import {
|
||||
countImagePromptMarkers,
|
||||
hasImagePromptMarker,
|
||||
isImageSourceUserTurn,
|
||||
nativeChatUserMessageImageEvidenceCount,
|
||||
nativeChatUserMessageMatchText,
|
||||
nativeChatUserTextMatchText,
|
||||
normalizeImageTranscriptMessages,
|
||||
normalizeNativeChatUserText,
|
||||
normalizedNativeChatUserMessageText
|
||||
} from './mobile-native-chat-image-transcript-markers'
|
||||
export {
|
||||
normalizeNativeChatUserText as normalizeReconcileText,
|
||||
normalizeNativeChatUserTextWithLiteralFallback as normalizeReconcileTextWithLiteralFallback
|
||||
} from './mobile-native-chat-image-transcript-markers'
|
||||
|
||||
/** An ack-lost ('unknown' outcome) send held until its transcript echo lands or
|
||||
* the deadline surfaces the uncertainty. */
|
||||
@@ -20,6 +17,7 @@ export type UnconfirmedSend = {
|
||||
pendingKey: string | null
|
||||
text: string
|
||||
normalizedText: string
|
||||
imageCount: number
|
||||
baselineTailMessageId: string | null
|
||||
deadline: ReturnType<typeof setTimeout> | null
|
||||
}
|
||||
@@ -30,11 +28,15 @@ export function normalizedUserText(message: NativeChatMessage): string | null {
|
||||
|
||||
export function countUserTextOccurrences(
|
||||
messages: readonly NativeChatMessage[],
|
||||
text: string
|
||||
text: string,
|
||||
imageCount = 0
|
||||
): number {
|
||||
let count = 0
|
||||
for (const message of messages) {
|
||||
if (nativeChatUserMessageMatchText(message) === text) {
|
||||
if (
|
||||
nativeChatUserMessageMatchText(message) === text &&
|
||||
nativeChatUserMessageImageEvidenceCount(message) >= imageCount
|
||||
) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
@@ -178,20 +180,24 @@ export function findLandedImagePreviewEchoes(
|
||||
if (!entry.images?.length) {
|
||||
continue
|
||||
}
|
||||
const targetText = normalizeNativeChatUserText(entry.text)
|
||||
const targetText = nativeChatUserTextMatchText(entry.text, true)
|
||||
const candidates = normalized.filter((message) => {
|
||||
if (message.role !== 'user') {
|
||||
return false
|
||||
}
|
||||
const imageCount = nativeChatUserMessageImageEvidenceCount(message)
|
||||
if (targetText) {
|
||||
return normalizedUserText(message) === targetText
|
||||
return (
|
||||
nativeChatUserMessageMatchText(message) === targetText &&
|
||||
imageCount >= entry.images!.length
|
||||
)
|
||||
}
|
||||
const imageCount = message.blocks.filter(isImageRefBlock).length
|
||||
const imageRefCount = message.blocks.filter(isImageRefBlock).length
|
||||
// Why: a marker-only echo proves delivery per marker, so a partially
|
||||
// rendered turn must not claim (and rebind) previews it cannot account for.
|
||||
return (
|
||||
message.blocks.length === 0 ||
|
||||
imageCount >= entry.images!.length ||
|
||||
imageRefCount >= entry.images!.length ||
|
||||
(countImagePromptMarkers(message) >= entry.images!.length &&
|
||||
normalizedUserText(message) === null)
|
||||
)
|
||||
@@ -228,7 +234,10 @@ export function findLandedUnconfirmedSends(
|
||||
// (`[Image: source: …]` or no text) keys under '' so an empty-text send can
|
||||
// claim it.
|
||||
const messageIndexById = new Map<string, number>()
|
||||
const userMessagesByText = new Map<string, Array<{ id: string; index: number }>>()
|
||||
const userMessagesByText = new Map<
|
||||
string,
|
||||
Array<{ id: string; index: number; imageCount: number }>
|
||||
>()
|
||||
for (const [index, message] of messages.entries()) {
|
||||
messageIndexById.set(message.id, index)
|
||||
if (message.role !== 'user') {
|
||||
@@ -239,9 +248,10 @@ export function findLandedUnconfirmedSends(
|
||||
// A marker-only turn keys both ways: '' claims it for a caption-less image
|
||||
// send, and its literal text claims it for a send of that same text.
|
||||
const literalKey = isImageSource ? key : (nativeChatUserMessageMatchText(message) ?? key)
|
||||
const imageCount = nativeChatUserMessageImageEvidenceCount(message)
|
||||
for (const candidateKey of literalKey === key ? [key] : [key, literalKey]) {
|
||||
const current = userMessagesByText.get(candidateKey) ?? []
|
||||
current.push({ id: message.id, index })
|
||||
current.push({ id: message.id, index, imageCount })
|
||||
userMessagesByText.set(candidateKey, current)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +267,12 @@ export function findLandedUnconfirmedSends(
|
||||
}
|
||||
const echo = userMessagesByText
|
||||
.get(entry.normalizedText)
|
||||
?.find((message) => message.index > tailIndex && !claimedMessageIds.has(message.id))
|
||||
?.find(
|
||||
(message) =>
|
||||
message.index > tailIndex &&
|
||||
message.imageCount >= entry.imageCount &&
|
||||
!claimedMessageIds.has(message.id)
|
||||
)
|
||||
if (echo) {
|
||||
claimedMessageIds.add(echo.id)
|
||||
landed.push(entry)
|
||||
|
||||
@@ -7,10 +7,11 @@ export {
|
||||
imageSourcePathFromText,
|
||||
hasImagePromptMarker,
|
||||
isImageSourceUserTurn,
|
||||
nativeChatUserMessageImageEvidenceCount,
|
||||
nativeChatUserMessageMatchText,
|
||||
nativeChatUserTextMatchText,
|
||||
normalizeImageTranscriptMessages,
|
||||
normalizeNativeChatUserText,
|
||||
normalizeNativeChatUserTextWithLiteralFallback,
|
||||
normalizedNativeChatUserMessageText,
|
||||
stripImagePromptMarker
|
||||
} from '../../../src/shared/native-chat-image-transcript-markers'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// an incomplete multi-image echo is allowed to do to the local previews.
|
||||
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { act, create } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isImageRefBlock,
|
||||
@@ -29,12 +29,14 @@ function userTextMessage(id: string, text: string): NativeChatMessage {
|
||||
}
|
||||
|
||||
describe('mobile literal image-marker turns', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
let updateRenderer: (messages: NativeChatMessage[]) => void = () => {}
|
||||
let unmountRenderer = (): void => {}
|
||||
let state: DraftState | null = null
|
||||
|
||||
afterEach(() => {
|
||||
act(() => renderer?.unmount())
|
||||
renderer = null
|
||||
act(unmountRenderer)
|
||||
updateRenderer = () => {}
|
||||
unmountRenderer = () => {}
|
||||
state = null
|
||||
})
|
||||
|
||||
@@ -51,12 +53,14 @@ describe('mobile literal image-marker turns', () => {
|
||||
|
||||
async function mount(): Promise<void> {
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness, {}))
|
||||
const renderer = create(createElement(Harness, {}))
|
||||
updateRenderer = (messages) => renderer.update(createElement(Harness, { messages }))
|
||||
unmountRenderer = () => renderer.unmount()
|
||||
})
|
||||
}
|
||||
|
||||
async function transcript(messages: NativeChatMessage[]): Promise<void> {
|
||||
await act(async () => renderer?.update(createElement(Harness, { messages })))
|
||||
await act(async () => updateRenderer(messages))
|
||||
}
|
||||
|
||||
function send(text: string, images?: string[]): void {
|
||||
@@ -158,4 +162,40 @@ describe('mobile literal image-marker turns', () => {
|
||||
expect(rendered.map(rowText)).toEqual(['look'])
|
||||
expect(rendered.map(rowImages)).toEqual([['file:///a.jpg']])
|
||||
})
|
||||
|
||||
it('keeps a captioned multi-image echo until the transcript accounts for every image', async () => {
|
||||
await mount()
|
||||
send('compare these', ['file:///a.jpg', 'file:///b.jpg', 'file:///c.jpg'])
|
||||
|
||||
const partial = [
|
||||
userTextMessage('source-a', '[Image: source: /tmp/a.png]'),
|
||||
userTextMessage('prompt', 'compare these [Image #1]')
|
||||
]
|
||||
await transcript(partial)
|
||||
|
||||
expect(rows(partial).map(rowImages)).toEqual([
|
||||
['/tmp/a.png'],
|
||||
['file:///a.jpg', 'file:///b.jpg', 'file:///c.jpg']
|
||||
])
|
||||
expect(rows(partial).map(rowText)).toEqual(['compare these', 'compare these'])
|
||||
expect(state?.imagePreviewsByMessageId).toEqual({})
|
||||
|
||||
const complete = [
|
||||
userTextMessage('source-a', '[Image: source: /tmp/a.png]'),
|
||||
userTextMessage('source-b', '[Image: source: /tmp/b.png]'),
|
||||
userTextMessage('source-c', '[Image: source: /tmp/c.png]'),
|
||||
userTextMessage('complete-prompt', 'compare these [Image #1] [Image #2] [Image #3]')
|
||||
]
|
||||
await transcript(complete)
|
||||
|
||||
expect(state?.imagePreviewsByMessageId).toEqual({
|
||||
'complete-prompt': ['file:///a.jpg', 'file:///b.jpg', 'file:///c.jpg']
|
||||
})
|
||||
const completeRows = rows(complete)
|
||||
expect(completeRows).toHaveLength(1)
|
||||
expect(completeRows.map(rowText)).toEqual(['compare these'])
|
||||
expect(completeRows.map(rowImages)).toEqual([
|
||||
['file:///a.jpg', 'file:///b.jpg', 'file:///c.jpg']
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
normalizeNativeChatUserText,
|
||||
normalizeNativeChatUserTextWithLiteralFallback
|
||||
nativeChatUserTextMatchText,
|
||||
normalizeNativeChatUserText
|
||||
} from './mobile-native-chat-image-transcript-markers'
|
||||
|
||||
export type MobileNativeChatPendingMessage = {
|
||||
@@ -16,6 +16,7 @@ export type MobileNativeChatSendOrigin = {
|
||||
draftKey: string
|
||||
pendingKey: string | null
|
||||
normalizedText: string
|
||||
imageCount: number
|
||||
baselineOccurrences: number
|
||||
baselineTailMessageId: string | null
|
||||
}
|
||||
@@ -25,9 +26,7 @@ type PendingByKey = Record<string, MobileNativeChatPendingMessage[]>
|
||||
/** Markers are the agent's only on a send that carried images; otherwise they
|
||||
* are literal text, so `captureSendOrigin` keeps them and so must this. */
|
||||
function pendingMatchText(pending: MobileNativeChatPendingMessage): string {
|
||||
return pending.images?.length
|
||||
? normalizeNativeChatUserText(pending.text)
|
||||
: normalizeNativeChatUserTextWithLiteralFallback(pending.text)
|
||||
return nativeChatUserTextMatchText(pending.text, Boolean(pending.images?.length))
|
||||
}
|
||||
|
||||
export function combineMobileNativeChatPending(
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
findLandedUnconfirmedSends,
|
||||
mergeLandedImagePreviewEchoes,
|
||||
migrateImagePreviewMessageIds,
|
||||
normalizeReconcileText,
|
||||
normalizeReconcileTextWithLiteralFallback,
|
||||
userTextOccurrenceCounts,
|
||||
type UnconfirmedSend
|
||||
} from './mobile-native-chat-draft-reconcile'
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
type MobileNativeChatSendOrigin
|
||||
} from './mobile-native-chat-pending-echo'
|
||||
import { mobileNativeChatScopeKey } from './mobile-native-chat-scope-key'
|
||||
import { nativeChatUserTextMatchText } from './mobile-native-chat-image-transcript-markers'
|
||||
import { useMobileNativeChatLaunchDraftSeed } from './use-mobile-native-chat-launch-draft-seed'
|
||||
import type { MobileNativeChatLaunchDraftSeed } from './use-mobile-native-chat-launch-draft-seed'
|
||||
|
||||
@@ -135,14 +134,18 @@ export function useMobileNativeChatDrafts(args: {
|
||||
}
|
||||
// Without an attached image an `[Image #n]` run is the user's own text, so
|
||||
// it has to stay in the key the transcript echo will be matched against.
|
||||
const normalizedText = images?.length
|
||||
? normalizeReconcileText(text)
|
||||
: normalizeReconcileTextWithLiteralFallback(text)
|
||||
const imageCount = images?.length ?? 0
|
||||
const normalizedText = nativeChatUserTextMatchText(text, imageCount > 0)
|
||||
return {
|
||||
draftKey,
|
||||
pendingKey,
|
||||
normalizedText,
|
||||
baselineOccurrences: countUserTextOccurrences(messagesRef.current, normalizedText),
|
||||
imageCount,
|
||||
baselineOccurrences: countUserTextOccurrences(
|
||||
messagesRef.current,
|
||||
normalizedText,
|
||||
imageCount
|
||||
),
|
||||
baselineTailMessageId: messagesRef.current.at(-1)?.id ?? null
|
||||
}
|
||||
},
|
||||
@@ -207,6 +210,7 @@ export function useMobileNativeChatDrafts(args: {
|
||||
pendingKey: origin.pendingKey,
|
||||
text,
|
||||
normalizedText: origin.normalizedText,
|
||||
imageCount: origin.imageCount,
|
||||
baselineTailMessageId: origin.baselineTailMessageId,
|
||||
deadline: null
|
||||
}
|
||||
@@ -299,17 +303,14 @@ export function useMobileNativeChatDrafts(args: {
|
||||
const landedCounts = userTextOccurrenceCounts(messages)
|
||||
// Image-only source-turn counts stay stable across reruns and ignore paginated history.
|
||||
const next = current.filter((item) => {
|
||||
if (landedImagePendingIds.has(item.id)) {
|
||||
return false
|
||||
}
|
||||
// Keep image echoes until their local preview reaches the authoritative message.
|
||||
if (item.images?.length) {
|
||||
return true
|
||||
return !landedImagePendingIds.has(item.id)
|
||||
}
|
||||
return item.text.trim() === ''
|
||||
? countImageSourceTurnsAfter(messages, item.baselineTailMessageId) <
|
||||
item.expectedOccurrence
|
||||
: (landedCounts.get(normalizeReconcileTextWithLiteralFallback(item.text)) ?? 0) <
|
||||
: (landedCounts.get(nativeChatUserTextMatchText(item.text, false)) ?? 0) <
|
||||
item.expectedOccurrence
|
||||
})
|
||||
if (next.length === current.length) {
|
||||
|
||||
@@ -124,4 +124,38 @@ describe('literal image-marker turns in native chat', () => {
|
||||
expect(rowText(rows[0]!)).toBe('')
|
||||
expect(rowImagePaths(rows[0]!)).toEqual(['/tmp/a.png'])
|
||||
})
|
||||
|
||||
it('keeps a captioned multi-image echo until the transcript accounts for every image', () => {
|
||||
const pending: NativeChatPendingSend[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
text: 'compare these',
|
||||
imagePaths: ['/tmp/a.png', '/tmp/b.png', '/tmp/c.png'],
|
||||
sentAt: 100
|
||||
}
|
||||
]
|
||||
const transcript = [
|
||||
userMessage('source-a', '[Image: source: /tmp/a.png]'),
|
||||
userMessage('prompt', 'compare these [Image #1]')
|
||||
]
|
||||
|
||||
const rows = renderedUserRows(transcript, pending)
|
||||
|
||||
expect(rows.map(rowText)).toEqual(['compare these', 'compare these'])
|
||||
expect(rows.map(rowImagePaths)).toEqual([
|
||||
['/tmp/a.png'],
|
||||
['/tmp/a.png', '/tmp/b.png', '/tmp/c.png']
|
||||
])
|
||||
|
||||
const complete = [
|
||||
userMessage('source-a', '[Image: source: /tmp/a.png]'),
|
||||
userMessage('source-b', '[Image: source: /tmp/b.png]'),
|
||||
userMessage('source-c', '[Image: source: /tmp/c.png]'),
|
||||
userMessage('complete-prompt', 'compare these [Image #1] [Image #2] [Image #3]')
|
||||
]
|
||||
const completeRows = renderedUserRows(complete, pending)
|
||||
expect(completeRows).toHaveLength(1)
|
||||
expect(rowText(completeRows[0]!)).toBe('compare these')
|
||||
expect(rowImagePaths(completeRows[0]!)).toEqual(['/tmp/a.png', '/tmp/b.png', '/tmp/c.png'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import {
|
||||
nativeChatUserMessageImageEvidenceCount,
|
||||
nativeChatUserMessageMatchText,
|
||||
nativeChatUserTextMatchText,
|
||||
normalizeNativeChatUserText,
|
||||
normalizeNativeChatUserTextWithLiteralFallback
|
||||
normalizedNativeChatUserMessageText
|
||||
} from '../../../../shared/native-chat-image-transcript-markers'
|
||||
import { isImageRefBlock, type NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
|
||||
@@ -25,36 +27,47 @@ export function normalizeNativeChatPendingText(text: string): string {
|
||||
function nativeChatPendingMatchText(
|
||||
pending: Pick<NativeChatPendingOccurrence, 'text' | 'imagePaths'>
|
||||
): string {
|
||||
return pending.imagePaths?.some(Boolean)
|
||||
? normalizeNativeChatPendingText(pending.text)
|
||||
: normalizeNativeChatUserTextWithLiteralFallback(pending.text)
|
||||
return nativeChatUserTextMatchText(pending.text, Boolean(pending.imagePaths?.some(Boolean)))
|
||||
}
|
||||
|
||||
function nativeChatTextContentKey(text: string, imageCount: number): string {
|
||||
return imageCount > 0 ? `text:${text}\0images:${imageCount}` : `text:${text}`
|
||||
}
|
||||
|
||||
export function nativeChatPendingContentKey(
|
||||
pending: Pick<NativeChatPendingOccurrence, 'text' | 'imagePaths'>
|
||||
): string {
|
||||
const text = nativeChatPendingMatchText(pending)
|
||||
if (text) {
|
||||
return `text:${text}`
|
||||
}
|
||||
const imagePaths = pending.imagePaths?.filter(Boolean) ?? []
|
||||
if (text) {
|
||||
return nativeChatTextContentKey(text, imagePaths.length)
|
||||
}
|
||||
return imagePaths.length > 0 ? `images:${JSON.stringify(imagePaths)}` : 'empty'
|
||||
}
|
||||
|
||||
function nativeChatUserMessageContentKey(message: NativeChatMessage): string | null {
|
||||
function nativeChatUserMessageContentKeys(message: NativeChatMessage): readonly string[] {
|
||||
if (message.role !== 'user') {
|
||||
return null
|
||||
return []
|
||||
}
|
||||
const text = nativeChatUserMessageMatchText(message) ?? ''
|
||||
if (text) {
|
||||
return `text:${text}`
|
||||
const keys = new Set<string>()
|
||||
const matchText = nativeChatUserMessageMatchText(message) ?? ''
|
||||
if (matchText) {
|
||||
keys.add(nativeChatTextContentKey(matchText, 0))
|
||||
}
|
||||
const imagePaths = message.blocks
|
||||
.filter(isImageRefBlock)
|
||||
.map((block) => block.path)
|
||||
.filter((path): path is string => Boolean(path))
|
||||
const key = nativeChatPendingContentKey({ text: '', imagePaths })
|
||||
return key === 'empty' ? null : key
|
||||
const imageCount = nativeChatUserMessageImageEvidenceCount(message)
|
||||
const imageText = normalizedNativeChatUserMessageText(message)
|
||||
if (imageText && imageCount > 0) {
|
||||
keys.add(nativeChatTextContentKey(imageText, imageCount))
|
||||
} else if (imageCount > 0) {
|
||||
const imagePaths = message.blocks
|
||||
.filter(isImageRefBlock)
|
||||
.map((block) => block.path)
|
||||
.filter((path): path is string => Boolean(path))
|
||||
if (imagePaths.length > 0) {
|
||||
keys.add(`images:${JSON.stringify(imagePaths)}`)
|
||||
}
|
||||
}
|
||||
return [...keys]
|
||||
}
|
||||
|
||||
export function matchingNativeChatUserContentCounts(
|
||||
@@ -62,8 +75,7 @@ export function matchingNativeChatUserContentCounts(
|
||||
): Map<string, number> {
|
||||
const counts = new Map<string, number>()
|
||||
for (const message of messages) {
|
||||
const key = nativeChatUserMessageContentKey(message)
|
||||
if (key) {
|
||||
for (const key of nativeChatUserMessageContentKeys(message)) {
|
||||
counts.set(key, (counts.get(key) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
@@ -77,8 +89,7 @@ export function advancedNativeChatUserContentCounts(
|
||||
const waiting = new Map<string, number>()
|
||||
for (const message of messages) {
|
||||
if (message.role === 'user') {
|
||||
const key = nativeChatUserMessageContentKey(message)
|
||||
if (key) {
|
||||
for (const key of nativeChatUserMessageContentKeys(message)) {
|
||||
waiting.set(key, (waiting.get(key) ?? 0) + 1)
|
||||
}
|
||||
continue
|
||||
@@ -180,10 +191,13 @@ export function selectPendingIndicesRepresentedByUserTexts(
|
||||
}
|
||||
const remaining = pending.map((entry, index) => ({
|
||||
index,
|
||||
text: nativeChatPendingMatchText(entry)
|
||||
text: nativeChatPendingMatchText(entry),
|
||||
hasImages: Boolean(entry.imagePaths?.some(Boolean))
|
||||
}))
|
||||
for (const userText of userTexts) {
|
||||
const open = remaining.filter((entry) => !represented.has(entry.index) && entry.text.length > 0)
|
||||
const open = remaining.filter(
|
||||
(entry) => !entry.hasImages && !represented.has(entry.index) && entry.text.length > 0
|
||||
)
|
||||
const gluedCount = countLeadingPendingTextsGluedToUserText(
|
||||
open.map((entry) => entry.text),
|
||||
userText
|
||||
|
||||
@@ -48,10 +48,16 @@ export function normalizeNativeChatUserText(text: string): string {
|
||||
/** Marker text that stripping would erase entirely is text the user typed
|
||||
* literally — a real marker always rides on an image turn, which normalization
|
||||
* has already folded away by the time echo matching runs. */
|
||||
export function normalizeNativeChatUserTextWithLiteralFallback(text: string): string {
|
||||
function normalizeNativeChatUserTextWithLiteralFallback(text: string): string {
|
||||
return normalizeNativeChatUserText(text) || text.trim().replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
export function nativeChatUserTextMatchText(text: string, hasImages: boolean): string {
|
||||
return hasImages
|
||||
? normalizeNativeChatUserText(text)
|
||||
: normalizeNativeChatUserTextWithLiteralFallback(text)
|
||||
}
|
||||
|
||||
function joinedUserText(message: NativeChatMessage): string {
|
||||
return message.blocks
|
||||
.filter(isTextBlock)
|
||||
@@ -74,11 +80,7 @@ export function nativeChatUserMessageMatchText(message: NativeChatMessage): stri
|
||||
return null
|
||||
}
|
||||
const joined = joinedUserText(message)
|
||||
const normalized = normalizeNativeChatUserText(joined)
|
||||
if (normalized || message.blocks.some(isImageRefBlock)) {
|
||||
return normalized || null
|
||||
}
|
||||
return normalizeNativeChatUserTextWithLiteralFallback(joined) || null
|
||||
return nativeChatUserTextMatchText(joined, message.blocks.some(isImageRefBlock)) || null
|
||||
}
|
||||
|
||||
function stripImagePromptMarkersFromTextBlocks(
|
||||
@@ -132,6 +134,14 @@ export function countImagePromptMarkers(message: NativeChatMessage): number {
|
||||
)
|
||||
}
|
||||
|
||||
export function nativeChatUserMessageImageEvidenceCount(message: NativeChatMessage): number {
|
||||
if (message.role !== 'user') {
|
||||
return 0
|
||||
}
|
||||
const imageRefCount = message.blocks.filter(isImageRefBlock).length
|
||||
return Math.max(imageRefCount, countImagePromptMarkers(message))
|
||||
}
|
||||
|
||||
/** Claude records image paths as source turns followed by a prompt carrying
|
||||
* image markers. Merge the whole run back into one native user turn. */
|
||||
export function normalizeImageTranscriptMessages(
|
||||
|
||||
Reference in New Issue
Block a user