fix(tui): avoid duplicating flushed stream text

This commit is contained in:
Brooklyn Nicholson
2026-04-26 10:58:18 -05:00
parent d91e24547c
commit 6814646b36
2 changed files with 33 additions and 1 deletions

View File

@@ -198,6 +198,22 @@ describe('createGatewayEventHandler', () => {
expect(appended[3]?.text).not.toContain('```diff')
})
it('keeps full final responses from duplicating flushed pre-diff narration', () => {
const appended: Msg[] = []
const onEvent = createGatewayEventHandler(buildCtx(appended))
const diff = '--- a/foo.ts\n+++ b/foo.ts\n@@\n-old\n+new'
const block = `\`\`\`diff\n${diff}\n\`\`\``
onEvent({ payload: { text: 'Before edit. ' }, type: 'message.delta' } as any)
onEvent({ payload: { context: 'foo.ts', name: 'patch', tool_id: 'tool-1' }, type: 'tool.start' } as any)
onEvent({ payload: { inline_diff: diff, summary: 'patched', tool_id: 'tool-1' }, type: 'tool.complete' } as any)
onEvent({ payload: { text: 'After edit.' }, type: 'message.delta' } as any)
onEvent({ payload: { text: 'Before edit. After edit.' }, type: 'message.complete' } as any)
expect(appended.map(msg => msg.text.trim()).filter(Boolean)).toEqual(['Before edit.', block, 'After edit.'])
expect(appended[1]?.tools?.[0]).toContain('Patch')
})
it('drops the diff segment when the final assistant text narrates the same diff', () => {
const appended: Msg[] = []
const onEvent = createGatewayEventHandler(buildCtx(appended))

View File

@@ -40,6 +40,22 @@ const diffSegmentBody = (msg: Msg): null | string => {
const hasDetails = (msg: Msg): boolean => Boolean(msg.thinking || msg.tools?.length || msg.toolTokens)
const textSegments = (segments: Msg[]) => segments.filter(msg => msg.role === 'assistant' && msg.kind !== 'diff').map(msg => msg.text)
const finalTail = (finalText: string, segments: Msg[]) => {
let tail = finalText
for (const text of textSegments(segments)) {
const trimmed = text.trim()
if (trimmed && tail.startsWith(trimmed)) {
tail = tail.slice(trimmed.length).trimStart()
}
}
return tail
}
export interface InterruptDeps {
appendMessage: (msg: Msg) => void
gw: { request: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T> }
@@ -294,7 +310,7 @@ class TurnController {
recordMessageComplete(payload: { rendered?: string; reasoning?: string; text?: string }) {
const rawText = (payload.rendered ?? payload.text ?? this.bufRef).trimStart()
const split = splitReasoning(rawText)
const finalText = split.text
const finalText = finalTail(split.text, this.segmentMessages)
const existingReasoning = this.reasoningText.trim() || String(payload.reasoning ?? '').trim()
const savedReasoning = [existingReasoning, existingReasoning ? '' : split.reasoning].filter(Boolean).join('\n\n')
const savedToolTokens = this.toolTokenAcc