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' ;
99import webSocketService from '@/store/WebSocketService' ;
1010import { PlanDataService } from '@/store/PlanDataService' ;
1111import { 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