diff --git a/ui-tui/src/__tests__/text.test.ts b/ui-tui/src/__tests__/text.test.ts index 1690996dd8..a81baa0fba 100644 --- a/ui-tui/src/__tests__/text.test.ts +++ b/ui-tui/src/__tests__/text.test.ts @@ -12,7 +12,8 @@ import { parseToolTrailResultLine, pasteTokenLabel, sameToolTrailGroup, - splitToolDuration + splitToolDuration, + thinkingPreview } from '../lib/text.js' describe('isToolTrailResultLine', () => { @@ -82,6 +83,17 @@ describe('estimateTokensRough', () => { }) }) +describe('thinkingPreview', () => { + it('adds paragraph breaks before markdown thinking headings', () => { + const raw = + '**Considering user instructions**\nI need to answer.**Planning tool execution**\nI can run tools.**Determining weather search parameters**\nUse SF.' + + expect(thinkingPreview(raw, 'full')).toBe( + '**Considering user instructions**\nI need to answer.\n\n**Planning tool execution**\nI can run tools.\n\n**Determining weather search parameters**\nUse SF.' + ) + }) +}) + describe('boundedLiveRenderText', () => { it('preserves short live text verbatim', () => { expect(boundedLiveRenderText('one\ntwo', { maxChars: 100, maxLines: 10 })).toBe('one\ntwo') diff --git a/ui-tui/src/lib/text.ts b/ui-tui/src/lib/text.ts index 256cbc0f0f..9c9758c3f1 100644 --- a/ui-tui/src/lib/text.ts +++ b/ui-tui/src/lib/text.ts @@ -74,14 +74,21 @@ export const pasteTokenLabel = (text: string, lineCount: number) => { const THINKING_STATUS_RE = new RegExp(`^(?:${VERBS.join('|')})\\.{0,3}$`, 'i') const THINKING_STATUS_CHUNK_RE = new RegExp(`[^A-Za-z\n]+\\s*(?:${VERBS.join('|')})\\.{0,3}\\s*`, 'giu') -export const cleanThinkingText = (reasoning: string) => - reasoning - .split('\n') - .map(line => line.replace(THINKING_STATUS_CHUNK_RE, '').trim()) - .filter(line => line && !THINKING_STATUS_RE.test(line.replace(/\.\.\.$/, '').trim())) - .join('\n') +const normalizeThinkingParagraphs = (text: string) => + text + .replace(/([^\n])(?=\*\*[^*\n][^\n]*?\*\*)/g, '$1\n\n') + .replace(/\n{3,}/g, '\n\n') .trim() +export const cleanThinkingText = (reasoning: string) => + normalizeThinkingParagraphs( + reasoning + .split('\n') + .map(line => line.replace(THINKING_STATUS_CHUNK_RE, '').trim()) + .filter(line => line && !THINKING_STATUS_RE.test(line.replace(/\.\.\.$/, '').trim())) + .join('\n') + ) + export const thinkingPreview = (reasoning: string, mode: ThinkingMode, max: number = THINKING_COT_MAX) => { const raw = cleanThinkingText(reasoning)