fix: streaming text accumulation via ref + rAF throttle (prevent duplicate chars)

This commit is contained in:
Markov 2026-03-14 06:27:16 +01:00
parent 7c0580e666
commit 665024e70b

View File

@ -73,40 +73,58 @@ export default function ChatPanel({ chatId, projectId }: Props) {
return [...prev, msg]; return [...prev, msg];
}); });
// Clear streaming when final message arrives // Clear streaming when final message arrives
streamRef.current = null;
setStreaming(null); setStreaming(null);
}); });
return () => { unsub?.(); }; return () => { unsub?.(); };
}, [chatId]); }, [chatId]);
// WS: agent streaming events // WS: agent streaming events — use ref to accumulate, throttle renders
const streamRef = useRef<StreamingState | null>(null);
const streamRafRef = useRef<number>(0);
useEffect(() => { useEffect(() => {
const flushStream = () => {
setStreaming(streamRef.current ? { ...streamRef.current } : null);
};
const scheduleFlush = () => {
if (!streamRafRef.current) {
streamRafRef.current = requestAnimationFrame(() => {
streamRafRef.current = 0;
flushStream();
});
}
};
const unsubStart = wsClient.on("agent.stream.start", (data: any) => { const unsubStart = wsClient.on("agent.stream.start", (data: any) => {
if (data.chat_id !== chatId) return; if (data.chat_id !== chatId) return;
setStreaming({ agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }); streamRef.current = { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] };
flushStream();
}); });
const unsubDelta = wsClient.on("agent.stream.delta", (data: any) => { const unsubDelta = wsClient.on("agent.stream.delta", (data: any) => {
if (data.chat_id !== chatId) return; if (data.chat_id !== chatId) return;
setStreaming((prev) => { const s = streamRef.current;
if (!prev) return { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }; if (!s) {
if (data.block_type === "thinking") { streamRef.current = { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] };
return { ...prev, thinking: prev.thinking + (data.text || "") };
} }
return { ...prev, text: prev.text + (data.text || "") }; if (data.block_type === "thinking") {
}); streamRef.current!.thinking += data.text || "";
} else {
streamRef.current!.text += data.text || "";
}
scheduleFlush();
}); });
const unsubTool = wsClient.on("agent.stream.tool", (data: any) => { const unsubTool = wsClient.on("agent.stream.tool", (data: any) => {
if (data.chat_id !== chatId) return; if (data.chat_id !== chatId) return;
setStreaming((prev) => { const s = streamRef.current;
if (!prev) return null; if (!s) return;
const tools = [...prev.tools]; const existing = s.tools.findIndex((t) => t.tool === data.tool);
const existing = tools.findIndex((t) => t.tool === data.tool);
if (existing >= 0) { if (existing >= 0) {
tools[existing] = { tool: data.tool, status: data.status }; s.tools[existing] = { tool: data.tool, status: data.status };
} else { } else {
tools.push({ tool: data.tool, status: data.status }); s.tools.push({ tool: data.tool, status: data.status });
} }
return { ...prev, tools }; scheduleFlush();
});
}); });
const unsubEnd = wsClient.on("agent.stream.end", (data: any) => { const unsubEnd = wsClient.on("agent.stream.end", (data: any) => {
if (data.chat_id !== chatId) return; if (data.chat_id !== chatId) return;
@ -117,6 +135,8 @@ export default function ChatPanel({ chatId, projectId }: Props) {
unsubDelta?.(); unsubDelta?.();
unsubTool?.(); unsubTool?.();
unsubEnd?.(); unsubEnd?.();
if (streamRafRef.current) cancelAnimationFrame(streamRafRef.current);
streamRef.current = null;
}; };
}, [chatId]); }, [chatId]);