shelley: refresh conversations list on reconnect

Philip Zeyliger and Shelley created

Prompt: When shelley comes back from a reconnection, it needs to refresh the conversations list, since it could have missed updates.

When the SSE stream reconnects after a disconnection, conversation list
updates may have been missed. Refresh the full conversations list on
reconnect to ensure the sidebar is up to date.

Uses a hasConnectedRef to distinguish initial connection from reconnects,
avoiding a redundant fetch on first load.

Co-authored-by: Shelley <shelley@exe.dev>

Change summary

ui/src/App.tsx                      | 10 ++++++++++
ui/src/components/ChatInterface.tsx | 11 ++++++++++-
2 files changed, 20 insertions(+), 1 deletion(-)

Detailed changes

ui/src/App.tsx 🔗

@@ -294,6 +294,15 @@ function App() {
     }
   };
 
+  const refreshConversations = async () => {
+    try {
+      const convs = await api.getConversations();
+      setConversations(convs);
+    } catch (err) {
+      console.error("Failed to refresh conversations:", err);
+    }
+  };
+
   const startNewConversation = () => {
     // Save the current conversation's cwd to localStorage so the new conversation picks it up
     if (currentConversation?.cwd) {
@@ -468,6 +477,7 @@ function App() {
             openDiffViewerTrigger={diffViewerTrigger}
             modelsRefreshTrigger={modelsRefreshTrigger}
             onOpenModelsModal={() => setModelsModalOpen(true)}
+            onReconnect={refreshConversations}
           />
         </div>
 

ui/src/components/ChatInterface.tsx 🔗

@@ -457,6 +457,7 @@ interface ChatInterfaceProps {
   openDiffViewerTrigger?: number; // increment to trigger opening diff viewer
   modelsRefreshTrigger?: number; // increment to trigger models list refresh
   onOpenModelsModal?: () => void;
+  onReconnect?: () => void;
 }
 
 function ChatInterface({
@@ -475,6 +476,7 @@ function ChatInterface({
   openDiffViewerTrigger,
   modelsRefreshTrigger,
   onOpenModelsModal,
+  onReconnect,
 }: ChatInterfaceProps) {
   const [messages, setMessages] = useState<Message[]>([]);
   const [loading, setLoading] = useState(true);
@@ -607,6 +609,7 @@ function ChatInterface({
   const periodicRetryRef = useRef<number | null>(null);
   const heartbeatTimeoutRef = useRef<number | null>(null);
   const lastSequenceIdRef = useRef<number>(-1);
+  const hasConnectedRef = useRef(false);
   const userScrolledRef = useRef(false);
 
   // Load messages and set up streaming
@@ -638,8 +641,9 @@ function ChatInterface({
       if (heartbeatTimeoutRef.current) {
         clearTimeout(heartbeatTimeoutRef.current);
       }
-      // Reset sequence ID when conversation changes
+      // Reset sequence ID and connection tracking when conversation changes
       lastSequenceIdRef.current = -1;
+      hasConnectedRef.current = false;
     };
   }, [conversationId]);
 
@@ -926,6 +930,11 @@ function ChatInterface({
 
     eventSource.onopen = () => {
       console.log("Message stream connected");
+      // Refresh conversations list on reconnect (may have missed updates while disconnected)
+      if (hasConnectedRef.current) {
+        onReconnect?.();
+      }
+      hasConnectedRef.current = true;
       // Reset reconnect attempts and clear periodic retry on successful connection
       setReconnectAttempts(0);
       setIsDisconnected(false);