From dff1c8fcf195d10dd5d642ca216f3df56159cf50 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 21 Apr 2026 18:35:59 -0500 Subject: [PATCH] fix(tui): tool inline_diff renders inline with the active turn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported during TUI v2 blitz retest: code-review diffs from tool.complete appeared at the top of the current interaction thread, out of sequence with the agent's messages and tool rows below them. Root cause — `sys(inline_diff)` appends to `historyItems`, which sits above the `StreamingAssistant` pane that renders the active turn. Until the turn closed, the diff visually floated above everything else happening in the same turn. Route the diff through `turnController.appendSegmentMessage` instead so it flushes any pending streaming text first, then lands in the segment stream beside assistant output and tool calls. On `message.complete` the segment list is committed to history in emit order (diff → final text), matching what the gateway sent. Adds a regression test that exercises tool.complete → message.complete with an inline_diff payload and asserts both the streaming and final placement. --- .../createGatewayEventHandler.test.ts | 32 +++++++++++++++++++ ui-tui/src/app/createGatewayEventHandler.ts | 7 +++- ui-tui/src/app/turnController.ts | 12 +++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/ui-tui/src/__tests__/createGatewayEventHandler.test.ts b/ui-tui/src/__tests__/createGatewayEventHandler.test.ts index f1f0c306..17b6e02f 100644 --- a/ui-tui/src/__tests__/createGatewayEventHandler.test.ts +++ b/ui-tui/src/__tests__/createGatewayEventHandler.test.ts @@ -143,6 +143,38 @@ describe('createGatewayEventHandler', () => { expect(appended[0]?.thinkingTokens).toBe(estimateTokensRough(fromServer)) }) + 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' + + 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) + + // While streaming, nothing has flowed to historyItems yet — diff must be + // 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 }) + + onEvent({ + payload: { text: 'patch applied' }, + type: 'message.complete' + } as any) + + // 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' }) + }) + it('shows setup panel for missing provider startup error', () => { const appended: Msg[] = [] const onEvent = createGatewayEventHandler(buildCtx(appended)) diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 8f45bb3d..3ae6b26d 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -266,7 +266,12 @@ 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) { - sys(ev.payload.inline_diff) + // 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 }) } return diff --git a/ui-tui/src/app/turnController.ts b/ui-tui/src/app/turnController.ts index 43622e7c..d3bd2989 100644 --- a/ui-tui/src/app/turnController.ts +++ b/ui-tui/src/app/turnController.ts @@ -182,6 +182,18 @@ class TurnController { }, REASONING_PULSE_MS) } + /** + * Append an inline artifact (e.g. tool-complete inline diff) to the active + * turn's segment stream. Routing through `historyItems` via `sys()` lands + * the artifact above the currently-streaming assistant bubble; adding it + * here keeps the paint order aligned with the order the gateway emitted. + */ + appendSegmentMessage(msg: Msg) { + this.flushStreamingSegment() + this.segmentMessages = [...this.segmentMessages, msg] + patchTurnState({ streamSegments: this.segmentMessages }) + } + pushActivity(text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) { patchTurnState(state => { const base = replaceLabel