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];
});
// 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<StreamingState | null>(null);
const streamRafRef = useRef<number>(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]);