mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
import { isTextBlock, type NativeChatBlock, type NativeChatMessage } from './native-chat-types'
|
|
|
|
const IMAGE_SOURCE_MARKER = /^\[Image:\s*source:\s*(.+?)\]\s*$/
|
|
const IMAGE_PROMPT_MARKER = /^\[Image #\d+\]\s*/
|
|
|
|
function soleText(message: NativeChatMessage): string | null {
|
|
return message.blocks.length === 1 && isTextBlock(message.blocks[0])
|
|
? message.blocks[0].text
|
|
: null
|
|
}
|
|
|
|
export function imageSourcePathFromText(text: string): string | null {
|
|
return text.match(IMAGE_SOURCE_MARKER)?.[1]?.trim() ?? null
|
|
}
|
|
|
|
export function stripImagePromptMarker(text: string): string {
|
|
return text.replace(IMAGE_PROMPT_MARKER, '')
|
|
}
|
|
|
|
function stripFirstImagePromptMarker(blocks: readonly NativeChatBlock[]): NativeChatBlock[] {
|
|
let stripped = false
|
|
const next: NativeChatBlock[] = []
|
|
for (const block of blocks) {
|
|
if (!stripped && isTextBlock(block)) {
|
|
stripped = true
|
|
const text = stripImagePromptMarker(block.text)
|
|
if (text.trim().length > 0) {
|
|
next.push({ ...block, text })
|
|
}
|
|
continue
|
|
}
|
|
next.push(block)
|
|
}
|
|
return next
|
|
}
|
|
|
|
function imagePromptMarkerStartsMessage(message: NativeChatMessage): boolean {
|
|
const firstText = message.blocks.find(isTextBlock)
|
|
return firstText ? IMAGE_PROMPT_MARKER.test(firstText.text) : false
|
|
}
|
|
|
|
/** Claude records an attached image as two user transcript turns:
|
|
* `[Image: source: /path]` and then `[Image #1] prompt`. Merge them back into
|
|
* one native turn so the UI keeps the same chip+text shape as the optimistic
|
|
* send and does not show raw TUI marker text after a view remount. */
|
|
export function normalizeImageTranscriptMessages(
|
|
messages: readonly NativeChatMessage[]
|
|
): NativeChatMessage[] {
|
|
const normalized: NativeChatMessage[] = []
|
|
for (let index = 0; index < messages.length; index += 1) {
|
|
const message = messages[index]!
|
|
if (message.role !== 'user') {
|
|
normalized.push(message)
|
|
continue
|
|
}
|
|
const imagePath = imageSourcePathFromText(soleText(message) ?? '')
|
|
const next = messages[index + 1]
|
|
if (
|
|
imagePath &&
|
|
next?.role === 'user' &&
|
|
next.source === message.source &&
|
|
imagePromptMarkerStartsMessage(next)
|
|
) {
|
|
normalized.push({
|
|
...next,
|
|
blocks: [
|
|
{ type: 'image-ref', path: imagePath },
|
|
...stripFirstImagePromptMarker(next.blocks)
|
|
]
|
|
})
|
|
index += 1
|
|
continue
|
|
}
|
|
// A lone `[Image: source: /path]` turn (no following `[Image #1]` prompt —
|
|
// e.g. an image sent with no caption) still renders as an image chip rather
|
|
// than the raw marker text.
|
|
if (imagePath) {
|
|
normalized.push({
|
|
...message,
|
|
blocks: [{ type: 'image-ref', path: imagePath }]
|
|
})
|
|
continue
|
|
}
|
|
normalized.push({
|
|
...message,
|
|
blocks: stripFirstImagePromptMarker(message.blocks)
|
|
})
|
|
}
|
|
return normalized
|
|
}
|