From a4f80f905d94f58f78f201cfbec650c6780be675 Mon Sep 17 00:00:00 2001 From: "clawsweeper[bot]" <274271284+clawsweeper[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 17:28:26 +0000 Subject: [PATCH] fix(ui): prevent reading indicator from sticking after assistant response (#83711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: - The PR removes the Control UI chat fallback that converts a null stream into an empty stream for abortable runs, adds null-vs-empty stream regression tests, and updates the changelog. - Reproducibility: yes. source-level reproduction is high confidence: current main converts null stream plus c ... ading indicator. The linked source PR also reports live Control UI verification after the equivalent patch. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(ui): prevent reading indicator from sticking after assistant resp… Validation: - ClawSweeper review passed for head 44bea55110f3b0b3a267d080e9da293d3fd21147. - Required merge gates passed before the squash merge. Prepared head SHA: 44bea55110f3b0b3a267d080e9da293d3fd21147 Review: https://github.com/openclaw/openclaw/pull/83711#issuecomment-4480128171 Co-authored-by: 二狗子 Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + ui/src/ui/views/chat.test.ts | 29 ++++++++++++++++++++++++++--- ui/src/ui/views/chat.ts | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) 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");