This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new ef9b98269 fix: prevent unread badge from inflating when streaming
fails (#4715)
ef9b98269 is described below
commit ef9b98269ad85ae0cf230c1f687f6ae366c926fc
Author: cyberslack_lee <[email protected]>
AuthorDate: Mon Sep 21 21:10:39 2026 +0800
fix: prevent unread badge from inflating when streaming fails (#4715)
`ChatThread.tsx` counted unread messages with an effect keyed on
`bubbleCount` that incremented on any change, although its own comment says it
counts what arrives while the reader is away. `bubbleCount` is `bubbles.length`
plus one whenever live blocks exist, so it goes down too: when an SSE read
fails, `useAgentRun` finishes the stream and refetches the timeline, and if the
run has not written an assistant row yet the refetch clears the live blocks and
the count drops. A reader who [...]
The effect now keeps the previous count in a ref and returns early unless
the count actually grew, so only arrivals increment the badge. That is the
component's only increment site — the other three write points clear it — and a
conversation switch already resets `atBottomRef` before this effect runs, so
nothing is counted across a switch.
---
web/src/pages/ai/components/ChatThread.tsx | 7 +++++-
.../ai/components/__tests__/ChatThread.test.tsx | 27 ++++++++++++++++++++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/web/src/pages/ai/components/ChatThread.tsx
b/web/src/pages/ai/components/ChatThread.tsx
index b350070b5..e954163e5 100644
--- a/web/src/pages/ai/components/ChatThread.tsx
+++ b/web/src/pages/ai/components/ChatThread.tsx
@@ -100,6 +100,7 @@ const ChatThread = ({
const positionedRef = useRef(false);
const [atBottom, setAtBottom] = useState(true);
const [unread, setUnread] = useState(0);
+ const prevBubbleCountRef = useRef(bubbles.length);
const bubbleCount = bubbles.length + (liveBlocks && liveBlocks.length > 0 ?
1 : 0);
@@ -149,8 +150,12 @@ const ChatThread = ({
}, [bubbles, followToBottom, liveBlocks]);
// Count what arrives while the reader is away: one per message, not one per
streamed token.
+ // Only a genuine increase in bubble count is a new message — a streaming
failure that removes
+ // the live bubble must not inflate the badge.
useEffect(() => {
- if (atBottomRef.current) return;
+ const previous = prevBubbleCountRef.current;
+ prevBubbleCountRef.current = bubbleCount;
+ if (atBottomRef.current || bubbleCount <= previous) return;
setUnread((count) => count + 1);
}, [bubbleCount]);
diff --git a/web/src/pages/ai/components/__tests__/ChatThread.test.tsx
b/web/src/pages/ai/components/__tests__/ChatThread.test.tsx
index 75f909b64..a1d0f7773 100644
--- a/web/src/pages/ai/components/__tests__/ChatThread.test.tsx
+++ b/web/src/pages/ai/components/__tests__/ChatThread.test.tsx
@@ -222,6 +222,33 @@ describe('ChatThread', () => {
expect(screen.queryByTestId('ai-thread-jump-to-latest')).not.toBeInTheDocument();
});
+ it('doesNotInflateUnreadWhenStreamingFailsAndLiveBlocksDisappearTest', () =>
{
+ const { rerender } = renderThread({ bubbles: [userBubble('检查集群状态')] });
+ placeReader(600);
+ fireEvent.scroll(screen.getByTestId('ai-thread-scroll'));
+
+ // The answer starts streaming: one new message arrives.
+ rerender(
+ <LangProvider>
+ <ChatThread
+ bubbles={[userBubble('检查集群状态')]}
+ liveBlocks={appendText([], '部分')}
+ streaming
+ />
+ </LangProvider>,
+ );
+ expect(screen.getByTestId('ai-thread-unread')).toHaveTextContent('1');
+
+ // Streaming fails / is aborted and the live bubble disappears without
being persisted.
+ // The unread count must not increase when a bubble is removed.
+ rerender(
+ <LangProvider>
+ <ChatThread bubbles={[userBubble('检查集群状态')]} />
+ </LangProvider>,
+ );
+ expect(screen.getByTestId('ai-thread-unread')).toHaveTextContent('1');
+ });
+
it('rendersTheEmptySlotWhenThereIsNoTranscriptTest', () => {
render(
<LangProvider>