@@ -125,6 +125,8 @@ export default function AgentChat() {
125125 // Tracks which conversation initiated the current request — SSE responses
126126 // are pinned to this ID so switching tabs doesn't misdirect them.
127127 const processingChatIdRef = useRef ( null )
128+ // Maps backend messageID → conversationId for robust SSE routing across navigations.
129+ const pendingRequestsRef = useRef ( new Map ( ) )
128130
129131 const processing = processingChatId === activeId
130132
@@ -142,20 +144,29 @@ export default function AgentChat() {
142144 es . addEventListener ( 'json_message' , ( e ) => {
143145 try {
144146 const data = JSON . parse ( e . data )
147+ const sender = data . sender || ( data . role === 'user' ? 'user' : 'agent' )
148+ // Skip user message echoes — already added locally in handleSend
149+ if ( sender === 'user' ) return
145150 const msg = {
146151 id : nextId ( ) ,
147- sender : data . sender || ( data . role === 'user' ? 'user' : 'agent' ) ,
152+ sender,
148153 content : data . content || data . message || '' ,
149154 timestamp : data . timestamp ? Math . floor ( data . timestamp / 1e6 ) : Date . now ( ) ,
150155 }
151156 if ( data . metadata && Object . keys ( data . metadata ) . length > 0 ) {
152157 msg . metadata = data . metadata
153158 }
154- // Pin message to the conversation that initiated the request
155- const targetId = processingChatIdRef . current || activeIdRef . current
159+ // Route to conversation: try messageID mapping first, then processingChatIdRef, then active
160+ const msgId = data . message_id || ''
161+ const baseId = msgId . replace ( / - a g e n t $ / , '' )
162+ const targetId = pendingRequestsRef . current . get ( baseId )
163+ || processingChatIdRef . current
164+ || activeIdRef . current
156165 addMessageToConvRef . current ( targetId , msg )
157166 // Clear streaming + processing state when the final agent message arrives
158- if ( msg . sender === 'agent' ) {
167+ if ( sender === 'agent' ) {
168+ pendingRequestsRef . current . delete ( baseId )
169+ processingChatIdRef . current = null
159170 setProcessingChatId ( null )
160171 setStreamContent ( '' )
161172 setStreamReasoning ( '' )
@@ -170,17 +181,20 @@ export default function AgentChat() {
170181 try {
171182 const data = JSON . parse ( e . data )
172183 if ( data . status === 'processing' ) {
173- // Track which conversation is processing so responses go to the right place
174- processingChatIdRef . current = activeIdRef . current
175- setProcessingChatId ( activeIdRef . current )
184+ // Track which conversation is processing so responses go to the right place.
185+ // Only set if not already pinned by handleSend (avoids race when user switches conversations).
186+ if ( ! processingChatIdRef . current ) {
187+ processingChatIdRef . current = activeIdRef . current
188+ setProcessingChatId ( activeIdRef . current )
189+ }
176190 setStreamContent ( '' )
177191 setStreamReasoning ( '' )
178192 setStreamToolCalls ( [ ] )
179193 } else if ( data . status === 'completed' ) {
180- processingChatIdRef . current = null
181- // Don't clear processingChatId or streaming state here —
194+ // Don't clear processingChatIdRef, processingChatId, or streaming state here —
182195 // they'll be cleared when the agent's json_message arrives,
183- // so reasoning and tool calls remain visible until the response replaces them.
196+ // so reasoning and tool calls remain visible until the response replaces them
197+ // and late-arriving messages still route to the correct conversation.
184198 }
185199 } catch ( _err ) {
186200 // ignore
@@ -206,6 +220,16 @@ export default function AgentChat() {
206220 updated [ updated . length - 1 ] = { ...updated [ updated . length - 1 ] , args : updated [ updated . length - 1 ] . args + args }
207221 return updated
208222 } )
223+ } else if ( data . type === 'tool_result' ) {
224+ const tname = data . tool_name || ''
225+ setStreamToolCalls ( prev => {
226+ const updated = [ ...prev ]
227+ const idx = updated . findLastIndex ( tc => tc . name === tname && ! tc . result )
228+ if ( idx >= 0 ) {
229+ updated [ idx ] = { ...updated [ idx ] , result : data . tool_result || 'done' }
230+ }
231+ return updated
232+ } )
209233 } else if ( data . type === 'done' ) {
210234 // Content will be finalized by json_message event
211235 }
@@ -244,6 +268,8 @@ export default function AgentChat() {
244268 return ( ) => {
245269 es . close ( )
246270 eventSourceRef . current = null
271+ processingChatIdRef . current = null
272+ pendingRequestsRef . current . clear ( )
247273 }
248274 } , [ name , userId , addToast , nextId ] )
249275
@@ -325,14 +351,22 @@ export default function AgentChat() {
325351 if ( ! msg || processing ) return
326352 setInput ( '' )
327353 if ( textareaRef . current ) textareaRef . current . style . height = 'auto'
354+ // Add user message locally immediately (like standard chat)
355+ addMessage ( { id : nextId ( ) , sender : 'user' , content : msg , timestamp : Date . now ( ) } )
328356 setProcessingChatId ( activeId )
357+ processingChatIdRef . current = activeId
329358 try {
330- await agentsApi . chat ( name , msg , userId )
359+ const resp = await agentsApi . chat ( name , msg , userId )
360+ // Map backend messageID → conversation so SSE events route correctly
361+ if ( resp && resp . message_id ) {
362+ pendingRequestsRef . current . set ( resp . message_id , activeId )
363+ }
331364 } catch ( err ) {
332365 addToast ( `Failed to send message: ${ err . message } ` , 'error' )
366+ processingChatIdRef . current = null
333367 setProcessingChatId ( null )
334368 }
335- } , [ input , processing , name , activeId , addToast , userId ] )
369+ } , [ input , processing , name , activeId , addToast , userId , addMessage , nextId ] )
336370
337371 const handleKeyDown = ( e ) => {
338372 if ( e . key === 'Enter' && ! e . shiftKey ) {
@@ -622,16 +656,28 @@ export default function AgentChat() {
622656 </ div >
623657 </ details >
624658 ) }
625- { streamToolCalls . length > 0 && ! streamContent && (
659+ { streamToolCalls . length > 0 && (
626660 < div className = "chat-activity-group" style = { { marginBottom : 'var(--spacing-sm)' } } >
627661 { streamToolCalls . map ( ( tc , idx ) => (
628- < div key = { idx } className = "chat-activity-item chat-activity-tool-call" style = { { padding : 'var(--spacing-xs) var(--spacing-sm)' } } >
629- < span className = "chat-activity-item-label" >
630- < i className = "fas fa-bolt" style = { { marginRight : 'var(--spacing-xs)' } } />
631- { tc . name }
632- </ span >
633- < span style = { { opacity : 0.5 , fontSize : '0.85em' , marginLeft : 'var(--spacing-xs)' } } > calling...</ span >
634- </ div >
662+ < details key = { idx } className = "chat-activity-item chat-activity-tool-call" style = { { padding : 'var(--spacing-xs) var(--spacing-sm)' } } open = { ! tc . result } >
663+ < summary className = "chat-activity-item-label" style = { { cursor : 'pointer' , display : 'flex' , alignItems : 'center' , gap : 'var(--spacing-xs)' } } >
664+ < i className = { `fas ${ tc . result ? 'fa-check' : 'fa-bolt' } ` } />
665+ < strong > { tc . name } </ strong >
666+ < span style = { { opacity : 0.5 , fontSize : '0.85em' } } >
667+ { tc . result ? 'done' : 'calling...' }
668+ </ span >
669+ </ summary >
670+ { tc . args && (
671+ < pre style = { { margin : '4px 0' , fontSize : '0.75rem' , opacity : 0.8 , whiteSpace : 'pre-wrap' , wordBreak : 'break-word' } } >
672+ { ( ( ) => { try { return JSON . stringify ( JSON . parse ( tc . args ) , null , 2 ) } catch { return tc . args } } ) ( ) }
673+ </ pre >
674+ ) }
675+ { tc . result && (
676+ < pre style = { { margin : '4px 0' , fontSize : '0.75rem' , opacity : 0.7 , whiteSpace : 'pre-wrap' , wordBreak : 'break-word' , maxHeight : '200px' , overflow : 'auto' } } >
677+ { tc . result }
678+ </ pre >
679+ ) }
680+ </ details >
635681 ) ) }
636682 </ div >
637683 ) }
0 commit comments