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:
@@ -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) &&
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user