From 2bd40aee1bf8413e8908b1d4e44cfcdc71b984e0 Mon Sep 17 00:00:00 2001
From: ZhenYi <434836402@qq.com>
Date: Mon, 27 Apr 2026 22:57:18 +0800
Subject: [PATCH] fix(room): clear AI stream state on room switch and hide
cursor for saved chunks
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Clear activeAiStream, streamingChunks, and timers when room changes
- Add showCursor prop to OrderedStreamChunks — only show cursor
during active streaming, not for saved content
---
src/components/room/message/MessageBubble.tsx | 10 ++++--
src/contexts/room-context.tsx | 33 ++++++++++++++++---
2 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/src/components/room/message/MessageBubble.tsx b/src/components/room/message/MessageBubble.tsx
index 5d4f450..54d9af1 100644
--- a/src/components/room/message/MessageBubble.tsx
+++ b/src/components/room/message/MessageBubble.tsx
@@ -52,9 +52,12 @@ function parseSavedChunks(raw: string | null | undefined): Array<{ type: string;
function OrderedStreamChunks({
chunks,
onMentionClick,
+ showCursor = true,
}: {
chunks: Array<{ type: string; content: string }>;
onMentionClick?: (type: string, id: string, label: string) => void;
+ /** Show blinking cursor — only during active streaming */
+ showCursor?: boolean;
}) {
// Group consecutive same-type chunks (tool_call hidden)
const groups: Array<{ type: 'thinking' | 'answer'; content: string }> = [];
@@ -78,8 +81,8 @@ function OrderedStreamChunks({
),
)}
- {/* Streaming cursor */}
-
+ {/* Streaming cursor — only shown during active streaming */}
+ {showCursor && }
>
);
}
@@ -415,10 +418,11 @@ export const MessageBubble = memo(function MessageBubble({
onMentionClick={handleMentionClick}
/>
) : parseSavedChunks(message.thinking_content) ? (
- /* Saved ordered chunks — render in original order */
+ /* Saved ordered chunks — render in original order (no cursor) */
) : (
/* Legacy: aggregated thinking at top, content at bottom */
diff --git a/src/contexts/room-context.tsx b/src/contexts/room-context.tsx
index 8e775b7..3035a12 100644
--- a/src/contexts/room-context.tsx
+++ b/src/contexts/room-context.tsx
@@ -278,6 +278,13 @@ export function RoomProvider({
setMessages([]);
setIsHistoryLoaded(false);
setNextCursor(null);
+ // Clear AI stream state — prevents ghost "thinking..." indicator on room switch
+ setActiveAiStream(null);
+ setStreamingChunks(new Map());
+ streamingChunksRef.current.clear();
+ // Clear all streaming timers
+ streamingTimersRef.current.forEach((timer) => clearTimeout(timer));
+ streamingTimersRef.current.clear();
// Merge any buffered messages for the new room (Bug 3 fix)
if (activeRoomId) {
@@ -1310,6 +1317,7 @@ export function RoomProvider({
}, [projectName]);
// Fetch room AI configs for @ai: mention suggestions
+ // Retry up to 3 times if any config has missing modelName
const fetchRoomAiConfigs = useCallback(async () => {
const client = wsClientRef.current;
if (!activeRoomId || !client) {
@@ -1318,13 +1326,28 @@ export function RoomProvider({
}
setAiConfigsLoading(true);
try {
- const configs = await client.aiList(activeRoomId);
- setRoomAiConfigs(
- configs.map((cfg) => ({
+ const fetchOnce = async (): Promise => {
+ const configs = await client.aiList(activeRoomId);
+ return configs.map((cfg) => ({
model: cfg.model,
modelName: cfg.model_name,
- })),
- );
+ }));
+ };
+
+ let configs = await fetchOnce();
+ let retries = 0;
+ const maxRetries = 3;
+
+ while (
+ configs.some((c) => !c.modelName) &&
+ retries < maxRetries
+ ) {
+ retries++;
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ configs = await fetchOnce();
+ }
+
+ setRoomAiConfigs(configs);
} catch {
setRoomAiConfigs([]);
} finally {