diff --git a/ui-tui/src/__tests__/createGatewayEventHandler.test.ts b/ui-tui/src/__tests__/createGatewayEventHandler.test.ts index 17b6e02f7c..92154fd008 100644 --- a/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +++ b/ui-tui/src/__tests__/createGatewayEventHandler.test.ts @@ -146,7 +146,8 @@ describe('createGatewayEventHandler', () => { it('routes inline_diff into the active segment stream, not historyItems', () => { const appended: Msg[] = [] const onEvent = createGatewayEventHandler(buildCtx(appended)) - const diff = '--- a/foo.ts\n+++ b/foo.ts\n@@\n-old\n+new' + const diff = '\u001b[31m--- a/foo.ts\u001b[0m\n\u001b[32m+++ b/foo.ts\u001b[0m\n@@\n-old\n+new' + const cleaned = '--- a/foo.ts\n+++ b/foo.ts\n@@\n-old\n+new' onEvent({ payload: { context: 'foo.ts', name: 'patch', tool_id: 'tool-1' }, @@ -161,7 +162,10 @@ describe('createGatewayEventHandler', () => { // held in segmentMessages so the transcript renders it inline with the // current turn rather than above it. expect(appended).toHaveLength(0) - expect(turnController.segmentMessages).toContainEqual({ role: 'system', text: diff }) + expect(turnController.segmentMessages).toContainEqual( + expect.objectContaining({ kind: 'trail', role: 'system', text: '' }) + ) + expect(turnController.segmentMessages).toContainEqual({ role: 'system', text: cleaned }) onEvent({ payload: { text: 'patch applied' }, @@ -170,9 +174,10 @@ describe('createGatewayEventHandler', () => { // After the turn closes, the diff lands in history in the order the // gateway emitted it — before the assistant's final text, not above it. - expect(appended).toHaveLength(2) - expect(appended[0]).toMatchObject({ role: 'system', text: diff }) - expect(appended[1]).toMatchObject({ role: 'assistant', text: 'patch applied' }) + expect(appended).toHaveLength(3) + expect(appended[0]).toMatchObject({ kind: 'trail', role: 'system', text: '' }) + expect(appended[1]).toMatchObject({ role: 'system', text: cleaned }) + expect(appended[2]).toMatchObject({ role: 'assistant', text: 'patch applied' }) }) it('shows setup panel for missing provider startup error', () => { diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 3ae6b26dc8..51df15e450 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -2,7 +2,7 @@ import { STREAM_BATCH_MS } from '../config/timing.js' import { buildSetupRequiredSections, SETUP_REQUIRED_TITLE } from '../content/setup.js' import type { CommandsCatalogResponse, GatewayEvent, GatewaySkin } from '../gatewayTypes.js' import { rpcErrorMessage } from '../lib/rpc.js' -import { formatToolCall } from '../lib/text.js' +import { formatToolCall, stripAnsi } from '../lib/text.js' import { fromSkin } from '../theme.js' import type { Msg, SubagentProgress } from '../types.js' @@ -266,12 +266,18 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: turnController.recordToolComplete(ev.payload.tool_id, ev.payload.name, ev.payload.error, ev.payload.summary) if (ev.payload.inline_diff && getUiState().inlineDiffs) { + const diffText = stripAnsi(String(ev.payload.inline_diff)) + + if (!diffText.trim()) { + return + } + // Push into the active turn's segment stream so the diff renders // inline with the assistant's output. Routing through `sys()` // lands it in the completed-history section above the streaming // bubble — which is why blitz testers saw diffs "appear at the // top, out of sequence" with the rest of the turn. - turnController.appendSegmentMessage({ role: 'system', text: ev.payload.inline_diff }) + turnController.appendSegmentMessage({ role: 'system', text: diffText }) } return diff --git a/ui-tui/src/app/turnController.ts b/ui-tui/src/app/turnController.ts index d3bd2989f6..d38d34659b 100644 --- a/ui-tui/src/app/turnController.ts +++ b/ui-tui/src/app/turnController.ts @@ -190,6 +190,16 @@ class TurnController { */ appendSegmentMessage(msg: Msg) { this.flushStreamingSegment() + + if (this.pendingSegmentTools.length) { + this.segmentMessages = [ + ...this.segmentMessages, + { kind: 'trail', role: 'system', text: '', tools: this.pendingSegmentTools } + ] + this.pendingSegmentTools = [] + patchTurnState({ streamPendingTools: [] }) + } + this.segmentMessages = [...this.segmentMessages, msg] patchTurnState({ streamSegments: this.segmentMessages }) }