From ff9d061ae90f7a4d3584ebc4a5e5dc03e76fdcf0 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 14 Mar 2026 10:27:54 -0700 Subject: [PATCH] fix(site): prevent duplicate chat in agents sidebar on creation (#23077) ## Problem When creating a new chat in the agents page (`/agents`), the chat could appear multiple times in the sidebar. This was a race condition triggered by the WebSocket `created` event handler. ## Root Cause `updateInfiniteChatsCache` applies its updater function **independently on each page** of the infinite query: ```ts const nextPages = prev.pages.map((page) => updater(page)); ``` When the `watchChats` WebSocket received a `"created"` event, the handler checked `exists` only within the *current page*, then prepended the new chat if not found: ```ts updateInfiniteChatsCache(queryClient, (chats) => { const exists = chats.some((c) => c.id === updatedChat.id); // ... if (chatEvent.kind === "created") { return [updatedChat, ...chats]; // runs per page! } }); ``` Since a brand-new chat doesn't exist in any page, **every loaded page** prepends it. After `pages.flat()`, the chat appears once per loaded page in the sidebar. ## Fix - Added `prependToInfiniteChatsCache` in `chats.ts` that checks across **all pages** before prepending, and only adds to page 0. - Split the WebSocket handler so `"created"` events use the new safe prepend, while update events (`title_change`, `status_change`) continue using `updateInfiniteChatsCache` (which is safe for `.map()` operations that don't add entries). --- site/src/api/queries/chats.ts | 29 ++++++++++++++++++++++++ site/src/pages/AgentsPage/AgentsPage.tsx | 21 +++++++++-------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 76e1c64010..6ced2b384f 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -30,6 +30,35 @@ export const updateInfiniteChatsCache = ( }); }; +/** + * Prepends a new chat to the first page of every infinite chats query + * in the cache, but only if the chat doesn't already exist in any + * page. This avoids the per-page duplication that would occur if + * a prepend updater were passed to updateInfiniteChatsCache, which + * runs independently on each page. + */ +export const prependToInfiniteChatsCache = ( + queryClient: QueryClient, + chat: TypesGen.Chat, +) => { + queryClient.setQueriesData<{ + pages: TypesGen.Chat[][]; + pageParams: unknown[]; + }>({ queryKey: chatsKey }, (prev) => { + if (!prev?.pages) return prev; + // Check across ALL pages to avoid duplicates. + const exists = prev.pages.some((page) => + page.some((c) => c.id === chat.id), + ); + if (exists) return prev; + // Only prepend to the first page. + const nextPages = prev.pages.map((page, i) => + i === 0 ? [chat, ...page] : page, + ); + return { ...prev, pages: nextPages }; + }); +}; + /** * Reads the flat list of chats from the first matching infinite query * in the cache. Returns undefined when no data is cached yet. diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index d96d31dc0e..eb28894fec 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -10,6 +10,7 @@ import { chatsKey, createChat, infiniteChats, + prependToInfiniteChatsCache, readInfiniteChatsCache, unarchiveChat, updateInfiniteChatsCache, @@ -405,9 +406,15 @@ const AgentsPage: FC = () => { const isTitleEvent = chatEvent.kind === "title_change"; const isStatusEvent = chatEvent.kind === "status_change"; - updateInfiniteChatsCache(queryClient, (chats) => { - const exists = chats.some((c) => c.id === updatedChat.id); - if (exists) { + // For "created" events, use a cross-page existence + // check and prepend only to the first page. + // updateInfiniteChatsCache runs the updater per + // page, so a naive prepend would duplicate the + // chat into every loaded page. + if (chatEvent.kind === "created") { + prependToInfiniteChatsCache(queryClient, updatedChat); + } else { + updateInfiniteChatsCache(queryClient, (chats) => { return chats.map((c) => { if (c.id !== updatedChat.id) return c; return { @@ -420,12 +427,8 @@ const AgentsPage: FC = () => { : updatedChat.updated_at, }; }); - } - if (chatEvent.kind === "created") { - return [updatedChat, ...chats]; - } - return chats; - }); + }); + } queryClient.setQueryData( chatKey(updatedChat.id), (previousChat) => {