From 2467a103b243dac687273edfb7cb56970e187633 Mon Sep 17 00:00:00 2001 From: Shadow Date: Mon, 12 Jan 2026 21:29:15 -0600 Subject: [PATCH] TUI: keep streamed text when final output is empty Closes #747 --- CHANGELOG.md | 1 + src/tui/components/chat-log.ts | 8 ++++++++ src/tui/tui.test.ts | 20 ++++++++++++++++++++ src/tui/tui.ts | 17 ++++++++++++++++- 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 src/tui/tui.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc9dce26..d19531364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- TUI: keep the last streamed response instead of replacing it with “(no output)”. (#747 — thanks @thewilloftheshadow) - Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow) - Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) diff --git a/src/tui/components/chat-log.ts b/src/tui/components/chat-log.ts index dae49be4e..e3ce93102 100644 --- a/src/tui/components/chat-log.ts +++ b/src/tui/components/chat-log.ts @@ -49,6 +49,14 @@ export class ChatLog extends Container { this.streamingAssistant.setText(text); } + getStreamingText(runId?: string) { + if (!this.streamingAssistant) return null; + if (runId && this.streamingRunId && runId !== this.streamingRunId) { + return null; + } + return this.streamingText; + } + finalizeAssistant(text: string, runId?: string) { if ( this.streamingAssistant && diff --git a/src/tui/tui.test.ts b/src/tui/tui.test.ts new file mode 100644 index 000000000..696e43c3b --- /dev/null +++ b/src/tui/tui.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { resolveFinalAssistantText } from "./tui.js"; + +describe("resolveFinalAssistantText", () => { + it("falls back to streamed text when final text is empty", () => { + expect( + resolveFinalAssistantText({ finalText: "", streamedText: "Hello" }), + ).toBe("Hello"); + }); + + it("prefers the final text when present", () => { + expect( + resolveFinalAssistantText({ + finalText: "All done", + streamedText: "partial", + }), + ).toBe("All done"); + }); +}); diff --git a/src/tui/tui.ts b/src/tui/tui.ts index b2ba32e82..2b26547b5 100644 --- a/src/tui/tui.ts +++ b/src/tui/tui.ts @@ -39,6 +39,17 @@ export type TuiOptions = { message?: string; }; +export function resolveFinalAssistantText(params: { + finalText?: string | null; + streamedText?: string | null; +}) { + const finalText = params.finalText ?? ""; + if (finalText.trim()) return finalText; + const streamedText = params.streamedText ?? ""; + if (streamedText.trim()) return streamedText; + return "(no output)"; +} + type ChatEvent = { runId: string; sessionKey: string; @@ -642,7 +653,11 @@ export async function runTui(opts: TuiOptions) { const text = extractTextFromMessage(evt.message, { includeThinking: showThinking, }); - chatLog.finalizeAssistant(text || "(no output)", evt.runId); + const finalText = resolveFinalAssistantText({ + finalText: text, + streamedText: chatLog.getStreamingText(evt.runId), + }); + chatLog.finalizeAssistant(finalText, evt.runId); noteFinalizedRun(evt.runId); activeChatRunId = null; setActivityStatus("idle");