From 5aa199660d42a8ac27029f04e125d81a13805b28 Mon Sep 17 00:00:00 2001 From: claude_code Date: Thu, 25 Jun 2026 00:34:22 +0300 Subject: [PATCH] fix(ai-chat): keep thinking dots visible between streamed steps showTypingIndicator hid the standalone thinking dots for any non-empty trailing text part, so during the pause after the model finished an intermediate narration and before its next step (e.g. a tool call) the UI looked frozen. Suppress the dots only while the text part is still streaming: a finalized ("done") trailing text part on an in-flight turn now shows the dots again, matching the function's documented intent. - message-list: guard the text branch with state !== "done" (AI SDK v6 TextUIPart.state); stateless parts keep their previous behavior - show-typing-indicator.test: add done -> shown and streaming -> hidden cases Co-Authored-By: Claude Opus 4.8 --- .../ai-chat/components/message-list.tsx | 18 ++++++++++++++++-- .../components/show-typing-indicator.test.ts | 10 ++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/apps/client/src/features/ai-chat/components/message-list.tsx b/apps/client/src/features/ai-chat/components/message-list.tsx index f04ca2ab..fda2a87f 100644 --- a/apps/client/src/features/ai-chat/components/message-list.tsx +++ b/apps/client/src/features/ai-chat/components/message-list.tsx @@ -50,7 +50,9 @@ const BOTTOM_THRESHOLD = 40; * assistant message's LAST part is not live output: * - the last message is still the user's (assistant hasn't started a row), or * - the assistant row has no parts yet, or - * - its last part is an empty/whitespace text part, or + * - its last part is an empty/whitespace text part, or a finished ("done") + * text part while the turn continues (the model paused after some narration + * and is thinking about its next step), or * - its last part is a finished/errored tool (the model is thinking about the * next step between tool calls). * It hides only while output is actively rendering: a non-empty streaming text @@ -64,7 +66,19 @@ export function showTypingIndicator(messages: UIMessage[], isStreaming: boolean) const lastPart = last.parts[last.parts.length - 1]; if (!lastPart) return true; // assistant row exists but has no parts yet. // The answer text is actively streaming in -> MessageItem renders it; no dots. - if (lastPart.type === "text" && lastPart.text.trim().length > 0) return false; + // Only while it is STILL streaming, though: once a non-empty text part is + // finalized ("done") but the turn is still in flight, the model has paused + // after some narration and is working on its next step (e.g. about to call a + // tool) — nothing is visibly progressing, so the dots must show. A text part + // without a `state` is treated as still-rendering (kept suppressed); this + // branch only runs while streaming, where live parts always carry a state. + if ( + lastPart.type === "text" && + lastPart.text.trim().length > 0 && + (lastPart as { state?: "streaming" | "done" }).state !== "done" + ) { + return false; + } // A tool still in flight shows its own Loader in ToolCallCard -> no dots. if ( isToolPart(lastPart.type) && diff --git a/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts b/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts index 0c18431b..34364b55 100644 --- a/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts +++ b/apps/client/src/features/ai-chat/components/show-typing-indicator.test.ts @@ -82,4 +82,14 @@ describe("showTypingIndicator", () => { showTypingIndicator([msg("assistant", [doneTool, text])], true), ).toBe(false); }); + + it("shows while streaming after a text part is finalized (paused before the next step)", () => { + const doneText = { type: "text", text: "Now creating the page in", state: "done" } as unknown as UIMessage["parts"][number]; + expect(showTypingIndicator([msg("assistant", [doneText])], true)).toBe(true); + }); + + it("hides while a text part is actively streaming (state: streaming)", () => { + const streamingText = { type: "text", text: "Now writ", state: "streaming" } as unknown as UIMessage["parts"][number]; + expect(showTypingIndicator([msg("assistant", [streamingText])], true)).toBe(false); + }); });