From 43521b4d3a5dcce23cfd2d81ea19a23117879c5c Mon Sep 17 00:00:00 2001 From: karthikmudunuri <102793643+karthikmudunuri@users.noreply.github.com> Date: Wed, 8 Apr 2026 03:03:33 +0530 Subject: [PATCH] fix(ui): prevent WebSocket reconnection loop after PR #213 PR #213 stabilised fetchAgents by moving URL params into refs and running the effect only on mount. A side-effect was that navigating to a conversation URL (e.g. from a Discord link) could trigger an infinite WebSocket reconnection loop when the conversation was not immediately available in the list response. Root causes and fixes: 1. fetchAgents auto-created a second conversation when the URL-specified one was missing from the list. Now it tries a direct GET for the conversation first, and if that also fails it returns early instead of auto-creating a duplicate. 2. applyConversationUpdate silently dropped WebSocket events when selectedConversation was null (null && ... evaluates to null). Changed the guard from (prev && prev.name === conv.name) to (!prev || prev.name === conv.name) so the first event properly sets the selection. 3. After mount, URL param changes had no way to update the selected conversation from already-fetched data. Added a useEffect that watches [name, urlConversationId, agents] and syncs the selection. --- ui/src/pages/chat.tsx | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/chat.tsx b/ui/src/pages/chat.tsx index 05c35d5..29d9ac1 100644 --- a/ui/src/pages/chat.tsx +++ b/ui/src/pages/chat.tsx @@ -157,11 +157,35 @@ export function ChatPage() { } if (currentUrlConversationId) { - const match = group.conversations.find((conversation) => conversation.metadata.name === currentUrlConversationId); + let match = group.conversations.find((conversation) => conversation.metadata.name === currentUrlConversationId); + if (!match) { + // Conversation not in the list yet (e.g. Discord-spawned, not yet propagated). + // Try fetching it directly before falling through to auto-create. + try { + const directConv = await request( + `/acp/conversations/${encodeURIComponent(currentUrlConversationId)}`, + ); + if (directConv?.metadata?.name === currentUrlConversationId) { + match = directConv; + const updatedGroups = groups.map((g) => + g.spritz.metadata.name === currentName + ? { ...g, conversations: sortConversationsByRecency([...g.conversations, directConv]) } + : g, + ); + setAgents(updatedGroups); + } + } catch { + // Conversation genuinely doesn't exist yet — don't auto-create a + // second one. Let the provisioning poller pick it up. + } + } if (match) { setSelectedConversation(match); return false; } + // URL names a specific conversation that isn't available yet. + // Don't fall through to auto-create a second conversation. + return true; } const latestConversation = getLatestConversation(group.conversations); @@ -220,6 +244,18 @@ export function ChatPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Sync selected conversation when URL params change (e.g. navigating from Discord link + // or switching conversations). fetchAgents only runs on mount, so this effect handles + // subsequent URL changes by looking up the conversation in already-fetched agent data. + useEffect(() => { + if (!urlConversationId || agents.length === 0) return; + const group = agents.find((g) => g.spritz.metadata.name === name); + const match = group?.conversations.find((c) => c.metadata.name === urlConversationId); + if (match) { + setSelectedConversation(match); + } + }, [name, urlConversationId, agents]); + useEffect(() => { if (!provisioningSpritz) { return; @@ -272,7 +308,7 @@ export function ChatPage() { const applyConversationUpdate = useCallback((conversation: ConversationInfo) => { setSelectedConversation((prev) => - prev && prev.metadata.name === conversation.metadata.name ? conversation : prev, + !prev || prev.metadata.name === conversation.metadata.name ? conversation : prev, ); setAgents((prev) => prev.map((group) => {