diff --git a/CHANGELOG.md b/CHANGELOG.md index 69da18858c14..6b8b11461f52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/image generation: allow distinct `image_generate` prompts to start separate session-backed background tasks while same-prompt retries still return the active task status. (#83614) Thanks @Elarwei001. +- Control UI: stop the chat reading indicator from sticking after an assistant response finishes. (#83515) Thanks @njuboy11. - Skills: reject empty or whitespace-only skill names and descriptions during quick validation. (#27061) - Sessions: skip trailing custom transcript entries when checking tail assistant replies so embedded CLI gap-fill does not duplicate canonical assistant output. (#83635) Thanks @yaoyi1222. - Memory Wiki: keep `wiki_lint` tool output path-safe by reporting vault-internal lint reports as relative paths in tool text and details while preserving absolute report paths for CLI/file callers. (#83439) Thanks @LLagoon3. diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index f890dc840eb4..e5921a6aafff 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -489,14 +489,37 @@ describe("chat loading skeleton", () => { expect(container.querySelector(".agent-chat__welcome")).toBeNull(); }); - it("shows the reading indicator instead of the skeleton while an active run has no stream yet", () => { + it("shows the loading skeleton for an active run with no stream", () => { const container = renderChatView({ canAbort: true, loading: true }); - expect(container.querySelector(".chat-loading-skeleton")).toBeNull(); - expect(container.querySelectorAll(".chat-reading-indicator")).toHaveLength(1); + expect(container.querySelector(".chat-loading-skeleton")).not.toBeNull(); + expect(container.querySelectorAll(".chat-reading-indicator")).toHaveLength(0); expect(container.querySelector(".agent-chat__welcome")).toBeNull(); }); + it("shows the reading indicator when an active run has an empty stream", () => { + const container = renderChatView({ canAbort: true, stream: "" }); + + expect(container.querySelector(".chat-reading-indicator")).not.toBeNull(); + }); + + it("does not keep the reading indicator after an assistant response has rendered", () => { + const container = renderChatView({ + canAbort: true, + messages: [ + { + role: "assistant", + content: "Finished answer", + timestamp: 1, + }, + ], + stream: null, + }); + + expect(container.querySelector(".chat-reading-indicator")).toBeNull(); + expect(container.querySelector(".chat-group")?.textContent?.trim()).toBe("Finished answer"); + }); + it("keeps existing messages visible without the skeleton during a background reload", () => { const container = renderChatView({ loading: true, diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index f6fba835b3b6..85d7a8c5d27b 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -1003,7 +1003,7 @@ export function renderChat(props: ChatProps) { const requestUpdate = props.onRequestUpdate ?? (() => {}); const splitRatio = props.splitRatio ?? 0.6; const sidebarOpen = Boolean(props.sidebarOpen && props.onCloseSidebar); - const displayStream = props.stream ?? (canAbort ? "" : null); + const displayStream = props.stream ?? null; const handleCodeBlockCopy = (e: Event) => { const btn = (e.target as HTMLElement).closest(".code-block-copy");