Skip to content

Commit 4210bd4

Browse files
committed
fix(chat): stop clearing messages on SSE reconnect
Messages were reset to [] every time the EventSource reconnected, including on transient network errors. This blanked the entire conversation. Now we let the browser handle SSE reconnection natively and never clear the message list. Also removes the draft filter in sendMessage's finally block which caused the user's message to briefly disappear before the server confirmed it. Drafts are now replaced inline when the matching confirmed message arrives via SSE, and cleaned up on send failure.
1 parent 268e949 commit 4210bd4

File tree

1 file changed

+63
-99
lines changed

1 file changed

+63
-99
lines changed

chat/src/components/chat-provider.tsx

Lines changed: 63 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useSearchParams } from "next/navigation";
44
import {
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

Comments
 (0)