mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
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).
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<TypesGen.Chat | undefined>(
|
||||
chatKey(updatedChat.id),
|
||||
(previousChat) => {
|
||||
|
||||
Reference in New Issue
Block a user