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 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-25 00:34:22 +03:00
parent bf2ebb9d47
commit 5aa199660d
2 changed files with 26 additions and 2 deletions

View File

@@ -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) &&

View File

@@ -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);
});
});