@@ -4,7 +4,6 @@ import { useSearchParams } from "next/navigation";
44import {
55 useState ,
66 useEffect ,
7- useRef ,
87 createContext ,
98 PropsWithChildren ,
109 useContext ,
@@ -138,116 +137,80 @@ export function ChatProvider({ children }: PropsWithChildren) {
138137 const [ loading , setLoading ] = useState < boolean > ( false ) ;
139138 const [ serverStatus , setServerStatus ] = useState < ServerStatus > ( "unknown" ) ;
140139 const [ agentType , setAgentType ] = useState < AgentType > ( "custom" ) ;
141- const eventSourceRef = useRef < EventSource | null > ( null ) ;
142140 const agentAPIUrl = useAgentAPIUrl ( ) ;
143141
144- // Set up SSE connection to the events endpoint
142+ // Set up SSE connection to the events endpoint. EventSource handles
143+ // reconnection automatically, so we only create it once per URL and
144+ // let the browser manage transient failures. Messages are NOT cleared
145+ // on reconnect to avoid blanking the conversation on network blips.
145146 useEffect ( ( ) => {
146- // Function to create and set up EventSource
147- const setupEventSource = ( ) => {
148- if ( eventSourceRef . current ) {
149- eventSourceRef . current . close ( ) ;
150- }
147+ if ( ! agentAPIUrl ) {
148+ console . warn (
149+ "agentAPIUrl is not set, SSE connection cannot be established."
150+ ) ;
151+ setServerStatus ( "offline" ) ;
152+ return ;
153+ }
151154
152- // Reset messages when establishing a new connection
153- setMessages ( [ ] ) ;
155+ const eventSource = new EventSource ( `${ agentAPIUrl } /events` ) ;
154156
155- if ( ! agentAPIUrl ) {
156- console . warn (
157- "agentAPIUrl is not set, SSE connection cannot be established."
157+ // Handle message updates
158+ eventSource . addEventListener ( "message_update" , ( event ) => {
159+ const data : MessageUpdateEvent = JSON . parse ( event . data ) ;
160+ const confirmed : Message = {
161+ role : data . role ,
162+ content : data . message ,
163+ id : data . id ,
164+ } ;
165+
166+ setMessages ( ( prevMessages ) => {
167+ // Check if message with this ID already exists
168+ const existingIndex = prevMessages . findIndex (
169+ ( m ) => m . id === data . id
158170 ) ;
159- setServerStatus ( "offline" ) ; // Or some other appropriate status
160- return null ; // Don't try to connect if URL is empty
161- }
162171
163- const eventSource = new EventSource ( `${ agentAPIUrl } /events` ) ;
164- eventSourceRef . current = eventSource ;
165-
166- // Handle message updates
167- eventSource . addEventListener ( "message_update" , ( event ) => {
168- const data : MessageUpdateEvent = JSON . parse ( event . data ) ;
169-
170- setMessages ( ( prevMessages ) => {
171- // Clean up draft messages
172- const updatedMessages = [ ...prevMessages ] . filter (
173- ( m ) => ! isDraftMessage ( m )
174- ) ;
175-
176- // Check if message with this ID already exists
177- const existingIndex = updatedMessages . findIndex (
178- ( m ) => m . id === data . id
179- ) ;
180-
181- if ( existingIndex !== - 1 ) {
182- // Update existing message
183- updatedMessages [ existingIndex ] = {
184- role : data . role ,
185- content : data . message ,
186- id : data . id ,
187- } ;
188- return updatedMessages ;
189- } else {
190- // Add new message
191- return [
192- ...updatedMessages ,
193- {
194- role : data . role ,
195- content : data . message ,
196- id : data . id ,
197- } ,
198- ] ;
199- }
200- } ) ;
201- } ) ;
172+ if ( existingIndex !== - 1 ) {
173+ // Update in place without copying the whole array prefix/suffix.
174+ const updated = [ ...prevMessages ] ;
175+ updated [ existingIndex ] = confirmed ;
176+ return updated ;
177+ }
202178
203- // Handle status changes
204- eventSource . addEventListener ( "status_change" , ( event ) => {
205- const data : StatusChangeEvent = JSON . parse ( event . data ) ;
206- if ( data . status === "stable" ) {
207- setServerStatus ( "stable" ) ;
208- } else if ( data . status === "running" ) {
209- setServerStatus ( "running" ) ;
210- } else {
211- setServerStatus ( "unknown" ) ;
179+ // New confirmed message: replace any trailing draft that matches
180+ // the same role (the optimistic message we inserted on send).
181+ const last = prevMessages [ prevMessages . length - 1 ] ;
182+ if ( last && isDraftMessage ( last ) && last . role === confirmed . role ) {
183+ return [ ...prevMessages . slice ( 0 , - 1 ) , confirmed ] ;
212184 }
213185
214- // Set agent type
215- setAgentType ( data . agent_type === "" ? "unknown" : data . agent_type as AgentType ) ;
186+ return [ ...prevMessages , confirmed ] ;
216187 } ) ;
188+ } ) ;
189+
190+ // Handle status changes
191+ eventSource . addEventListener ( "status_change" , ( event ) => {
192+ const data : StatusChangeEvent = JSON . parse ( event . data ) ;
193+ if ( data . status === "stable" ) {
194+ setServerStatus ( "stable" ) ;
195+ } else if ( data . status === "running" ) {
196+ setServerStatus ( "running" ) ;
197+ } else {
198+ setServerStatus ( "unknown" ) ;
199+ }
200+ // Set agent type
201+ setAgentType ( data . agent_type === "" ? "unknown" : data . agent_type as AgentType ) ;
202+ } ) ;
217203
218- // Handle connection open (server is online)
219- eventSource . onopen = ( ) => {
220- // Connection is established, but we'll wait for status_change event
221- // for the actual server status
222- console . log ( "EventSource connection established - messages reset" ) ;
223- } ;
224-
225- // Handle connection errors
226- eventSource . onerror = ( error ) => {
227- console . error ( "EventSource error:" , error ) ;
228- setServerStatus ( "offline" ) ;
229-
230- // Try to reconnect after delay
231- setTimeout ( ( ) => {
232- if ( eventSourceRef . current ) {
233- setupEventSource ( ) ;
234- }
235- } , 3000 ) ;
236- } ;
237-
238- return eventSource ;
204+ eventSource . onopen = ( ) => {
205+ console . log ( "EventSource connection established" ) ;
239206 } ;
240207
241- // Initial setup
242- const eventSource = setupEventSource ( ) ;
243-
244- // Clean up on component unmount
245- return ( ) => {
246- if ( eventSource ) {
247- // Check if eventSource was successfully created
248- eventSource . close ( ) ;
249- }
208+ // Mark offline on error. The browser will retry automatically.
209+ eventSource . onerror = ( ) => {
210+ setServerStatus ( "offline" ) ;
250211 } ;
212+
213+ return ( ) => eventSource . close ( ) ;
251214 } , [ agentAPIUrl ] ) ;
252215
253216 // Send a new message
@@ -293,6 +256,8 @@ export function ChatProvider({ children }: PropsWithChildren) {
293256 toast . error ( `Failed to send message` , {
294257 description : fullDetail ,
295258 } ) ;
259+ // Remove the optimistic draft since the server rejected it.
260+ setMessages ( ( prev ) => prev . filter ( ( m ) => ! isDraftMessage ( m ) ) ) ;
296261 }
297262
298263 } catch ( error ) {
@@ -302,11 +267,10 @@ export function ChatProvider({ children }: PropsWithChildren) {
302267 toast . error ( `Error sending message` , {
303268 description : message ,
304269 } ) ;
270+ // Remove the optimistic draft since the request failed.
271+ setMessages ( ( prev ) => prev . filter ( ( m ) => ! isDraftMessage ( m ) ) ) ;
305272 } finally {
306273 if ( type === "user" ) {
307- setMessages ( ( prevMessages ) =>
308- prevMessages . filter ( ( m ) => ! isDraftMessage ( m ) )
309- ) ;
310274 setLoading ( false ) ;
311275 }
312276 }
0 commit comments