diff --git a/ui-tui/src/__tests__/createGatewayEventHandler.test.ts b/ui-tui/src/__tests__/createGatewayEventHandler.test.ts index f1f0c306bc..17b6e02f7c 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 8f45bb3d7e..3ae6b26dc8 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 43622e7c7a..d3bd2989f6 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