fix(room): clear AI stream state on room switch and hide cursor for saved chunks

- Clear activeAiStream, streamingChunks, and timers when room changes
- Add showCursor prop to OrderedStreamChunks — only show cursor
  during active streaming, not for saved content
This commit is contained in:
ZhenYi 2026-04-27 22:57:18 +08:00
parent ab1ef0d1a7
commit 2bd40aee1b
2 changed files with 35 additions and 8 deletions

View File

@ -52,9 +52,12 @@ function parseSavedChunks(raw: string | null | undefined): Array<{ type: string;
function OrderedStreamChunks({ function OrderedStreamChunks({
chunks, chunks,
onMentionClick, onMentionClick,
showCursor = true,
}: { }: {
chunks: Array<{ type: string; content: string }>; chunks: Array<{ type: string; content: string }>;
onMentionClick?: (type: string, id: string, label: string) => void; 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) // Group consecutive same-type chunks (tool_call hidden)
const groups: Array<{ type: 'thinking' | 'answer'; content: string }> = []; const groups: Array<{ type: 'thinking' | 'answer'; content: string }> = [];
@ -78,8 +81,8 @@ function OrderedStreamChunks({
<MessageContent key={i} content={group.content} onMentionClick={onMentionClick} /> <MessageContent key={i} content={group.content} onMentionClick={onMentionClick} />
), ),
)} )}
{/* Streaming cursor */} {/* Streaming cursor — only shown during active streaming */}
<span className="discord-streaming-cursor" /> {showCursor && <span className="discord-streaming-cursor" />}
</> </>
); );
} }
@ -415,10 +418,11 @@ export const MessageBubble = memo(function MessageBubble({
onMentionClick={handleMentionClick} onMentionClick={handleMentionClick}
/> />
) : parseSavedChunks(message.thinking_content) ? ( ) : parseSavedChunks(message.thinking_content) ? (
/* Saved ordered chunks — render in original order */ /* Saved ordered chunks — render in original order (no cursor) */
<OrderedStreamChunks <OrderedStreamChunks
chunks={parseSavedChunks(message.thinking_content)!} chunks={parseSavedChunks(message.thinking_content)!}
onMentionClick={handleMentionClick} onMentionClick={handleMentionClick}
showCursor={false}
/> />
) : ( ) : (
/* Legacy: aggregated thinking at top, content at bottom */ /* Legacy: aggregated thinking at top, content at bottom */

View File

@ -278,6 +278,13 @@ export function RoomProvider({
setMessages([]); setMessages([]);
setIsHistoryLoaded(false); setIsHistoryLoaded(false);
setNextCursor(null); 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) // Merge any buffered messages for the new room (Bug 3 fix)
if (activeRoomId) { if (activeRoomId) {
@ -1310,6 +1317,7 @@ export function RoomProvider({
}, [projectName]); }, [projectName]);
// Fetch room AI configs for @ai: mention suggestions // Fetch room AI configs for @ai: mention suggestions
// Retry up to 3 times if any config has missing modelName
const fetchRoomAiConfigs = useCallback(async () => { const fetchRoomAiConfigs = useCallback(async () => {
const client = wsClientRef.current; const client = wsClientRef.current;
if (!activeRoomId || !client) { if (!activeRoomId || !client) {
@ -1318,13 +1326,28 @@ export function RoomProvider({
} }
setAiConfigsLoading(true); setAiConfigsLoading(true);
try { try {
const configs = await client.aiList(activeRoomId); const fetchOnce = async (): Promise<RoomAiConfig[]> => {
setRoomAiConfigs( const configs = await client.aiList(activeRoomId);
configs.map((cfg) => ({ return configs.map((cfg) => ({
model: cfg.model, model: cfg.model,
modelName: cfg.model_name, 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 { } catch {
setRoomAiConfigs([]); setRoomAiConfigs([]);
} finally { } finally {