From ddbec7b9b069f2bf5b3218db790b3e66ac8add62 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:43:14 -0700 Subject: [PATCH] fix(native-chat): preserve image caption whitespace --- ...tive-chat-image-transcript-markers.test.ts | 15 ++++++++++-- .../native-chat-image-transcript-markers.ts | 23 +++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/shared/native-chat-image-transcript-markers.test.ts b/src/shared/native-chat-image-transcript-markers.test.ts index 0308c44951d..cd9e7d3b8d6 100644 --- a/src/shared/native-chat-image-transcript-markers.test.ts +++ b/src/shared/native-chat-image-transcript-markers.test.ts @@ -43,14 +43,25 @@ describe('normalizeImageTranscriptMessages', () => { it.each([ ['[Image #1] describe this', 'describe this'], + ['[Image #1]\t describe this', 'describe this'], ['describe this [Image #1]', 'describe this'], - ['describe [Image #1] this', 'describe this'], + ['describe this \t[Image #1]', 'describe this'], + ['describe [Image #1] this', 'describe this'], + ['describe [Image #1]\t this', 'describe \t this'], + ['describe[Image #1]\t this', 'describe\t this'], + ['describe\n[Image #1]\nthis', 'describe\n\nthis'], ['com[Image #1]pare this', 'compare this'], - ['[Image #1] [Image #2]', ''] + ['[Image #1] [Image #2]', ''], + ['literal [Image #x] text', 'literal [Image #x] text'] ])('strips image prompt markers anywhere in text', (text, expected) => { expect(stripImagePromptMarker(text)).toBe(expected) }) + it('returns long marker-free whitespace without regex backtracking', () => { + const text = ' '.repeat(50_000) + expect(stripImagePromptMarker(text)).toBe(text) + }) + it('converts a lone [Image: source] turn (no prompt) into an image-ref instead of raw text', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /Users/me/Pictures/hero-image-2.png]') diff --git a/src/shared/native-chat-image-transcript-markers.ts b/src/shared/native-chat-image-transcript-markers.ts index a14f395ea2d..6523688018b 100644 --- a/src/shared/native-chat-image-transcript-markers.ts +++ b/src/shared/native-chat-image-transcript-markers.ts @@ -2,7 +2,11 @@ import { isTextBlock, type NativeChatBlock, type NativeChatMessage } from './nat const IMAGE_SOURCE_MARKER = /^\[Image:\s*source:\s*(.+?)\]\s*$/ const IMAGE_PROMPT_MARKER = /\[Image #\d+\]/ -const IMAGE_PROMPT_MARKER_RUN = /[^\S\r\n]*(?:\[Image #\d+\][^\S\r\n]*)+/g +const IMAGE_PROMPT_MARKERS = /\[Image #\d+\]/g +const IMAGE_PROMPT_MARKER_AT_START = /^\[Image #\d+\]/ +const IMAGE_PROMPT_MARKER_AT_END = /\[Image #\d+\]$/ +const HORIZONTAL_WHITESPACE_START = /^[^\S\r\n]+/ +const HORIZONTAL_WHITESPACE_END = /[^\S\r\n]+$/ function soleText(message: NativeChatMessage): string | null { return message.blocks.length === 1 && isTextBlock(message.blocks[0]) @@ -15,12 +19,17 @@ export function imageSourcePathFromText(text: string): string | null { } export function stripImagePromptMarker(text: string): string { - return text.replace(IMAGE_PROMPT_MARKER_RUN, (run, offset: number, source: string) => { - const hasTextBefore = offset > 0 - const hasTextAfter = offset + run.length < source.length - const hadSurroundingWhitespace = run.replace(/\[Image #\d+\]/g, '').length > 0 - return hasTextBefore && hasTextAfter && hadSurroundingWhitespace ? ' ' : '' - }) + const stripped = text.replace(IMAGE_PROMPT_MARKERS, '') + if (stripped === text) { + return text + } + let result = IMAGE_PROMPT_MARKER_AT_START.test(text) + ? stripped.replace(HORIZONTAL_WHITESPACE_START, '') + : stripped + if (IMAGE_PROMPT_MARKER_AT_END.test(text)) { + result = result.replace(HORIZONTAL_WHITESPACE_END, '') + } + return result } function stripImagePromptMarkersFromFirstText(