fix(tui): keep inline diffs below tool rows and strip ANSI

Follow-up on #13729 from blitz screenshot feedback.\n\n- When tool.complete carried inline_diff but no buffered assistant text existed, pending tool rows were still in streamPendingTools, so diff rendered above the tool row section. appendSegmentMessage now emits pending tool rows as a trail segment before appending the diff artifact.\n- Strip ANSI color escapes from inline_diff payloads so we don't render loud red/green terminal palettes in the transcript.
This commit is contained in:
Brooklyn Nicholson
2026-04-21 18:50:42 -05:00
parent dff1c8fcf1
commit bddf0cd61e
3 changed files with 28 additions and 7 deletions

View File

@@ -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', () => {

View File

@@ -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

View File

@@ -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 })
}