diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 954ec3cc7..541c58727 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -87,4 +87,14 @@ describe("extractContentFromMessage", () => { expect(text).toBe("hello"); }); + + it("renders error text when stopReason is error and content is not an array", () => { + const text = extractContentFromMessage({ + role: "assistant", + stopReason: "error", + errorMessage: '429 {"error":{"message":"rate limit"}}', + }); + + expect(text).toContain("HTTP 429"); + }); }); diff --git a/src/tui/tui-stream-assembler.test.ts b/src/tui/tui-stream-assembler.test.ts index 9be05e608..4a180a0d8 100644 --- a/src/tui/tui-stream-assembler.test.ts +++ b/src/tui/tui-stream-assembler.test.ts @@ -65,4 +65,29 @@ describe("TuiStreamAssembler", () => { expect(finalText).toBe("Streamed"); }); + + it("returns null when delta text is unchanged", () => { + const assembler = new TuiStreamAssembler(); + const first = assembler.ingestDelta( + "run-4", + { + role: "assistant", + content: [{ type: "text", text: "Repeat" }], + }, + false, + ); + + expect(first).toBe("Repeat"); + + const second = assembler.ingestDelta( + "run-4", + { + role: "assistant", + content: [{ type: "text", text: "Repeat" }], + }, + false, + ); + + expect(second).toBeNull(); + }); }); diff --git a/src/tui/tui-stream-assembler.ts b/src/tui/tui-stream-assembler.ts index 99904dda0..d6f5d817a 100644 --- a/src/tui/tui-stream-assembler.ts +++ b/src/tui/tui-stream-assembler.ts @@ -27,10 +27,9 @@ export class TuiStreamAssembler { return state; } - ingestDelta(runId: string, message: unknown, showThinking: boolean): string | null { + private updateRunState(state: RunStreamState, message: unknown, showThinking: boolean) { const thinkingText = extractThinkingFromMessage(message); const contentText = extractContentFromMessage(message); - const state = this.getOrCreateRun(runId); if (thinkingText) { state.thinkingText = thinkingText; @@ -45,29 +44,23 @@ export class TuiStreamAssembler { showThinking, }); - if (!displayText || displayText === state.displayText) return null; - state.displayText = displayText; - return displayText; + } + + ingestDelta(runId: string, message: unknown, showThinking: boolean): string | null { + const state = this.getOrCreateRun(runId); + const previousDisplayText = state.displayText; + this.updateRunState(state, message, showThinking); + + if (!state.displayText || state.displayText === previousDisplayText) return null; + + return state.displayText; } finalize(runId: string, message: unknown, showThinking: boolean): string { const state = this.getOrCreateRun(runId); - const thinkingText = extractThinkingFromMessage(message); - const contentText = extractContentFromMessage(message); - - if (thinkingText) { - state.thinkingText = thinkingText; - } - if (contentText) { - state.contentText = contentText; - } - - const finalComposed = composeThinkingAndContent({ - thinkingText: state.thinkingText, - contentText: state.contentText, - showThinking, - }); + this.updateRunState(state, message, showThinking); + const finalComposed = state.displayText; const finalText = resolveFinalAssistantText({ finalText: finalComposed, streamedText: state.displayText,