fix(tui): filter thinking status noise

This commit is contained in:
Brooklyn Nicholson
2026-04-26 13:59:56 -05:00
parent a8bfe72d35
commit c9f7b703dd
3 changed files with 61 additions and 1 deletions

View File

@@ -150,6 +150,20 @@ describe('createGatewayEventHandler', () => {
expect(appended[appended.length - 1]).toMatchObject({ role: 'assistant', text: 'final answer' })
})
it('filters spinner/status-only reasoning noise from completed thinking', () => {
const appended: Msg[] = []
const streamed = '(¬_¬) synthesizing...\nactual plan\n( ͡° ͜ʖ ͡°) pondering...\nnext step'
const onEvent = createGatewayEventHandler(buildCtx(appended))
onEvent({ payload: { text: streamed }, type: 'reasoning.delta' } as any)
onEvent({ payload: { text: 'final answer' }, type: 'message.complete' } as any)
expect(appended[0]?.thinking).toBe(streamed)
expect(appended[0]?.text).toBe('')
expect(appended[appended.length - 1]).toMatchObject({ role: 'assistant', text: 'final answer' })
})
it('ignores fallback reasoning.available when streamed reasoning already exists', () => {
const appended: Msg[] = []
const streamed = 'short streamed reasoning'

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
import { cleanThinkingText } from '../lib/text.js'
describe('splitReasoning', () => {
it('extracts <think>…</think> and strips it from text', () => {
@@ -48,3 +49,13 @@ describe('splitReasoning', () => {
expect(hasReasoningTag('no tags at all')).toBe(false)
})
})
describe('cleanThinkingText', () => {
it('removes face/status ticker fragments while preserving real reasoning', () => {
expect(
cleanThinkingText(
'(¬_¬) synthesizing...**Resolving comments on GitHub**\n( ͡° ͜ʖ ͡°) musing...\nActual step\n٩(๑❛ᴗ❛๑)۶ contemplating...next step'
)
).toBe('**Resolving comments on GitHub**\nActual step\nnext step')
})
})

View File

@@ -70,8 +70,43 @@ export const pasteTokenLabel = (text: string, lineCount: number) => {
: `[[ ${preview} [${fmtK(lineCount)} lines] ]]`
}
const THINKING_STATUS_WORDS = [
'pondering',
'contemplating',
'musing',
'cogitating',
'ruminating',
'deliberating',
'mulling',
'reflecting',
'processing',
'reasoning',
'analyzing',
'computing',
'synthesizing',
'formulating',
'brainstorming'
]
const THINKING_STATUS_RE = new RegExp(`^(?:${THINKING_STATUS_WORDS.join('|')})\\.{0,3}$`, 'i')
const THINKING_FACE_SOURCE = '[^A-Za-z\n]+'
const THINKING_STATUS_CHUNK_RE = new RegExp(
`${THINKING_FACE_SOURCE}\\s*(?:${THINKING_STATUS_WORDS.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')
.trim()
export const thinkingPreview = (reasoning: string, mode: ThinkingMode, max: number = THINKING_COT_MAX) => {
const raw = reasoning.trim()
const raw = cleanThinkingText(reasoning)
return !raw || mode === 'collapsed' ? '' : mode === 'full' ? raw : compactPreview(raw.replace(WS_RE, ' '), max)
}