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 {