From 665024e70bf733ff2939957b69be0eebb2147dee Mon Sep 17 00:00:00 2001 From: Markov Date: Sat, 14 Mar 2026 06:27:16 +0100 Subject: [PATCH] fix: streaming text accumulation via ref + rAF throttle (prevent duplicate chars) --- src/components/ChatPanel.tsx | 60 ++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index ad35e80..3a03149 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -73,40 +73,58 @@ export default function ChatPanel({ chatId, projectId }: Props) { return [...prev, msg]; }); // Clear streaming when final message arrives + streamRef.current = null; setStreaming(null); }); return () => { unsub?.(); }; }, [chatId]); - // WS: agent streaming events + // WS: agent streaming events — use ref to accumulate, throttle renders + const streamRef = useRef(null); + const streamRafRef = useRef(0); + 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) => { 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) => { if (data.chat_id !== chatId) return; - setStreaming((prev) => { - if (!prev) return { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }; - if (data.block_type === "thinking") { - return { ...prev, thinking: prev.thinking + (data.text || "") }; - } - return { ...prev, text: prev.text + (data.text || "") }; - }); + const s = streamRef.current; + if (!s) { + streamRef.current = { agentSlug: data.agent_slug || "agent", text: "", thinking: "", tools: [] }; + } + 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) => { if (data.chat_id !== chatId) return; - setStreaming((prev) => { - if (!prev) return null; - const tools = [...prev.tools]; - const existing = tools.findIndex((t) => t.tool === data.tool); - if (existing >= 0) { - tools[existing] = { tool: data.tool, status: data.status }; - } else { - tools.push({ tool: data.tool, status: data.status }); - } - return { ...prev, tools }; - }); + const s = streamRef.current; + if (!s) return; + const existing = s.tools.findIndex((t) => t.tool === data.tool); + if (existing >= 0) { + s.tools[existing] = { tool: data.tool, status: data.status }; + } else { + s.tools.push({ tool: data.tool, status: data.status }); + } + scheduleFlush(); }); const unsubEnd = wsClient.on("agent.stream.end", (data: any) => { if (data.chat_id !== chatId) return; @@ -117,6 +135,8 @@ export default function ChatPanel({ chatId, projectId }: Props) { unsubDelta?.(); unsubTool?.(); unsubEnd?.(); + if (streamRafRef.current) cancelAnimationFrame(streamRafRef.current); + streamRef.current = null; }; }, [chatId]);