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({
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({
<MessageContent key={i} content={group.content} onMentionClick={onMentionClick} />
),
)}
{/* Streaming cursor */}
<span className="discord-streaming-cursor" />
{/* Streaming cursor — only shown during active streaming */}
{showCursor && <span className="discord-streaming-cursor" />}
</>
);
}
@ -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) */
<OrderedStreamChunks
chunks={parseSavedChunks(message.thinking_content)!}
onMentionClick={handleMentionClick}
showCursor={false}
/>
) : (
/* Legacy: aggregated thinking at top, content at bottom */

View File

@ -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<RoomAiConfig[]> => {
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 {