Skip to content

Commit 50edcc1

Browse files
feat: enhance WebSocket handling with streaming chunk coalescing and cleanup
1 parent 2584721 commit 50edcc1

1 file changed

Lines changed: 57 additions & 6 deletions

File tree

src/App/src/hooks/usePlanWebSocket.tsx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Dispatches Redux actions for each event type so PlanPage no longer
66
* needs 7+ useEffect blocks for WebSocket handling.
77
*/
8-
import React, { useEffect, useRef } from 'react';
8+
import React, { useEffect } from 'react';
99
import webSocketService from '@/store/WebSocketService';
1010
import { PlanDataService } from '@/store/PlanDataService';
1111
import { useAppDispatch, useAppSelector } from '@/store/hooks';
@@ -106,6 +106,22 @@ export function usePlanWebSocket({
106106
const showProcessingPlanSpinner = useAppSelector(selectShowProcessingPlanSpinner);
107107
const continueWithWebsocketFlow = useAppSelector(selectContinueWithWebsocketFlow);
108108
const streamingMessageBuffer = useAppSelector(selectStreamingMessageBuffer);
109+
const processingStartedAtRef = React.useRef<number | null>(null);
110+
111+
// Coalesce high-frequency streaming tokens into one flush per animation frame
112+
// to avoid a synchronous re-render per token freezing the UI on fast streams.
113+
const streamingChunkQueueRef = React.useRef<string[]>([]);
114+
const streamingFlushHandleRef = React.useRef<number | null>(null);
115+
116+
useEffect(() => {
117+
if (showProcessingPlanSpinner) {
118+
if (processingStartedAtRef.current === null) {
119+
processingStartedAtRef.current = Date.now();
120+
}
121+
} else {
122+
processingStartedAtRef.current = null;
123+
}
124+
}, [showProcessingPlanSpinner]);
109125

110126
// ── PLAN_APPROVAL_REQUEST ─────────────────────────────────────
111127
useEffect(() => {
@@ -148,12 +164,26 @@ export function usePlanWebSocket({
148164
const unsub = webSocketService.on(
149165
WebsocketMessageType.AGENT_MESSAGE_STREAMING,
150166
(msg: any) => {
151-
const line = PlanDataService.simplifyHumanClarification(msg.data.content);
152-
dispatch(setShowBufferingText(true));
153-
dispatch(appendToStreamingBuffer(line));
167+
const line = PlanDataService.simplifyHumanClarification(msg.data?.content || msg.content || '');
168+
streamingChunkQueueRef.current.push(line);
169+
if (streamingFlushHandleRef.current === null) {
170+
streamingFlushHandleRef.current = requestAnimationFrame(flushStreamingChunks);
171+
}
154172
},
155173
);
156-
return unsub;
174+
return () => {
175+
unsub();
176+
// Cancel pending frame and flush leftovers so no streamed text is lost
177+
if (streamingFlushHandleRef.current !== null) {
178+
cancelAnimationFrame(streamingFlushHandleRef.current);
179+
streamingFlushHandleRef.current = null;
180+
}
181+
if (streamingChunkQueueRef.current.length > 0) {
182+
const remaining = streamingChunkQueueRef.current.join('');
183+
streamingChunkQueueRef.current = [];
184+
dispatch(appendToStreamingBuffer(remaining));
185+
}
186+
};
157187
}, [dispatch]);
158188

159189
// ── USER_CLARIFICATION_REQUEST ────────────────────────────────
@@ -242,6 +272,27 @@ export function usePlanWebSocket({
242272
scrollToBottom();
243273
showToast(errorContent, 'error');
244274
webSocketService.disconnect();
275+
} else {
276+
// Any other terminal status (e.g. "terminated"): clear the spinner
277+
// so the UI doesn't hang after the answer has already arrived.
278+
const content = finalMessage.data?.content;
279+
if (content) {
280+
const terminalMessage: AgentMessageData = {
281+
agent: AgentType.GROUP_CHAT_MANAGER,
282+
agent_type: AgentMessageType.AI_AGENT,
283+
timestamp: Date.now(),
284+
steps: [],
285+
next_steps: [],
286+
content,
287+
raw_data: finalMessage,
288+
};
289+
dispatch(addAgentMessage(terminalMessage));
290+
}
291+
dispatch(setShowBufferingText(false));
292+
dispatch(setShowProcessingPlanSpinner(false));
293+
processingStartedAtRef.current = null;
294+
scrollToBottom();
295+
webSocketService.disconnect();
245296
}
246297
},
247298
);
@@ -372,4 +423,4 @@ export function usePlanWebSocket({
372423
}, [dispatch, planId, continueWithWebsocketFlow]);
373424
}
374425

375-
export default usePlanWebSocket;
426+
export default usePlanWebSocket;

0 commit comments

Comments
 (0)