@@ -105,6 +105,11 @@ export function usePlanWebSocket({
105105 const streamingMessageBuffer = useAppSelector ( selectStreamingMessageBuffer ) ;
106106 const processingStartedAtRef = React . useRef < number | null > ( null ) ;
107107
108+ // Coalesce high-frequency streaming tokens into one flush per animation frame
109+ // to avoid a synchronous re-render per token freezing the UI on fast streams.
110+ const streamingChunkQueueRef = React . useRef < string [ ] > ( [ ] ) ;
111+ const streamingFlushHandleRef = React . useRef < number | null > ( null ) ;
112+
108113 useEffect ( ( ) => {
109114 if ( showProcessingPlanSpinner ) {
110115 if ( processingStartedAtRef . current === null ) {
@@ -144,15 +149,38 @@ export function usePlanWebSocket({
144149
145150 // ── AGENT_MESSAGE_STREAMING ───────────────────────────────────
146151 useEffect ( ( ) => {
152+ const flushStreamingChunks = ( ) => {
153+ streamingFlushHandleRef . current = null ;
154+ const chunks = streamingChunkQueueRef . current ;
155+ if ( chunks . length === 0 ) return ;
156+ streamingChunkQueueRef . current = [ ] ;
157+ dispatch ( setShowBufferingText ( true ) ) ;
158+ dispatch ( appendToStreamingBuffer ( chunks . join ( '' ) ) ) ;
159+ } ;
160+
147161 const unsub = webSocketService . on (
148162 WebsocketMessageType . AGENT_MESSAGE_STREAMING ,
149163 ( msg : any ) => {
150164 const line = PlanDataService . simplifyHumanClarification ( msg . data ?. content || msg . content || '' ) ;
151- dispatch ( setShowBufferingText ( true ) ) ;
152- dispatch ( appendToStreamingBuffer ( line ) ) ;
165+ streamingChunkQueueRef . current . push ( line ) ;
166+ if ( streamingFlushHandleRef . current === null ) {
167+ streamingFlushHandleRef . current = requestAnimationFrame ( flushStreamingChunks ) ;
168+ }
153169 } ,
154170 ) ;
155- return unsub ;
171+ return ( ) => {
172+ unsub ( ) ;
173+ // Cancel pending frame and flush leftovers so no streamed text is lost
174+ if ( streamingFlushHandleRef . current !== null ) {
175+ cancelAnimationFrame ( streamingFlushHandleRef . current ) ;
176+ streamingFlushHandleRef . current = null ;
177+ }
178+ if ( streamingChunkQueueRef . current . length > 0 ) {
179+ const remaining = streamingChunkQueueRef . current . join ( '' ) ;
180+ streamingChunkQueueRef . current = [ ] ;
181+ dispatch ( appendToStreamingBuffer ( remaining ) ) ;
182+ }
183+ } ;
156184 } , [ dispatch ] ) ;
157185
158186 // ── USER_CLARIFICATION_REQUEST ────────────────────────────────
@@ -241,6 +269,27 @@ export function usePlanWebSocket({
241269 scrollToBottom ( ) ;
242270 showToast ( errorContent , 'error' ) ;
243271 webSocketService . disconnect ( ) ;
272+ } else {
273+ // Any other terminal status (e.g. "terminated"): clear the spinner
274+ // so the UI doesn't hang after the answer has already arrived.
275+ const content = finalMessage . data ?. content ;
276+ if ( content ) {
277+ const terminalMessage : AgentMessageData = {
278+ agent : AgentType . GROUP_CHAT_MANAGER ,
279+ agent_type : AgentMessageType . AI_AGENT ,
280+ timestamp : Date . now ( ) ,
281+ steps : [ ] ,
282+ next_steps : [ ] ,
283+ content,
284+ raw_data : finalMessage ,
285+ } ;
286+ dispatch ( addAgentMessage ( terminalMessage ) ) ;
287+ }
288+ dispatch ( setShowBufferingText ( false ) ) ;
289+ dispatch ( setShowProcessingPlanSpinner ( false ) ) ;
290+ processingStartedAtRef . current = null ;
291+ scrollToBottom ( ) ;
292+ webSocketService . disconnect ( ) ;
244293 }
245294 } ,
246295 ) ;
0 commit comments