refactor(site): migrate chats query keys to collections/entities taxonomy (#27841)

This commit is contained in:
Danielle Maywood
2026-08-05 21:19:41 +01:00
committed by GitHub
parent 97c4031526
commit 0c88c2accc
19 changed files with 642 additions and 511 deletions
+28 -1
View File
@@ -1,8 +1,9 @@
import type { QueryClient } from "react-query";
import { type QueryClient, queryOptions } from "react-query";
import { API } from "#/api/api";
import { invalidateChatProviderDependentQueries } from "#/api/queries/chats";
import type {
AIProvider,
ChatProviderConfig,
CreateAIProviderRequest,
UpdateAIProviderRequest,
} from "#/api/typesGenerated";
@@ -17,6 +18,32 @@ export const aiProvidersList = () => ({
queryFn: (): Promise<AIProvider[]> => API.getAIProviders(),
});
const selectChatProviderConfigs = (
providers: readonly AIProvider[],
): ChatProviderConfig[] =>
providers.map((provider) => ({
id: provider.id,
provider: provider.type,
display_name: provider.display_name || provider.type,
icon: provider.icon,
enabled: provider.enabled,
has_api_key: provider.api_keys.length > 0,
central_api_key_enabled: true,
allow_user_api_key: true,
allow_central_api_key_fallback: true,
base_url: provider.base_url,
source: "database",
created_at: provider.created_at,
updated_at: provider.updated_at,
}));
export const chatProviderConfigs = () =>
queryOptions({
queryKey: aiProvidersListKey,
queryFn: (): Promise<AIProvider[]> => API.getAIProviders(),
select: selectChatProviderConfigs,
});
export const aiProvider = (idOrName: string) => ({
queryKey: aiProviderKeyFor(idOrName),
queryFn: (): Promise<AIProvider> => API.getAIProvider(idOrName),
-36
View File
@@ -1,36 +0,0 @@
import type { QueryClient } from "react-query";
import { API } from "#/api/api";
const chatDebugLoggingKey = ["chat-debug-logging"] as const;
const userChatDebugLoggingKey = ["user-chat-debug-logging"] as const;
export const chatDebugLogging = () => ({
queryKey: chatDebugLoggingKey,
queryFn: () => API.experimental.getChatDebugLogging(),
});
export const userChatDebugLogging = () => ({
queryKey: userChatDebugLoggingKey,
queryFn: () => API.experimental.getUserChatDebugLogging(),
});
export const updateChatDebugLogging = (queryClient: QueryClient) => ({
mutationFn: API.experimental.updateChatDebugLogging,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: chatDebugLoggingKey,
});
await queryClient.invalidateQueries({
queryKey: userChatDebugLoggingKey,
});
},
});
export const updateUserChatDebugLogging = (queryClient: QueryClient) => ({
mutationFn: API.experimental.updateUserChatDebugLogging,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: userChatDebugLoggingKey,
});
},
});
+216 -146
View File
@@ -10,25 +10,27 @@ import { buildOptimisticEditedMessage } from "./chatMessageEdits";
import {
addChildToParentInCache,
archiveChat,
type ChatListInput,
cancelChatListRefetches,
chatACL,
chatACLKey,
chatAdvisorConfig,
chatAdvisorConfigKey,
chatCost,
chatCostKey,
chatCostTreeKey,
chatDebugRunsKey,
chatDiffContentsKey,
chatKey,
chatEntityKey,
chatListFamilyKey,
chatListKey,
chatMessagesKey,
chatSearch,
chatsKey,
chatsByWorkspace,
createChat,
createChatMessage,
deleteChatQueuedMessage,
editChatMessage,
infiniteChats,
infiniteChatsKey,
interruptChat,
invalidateChatListQueries,
mergeWatchedChatIntoCaches,
@@ -42,6 +44,7 @@ import {
setChatGroupRole,
setChatUserRole,
TERMINAL_RUN_STATUSES,
toChatListParams,
unarchiveChat,
unpinChat,
updateChatAdvisorConfig,
@@ -58,6 +61,7 @@ vi.mock("#/api/api", () => ({
createChat: vi.fn(),
deleteChatQueuedMessage: vi.fn(),
getChats: vi.fn(),
getChatsByWorkspace: vi.fn(),
getChatCost: vi.fn(),
createChatMessage: vi.fn(),
editChatMessage: vi.fn(),
@@ -72,9 +76,9 @@ vi.mock("#/api/api", () => ({
},
}));
type InfiniteChatsTestOptions = Parameters<typeof infiniteChatsKey>[0];
type InfiniteChatsTestOptions = ChatListInput;
const infiniteChatsTestKey = infiniteChatsKey();
const infiniteChatsTestKey = chatListKey(toChatListParams());
type InfiniteData = {
pages: TypesGen.Chat[][];
@@ -87,7 +91,7 @@ const seedInfiniteChats = (
chats: TypesGen.Chat[],
opts?: InfiniteChatsTestOptions,
) => {
queryClient.setQueryData<InfiniteData>(infiniteChatsKey(opts), {
queryClient.setQueryData<InfiniteData>(chatListKey(toChatListParams(opts)), {
pages: [chats],
pageParams: [0],
});
@@ -98,7 +102,9 @@ const readInfiniteChats = (
queryClient: QueryClient,
opts?: InfiniteChatsTestOptions,
): TypesGen.Chat[] | undefined => {
const data = queryClient.getQueryData<InfiniteData>(infiniteChatsKey(opts));
const data = queryClient.getQueryData<InfiniteData>(
chatListKey(toChatListParams(opts)),
);
return data?.pages.flat();
};
@@ -192,34 +198,39 @@ describe("invalidateChatListQueries", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
// Sidebar queries.
queryClient.setQueryData(chatsKey, [makeChat(chatId)]);
queryClient.setQueryData(infiniteChatsKey({ archived: false }), {
queryClient.setQueryData(chatListKey(toChatListParams()), {
pages: [[makeChat(chatId)]],
pageParams: [0],
});
queryClient.setQueryData(
chatListKey(toChatListParams({ archived: true })),
{
pages: [[makeChat(chatId)]],
pageParams: [0],
},
);
// Per-chat queries that should NOT be touched.
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatMessagesKey(chatId), []);
queryClient.setQueryData(chatDiffContentsKey(chatId), {});
await invalidateChatListQueries(queryClient);
// Sidebar queries should be invalidated.
expect(
queryClient.getQueryState(chatsKey)?.isInvalidated,
"flat chats should be invalidated",
queryClient.getQueryState(chatListKey(toChatListParams()))?.isInvalidated,
"default chat list should be invalidated",
).toBe(true);
expect(
queryClient.getQueryState(infiniteChatsKey({ archived: false }))
?.isInvalidated,
"infinite chats should be invalidated",
queryClient.getQueryState(
chatListKey(toChatListParams({ archived: true })),
)?.isInvalidated,
"archived chat list should be invalidated",
).toBe(true);
// Per-chat queries should NOT be invalidated.
expect(
queryClient.getQueryState(chatKey(chatId))?.isInvalidated,
"chatKey should NOT be invalidated",
queryClient.getQueryState(chatEntityKey(chatId))?.isInvalidated,
"chatEntityKey should NOT be invalidated",
).not.toBe(true);
expect(
queryClient.getQueryState(chatMessagesKey(chatId))?.isInvalidated,
@@ -231,10 +242,10 @@ describe("invalidateChatListQueries", () => {
).not.toBe(true);
});
it("invalidates the infinite query with undefined opts", async () => {
it("invalidates the list query built from default params", async () => {
const queryClient = createTestQueryClient();
queryClient.setQueryData(infiniteChatsKey(), {
queryClient.setQueryData(chatListKey(toChatListParams()), {
pages: [[makeChat("chat-1")]],
pageParams: [0],
});
@@ -242,8 +253,8 @@ describe("invalidateChatListQueries", () => {
await invalidateChatListQueries(queryClient);
expect(
queryClient.getQueryState(infiniteChatsKey())?.isInvalidated,
"infinite chats with undefined opts should be invalidated",
queryClient.getQueryState(chatListKey(toChatListParams()))?.isInvalidated,
"default params chat list should be invalidated",
).toBe(true);
});
@@ -252,15 +263,17 @@ describe("invalidateChatListQueries", () => {
const chatId = "chat-1";
const otherChatId = "chat-2";
queryClient.setQueryData(chatsKey, [makeChat(chatId)]);
queryClient.setQueryData(chatKey(otherChatId), makeChat(otherChatId));
queryClient.setQueryData(chatListKey(toChatListParams()), [
makeChat(chatId),
]);
queryClient.setQueryData(chatEntityKey(otherChatId), makeChat(otherChatId));
queryClient.setQueryData(chatMessagesKey(otherChatId), []);
await invalidateChatListQueries(queryClient);
expect(
queryClient.getQueryState(chatKey(otherChatId))?.isInvalidated,
"other chat's chatKey should NOT be invalidated",
queryClient.getQueryState(chatEntityKey(otherChatId))?.isInvalidated,
"other chat's chatEntityKey should NOT be invalidated",
).not.toBe(true);
expect(
queryClient.getQueryState(chatMessagesKey(otherChatId))?.isInvalidated,
@@ -317,7 +330,7 @@ describe("updateChatTitle cache update", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { title: "Old" }),
);
seedInfiniteChats(queryClient, [
@@ -334,7 +347,7 @@ describe("updateChatTitle cache update", () => {
mutation.onSuccess(undefined, { chatId, title: "New" });
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.title,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.title,
).toBe("New");
expect(
readInfiniteChats(queryClient)?.find((chat) => chat.id === chatId),
@@ -361,10 +374,10 @@ describe("updateChatTitle cache update", () => {
expect(result).toBeUndefined();
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
invalidateSpy.mockRestore();
@@ -394,14 +407,16 @@ describe("archiveChat optimistic update", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId)]);
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
vi.mocked(API.experimental.updateChat).mockResolvedValue();
const mutation = archiveChat(queryClient);
await mutation.onMutate(chatId);
const cachedChat = queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId));
const cachedChat = queryClient.getQueryData<TypesGen.Chat>(
chatEntityKey(chatId),
);
expect(cachedChat?.archived).toBe(true);
});
@@ -440,7 +455,7 @@ describe("archiveChat optimistic update", () => {
{ archived: false },
);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { pin_order: 2 }),
);
@@ -453,33 +468,50 @@ describe("archiveChat optimistic update", () => {
),
).toEqual(["chat-2"]);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId)),
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId)),
).toMatchObject({
archived: true,
pin_order: 0,
});
});
it("clears pin order for archived chats that remain in unfiltered lists", () => {
it("clears pin order for archived chats that remain in archived lists", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { pin_order: 3 })]);
seedInfiniteChats(queryClient, [makeChat(chatId, { pin_order: 3 })], {
archived: true,
});
const mutation = archiveChat(queryClient);
mutation.onSuccess(undefined, chatId);
expect(readInfiniteChats(queryClient)?.[0]).toMatchObject({
expect(
readInfiniteChats(queryClient, { archived: true })?.[0],
).toMatchObject({
archived: true,
pin_order: 0,
});
});
it("removes newly archived chats from lists filtered to active chats", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { pin_order: 3 })], {
archived: false,
});
const mutation = archiveChat(queryClient);
mutation.onSuccess(undefined, chatId);
expect(readInfiniteChats(queryClient, { archived: false })).toEqual([]);
});
it("rolls back the chats list on error by invalidating", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
const initialChats = [makeChat(chatId)];
seedInfiniteChats(queryClient, initialChats);
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
const mutation = archiveChat(queryClient);
@@ -493,7 +525,7 @@ describe("archiveChat optimistic update", () => {
mutation.onError(new Error("server error"), chatId, context);
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
});
@@ -501,18 +533,20 @@ describe("archiveChat optimistic update", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId)]);
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
const mutation = archiveChat(queryClient);
const context = await mutation.onMutate(chatId);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.archived,
).toBe(true);
mutation.onError(new Error("server error"), chatId, context);
const rolledBack = queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId));
const rolledBack = queryClient.getQueryData<TypesGen.Chat>(
chatEntityKey(chatId),
);
expect(rolledBack?.archived).toBe(false);
});
@@ -531,7 +565,7 @@ describe("archiveChat optimistic update", () => {
// The handler should still invalidate to trigger a refetch.
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
});
@@ -539,7 +573,6 @@ describe("archiveChat optimistic update", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId)]);
// Deliberately do NOT set chatKey(chatId) data.
const mutation = archiveChat(queryClient);
const context = await mutation.onMutate(chatId);
@@ -565,10 +598,10 @@ describe("archiveChat optimistic update", () => {
expect(result).toBeUndefined();
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
invalidateSpy.mockRestore();
@@ -592,7 +625,7 @@ describe("unarchiveChat optimistic update", () => {
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { archived: true })]);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { archived: true }),
);
@@ -600,7 +633,7 @@ describe("unarchiveChat optimistic update", () => {
await mutation.onMutate(chatId);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.archived,
).toBe(false);
});
@@ -616,7 +649,7 @@ describe("unarchiveChat optimistic update", () => {
{ archived: true },
);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { archived: true }),
);
@@ -629,7 +662,7 @@ describe("unarchiveChat optimistic update", () => {
),
).toEqual(["chat-2"]);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId)),
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId)),
).toMatchObject({
archived: false,
});
@@ -640,7 +673,7 @@ describe("unarchiveChat optimistic update", () => {
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { archived: true })]);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { archived: true }),
);
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
@@ -651,7 +684,7 @@ describe("unarchiveChat optimistic update", () => {
// Verify optimistic update.
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(false);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.archived,
).toBe(false);
// Roll back.
@@ -659,11 +692,11 @@ describe("unarchiveChat optimistic update", () => {
// The chats list is rolled back via invalidation.
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
// The individual chat cache is restored directly.
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.archived,
).toBe(true);
});
@@ -682,10 +715,10 @@ describe("unarchiveChat optimistic update", () => {
expect(result).toBeUndefined();
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
invalidateSpy.mockRestore();
@@ -701,11 +734,14 @@ describe("pinChat optimistic update", () => {
makeChat(chatId),
makeChat("chat-pinned-2", { pin_order: 2 }),
]);
queryClient.setQueryData(infiniteChatsKey({ archived: true }), {
pages: [[makeChat("chat-pinned-archived", { pin_order: 4 })]],
pageParams: [0],
});
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(
chatListKey(toChatListParams({ archived: true })),
{
pages: [[makeChat("chat-pinned-archived", { pin_order: 4 })]],
pageParams: [0],
},
);
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
const mutation = pinChat(queryClient);
await mutation.onMutate(chatId);
@@ -715,7 +751,7 @@ describe("pinChat optimistic update", () => {
?.pin_order,
).toBe(5);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.pin_order,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.pin_order,
).toBe(5);
});
});
@@ -737,7 +773,7 @@ describe("unpinChat optimistic update", () => {
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { pin_order: 2 })]);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { pin_order: 2 }),
);
@@ -745,7 +781,7 @@ describe("unpinChat optimistic update", () => {
await mutation.onMutate(chatId);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.pin_order,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.pin_order,
).toBe(0);
});
@@ -754,7 +790,7 @@ describe("unpinChat optimistic update", () => {
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { pin_order: 3 })]);
queryClient.setQueryData(
chatKey(chatId),
chatEntityKey(chatId),
makeChat(chatId, { pin_order: 3 }),
);
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
@@ -765,7 +801,7 @@ describe("unpinChat optimistic update", () => {
// Verify optimistic update.
expect(readInfiniteChats(queryClient)?.[0].pin_order).toBe(0);
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.pin_order,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.pin_order,
).toBe(0);
// Roll back.
@@ -773,11 +809,11 @@ describe("unpinChat optimistic update", () => {
// The chats list is rolled back via invalidation.
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
// The individual chat cache is restored directly.
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.pin_order,
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId))?.pin_order,
).toBe(3);
});
@@ -790,10 +826,10 @@ describe("unpinChat optimistic update", () => {
await mutation.onSettled(undefined, undefined, chatId);
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
});
@@ -813,20 +849,20 @@ describe("reorderPinnedChat", () => {
await mutation.onSettled?.(undefined, undefined, { chatId, pinOrder: 2 });
expect(cancelSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(cancelSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
expect(API.experimental.updateChat).toHaveBeenCalledWith(chatId, {
pin_order: 2,
});
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
expect.objectContaining({ queryKey: chatListFamilyKey }),
);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
});
@@ -841,8 +877,20 @@ describe("chat cost query factories", () => {
const query = chatCost(chatId);
expect(chatCostKey(chatId)).toEqual(["chats", chatId, "cost"]);
expect(query.queryKey).toEqual(["chats", chatId, "cost"]);
expect(chatCostTreeKey(chatId)).toEqual([
"chats",
"analytics",
"cost",
"tree",
chatId,
]);
expect(query.queryKey).toEqual([
"chats",
"analytics",
"cost",
"tree",
chatId,
]);
await query.queryFn();
expect(API.experimental.getChatCost).toHaveBeenCalledWith(chatId);
});
@@ -860,20 +908,13 @@ describe("mutation invalidation scope", () => {
/** Populate the QueryClient with every query key that is actively
* observed on the /agents/:id detail page. */
const seedAllActiveQueries = (queryClient: QueryClient, chatId: string) => {
// Infinite sidebar list: ["chats", { archived: false }]
queryClient.setQueryData(infiniteChatsKey({ archived: false }), {
queryClient.setQueryData(chatListKey(toChatListParams()), {
pages: [[makeChat(chatId)]],
pageParams: [0],
});
// Flat chats list: ["chats"]
queryClient.setQueryData(chatsKey, [makeChat(chatId)]);
// Individual chat: ["chats", chatId]
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
// Messages: ["chats", chatId, "messages"]
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatMessagesKey(chatId), []);
// Debug runs: ["chats", chatId, "debug-runs"]
queryClient.setQueryData(chatDebugRunsKey(chatId), []);
// Diff contents: ["chats", chatId, "diff-contents"]
queryClient.setQueryData(chatDiffContentsKey(chatId), { files: [] });
};
@@ -913,10 +954,11 @@ describe("mutation invalidation scope", () => {
"chatDebugRunsKey should be invalidated",
).toBe(true);
const chatState = queryClient.getQueryState(chatKey(chatId));
expect(chatState?.isInvalidated, "chatKey should be invalidated").toBe(
true,
);
const chatState = queryClient.getQueryState(chatEntityKey(chatId));
expect(
chatState?.isInvalidated,
"chatEntityKey should be invalidated",
).toBe(true);
const messagesState = queryClient.getQueryState(chatMessagesKey(chatId));
expect(
@@ -957,10 +999,11 @@ describe("mutation invalidation scope", () => {
// Chat metadata and debug runs should be invalidated because
// editing changes the chat's updated_at and can start a new
// debug run.
const chatState = queryClient.getQueryState(chatKey(chatId));
expect(chatState?.isInvalidated, "chatKey should be invalidated").toBe(
true,
);
const chatState = queryClient.getQueryState(chatEntityKey(chatId));
expect(
chatState?.isInvalidated,
"chatEntityKey should be invalidated",
).toBe(true);
// Messages are NOT invalidated. The per-chat WebSocket handles
// post-edit message delivery, making REST invalidation
@@ -1408,12 +1451,11 @@ describe("mutation invalidation scope", () => {
).toBe(true);
for (const { label, key } of [
{ label: "flat chats", key: chatsKey },
{
label: "infinite chats",
key: infiniteChatsKey({ archived: false }),
label: "chat list",
key: chatListKey(toChatListParams()),
},
{ label: "chat detail", key: chatKey(chatId) },
{ label: "chat detail", key: chatEntityKey(chatId) },
{ label: "messages", key: chatMessagesKey(chatId) },
...unrelatedKeys(chatId),
]) {
@@ -1438,13 +1480,8 @@ describe("mutation invalidation scope", () => {
// Sidebar lists SHOULD be invalidated.
expect(
queryClient.getQueryState(chatsKey)?.isInvalidated,
"flat chats should be invalidated",
).toBe(true);
expect(
queryClient.getQueryState(infiniteChatsKey({ archived: false }))
?.isInvalidated,
"infinite chats should be invalidated",
queryClient.getQueryState(chatListKey(toChatListParams()))?.isInvalidated,
"chat list should be invalidated",
).toBe(true);
// Per-chat queries should NOT be touched.
@@ -1455,8 +1492,8 @@ describe("mutation invalidation scope", () => {
).not.toBe(true);
}
expect(
queryClient.getQueryState(chatKey(chatId))?.isInvalidated,
"chatKey should NOT be invalidated",
queryClient.getQueryState(chatEntityKey(chatId))?.isInvalidated,
"chatEntityKey should NOT be invalidated",
).not.toBe(true);
expect(
queryClient.getQueryState(chatMessagesKey(chatId))?.isInvalidated,
@@ -1474,8 +1511,8 @@ describe("mutation invalidation scope", () => {
// These two should be invalidated (exact match).
expect(
queryClient.getQueryState(chatKey(chatId))?.isInvalidated,
"chatKey should be invalidated",
queryClient.getQueryState(chatEntityKey(chatId))?.isInvalidated,
"chatEntityKey should be invalidated",
).toBe(true);
expect(
queryClient.getQueryState(chatMessagesKey(chatId))?.isInvalidated,
@@ -1492,21 +1529,46 @@ describe("mutation invalidation scope", () => {
// Sidebar list should NOT be touched.
expect(
queryClient.getQueryState(chatsKey)?.isInvalidated,
"flat chats should NOT be invalidated",
queryClient.getQueryState(chatListKey(toChatListParams()))?.isInvalidated,
"chat list should NOT be invalidated",
).not.toBe(true);
});
});
describe("infiniteChatsKey shape", () => {
it("places the filter object one slot after the chatsKey prefix", () => {
// archivedFilterForChatListKey reads the archived filter from the
// slot immediately after the chatsKey prefix. If this layout ever
// changes, that helper silently stops removing chats from
// conflicting filtered lists, so keep the two in sync.
const key = infiniteChatsKey({ archived: true });
expect(key.length).toBe(chatsKey.length + 1);
expect(key[chatsKey.length]).toEqual({ archived: true });
describe("chatListKey shape", () => {
it("places the params object one slot after the list family prefix", () => {
const key = chatListKey(toChatListParams({ archived: true }));
expect(key.length).toBe(chatListFamilyKey.length + 1);
expect(key[chatListFamilyKey.length]).toEqual({
archived: true,
prStatuses: [],
status: "all",
sources: [],
});
});
});
describe("chatsByWorkspace", () => {
it("disables the query when no workspace IDs are given", () => {
expect(chatsByWorkspace([]).enabled).toBe(false);
expect(chatsByWorkspace(["ws-1"]).enabled).toBe(true);
});
it("canonicalizes the key with sorted, deduplicated workspace IDs", () => {
const options = chatsByWorkspace(["ws-b", "ws-a", "ws-b"]);
expect(options.queryKey).toEqual([
"chats",
"collections",
"by-workspace",
["ws-a", "ws-b"],
]);
});
it("fetches with the sorted, deduplicated workspace IDs", async () => {
const getChatsByWorkspace = vi.mocked(API.experimental.getChatsByWorkspace);
getChatsByWorkspace.mockResolvedValue({});
await chatsByWorkspace(["ws-b", "ws-a", "ws-b"]).queryFn();
expect(getChatsByWorkspace).toHaveBeenCalledWith(["ws-a", "ws-b"]);
});
});
@@ -1540,6 +1602,7 @@ describe("infiniteChats", () => {
expect(API.experimental.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: 0,
q: "archived:false",
});
});
@@ -1550,6 +1613,7 @@ describe("infiniteChats", () => {
expect(API.experimental.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: 0,
q: "archived:false",
});
});
@@ -1561,12 +1625,14 @@ describe("infiniteChats", () => {
expect(API.experimental.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: PAGE_LIMIT,
q: "archived:false",
});
await queryFn({ pageParam: 3 });
expect(API.experimental.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: PAGE_LIMIT * 2,
q: "archived:false",
});
});
@@ -1616,10 +1682,15 @@ describe("infiniteChats", () => {
describe("chatSearch", () => {
it("requests chats with q and a fixed limit", async () => {
vi.mocked(API.experimental.getChats).mockResolvedValue([]);
const query = chatSearch("title:fix");
const query = chatSearch({ q: "title:fix" });
const queryClient = createTestQueryClient();
expect(query.queryKey).toEqual(["chats", "search", { q: "title:fix" }]);
expect(query.queryKey).toEqual([
"chats",
"collections",
"search",
{ q: "title:fix" },
]);
await queryClient.fetchQuery(query);
expect(API.experimental.getChats).toHaveBeenCalledWith({
limit: 50,
@@ -1634,62 +1705,61 @@ describe("diff_status_change invalidation scope", () => {
// invalidate only the individual chat detail and diff-contents
// queries, NOT the chat list (sidebar) or messages.
it("exact chatKey invalidation does not cascade to messages or diff-contents", async () => {
it("exact chatEntityKey invalidation does not cascade to messages or diff-contents", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
// Seed all the queries that are active on the /agents/:id page.
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatMessagesKey(chatId), []);
queryClient.setQueryData(chatDiffContentsKey(chatId), { files: [] });
queryClient.setQueryData(chatsKey, [makeChat(chatId)]);
queryClient.setQueryData(chatListKey(toChatListParams()), [
makeChat(chatId),
]);
// This is what the fixed handler does, exact: true.
await queryClient.invalidateQueries({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
// chatKey itself should be invalidated.
expect(
queryClient.getQueryState(chatKey(chatId))?.isInvalidated,
"chatKey should be invalidated",
queryClient.getQueryState(chatEntityKey(chatId))?.isInvalidated,
"chatEntityKey should be invalidated",
).toBe(true);
// Messages should NOT be invalidated.
expect(
queryClient.getQueryState(chatMessagesKey(chatId))?.isInvalidated,
"chatMessagesKey should NOT be invalidated by exact chatKey",
"chatMessagesKey should NOT be invalidated by exact chatEntityKey",
).not.toBe(true);
// Diff-contents should NOT be invalidated.
expect(
queryClient.getQueryState(chatDiffContentsKey(chatId))?.isInvalidated,
"chatDiffContentsKey should NOT be invalidated by exact chatKey",
"chatDiffContentsKey should NOT be invalidated by exact chatEntityKey",
).not.toBe(true);
// Chat list should NOT be invalidated.
expect(
queryClient.getQueryState(chatsKey)?.isInvalidated,
"chatsKey should NOT be invalidated by exact chatKey",
queryClient.getQueryState(chatListKey(toChatListParams()))?.isInvalidated,
"chatListKey should NOT be invalidated by exact chatEntityKey",
).not.toBe(true);
});
it("without exact: true, chatKey invalidation cascades to messages and diff-contents (the old bug)", async () => {
it("without exact: true, chatEntityKey invalidation cascades to messages and diff-contents (the old bug)", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatEntityKey(chatId), makeChat(chatId));
queryClient.setQueryData(chatMessagesKey(chatId), []);
queryClient.setQueryData(chatDiffContentsKey(chatId), { files: [] });
// This is what the OLD (broken) handler did, no exact: true.
await queryClient.invalidateQueries({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
});
// Without exact: true, ALL queries starting with ["chats", chatId]
// get invalidated, including messages and diff-contents.
expect(
queryClient.getQueryState(chatMessagesKey(chatId))?.isInvalidated,
"chatMessagesKey IS invalidated without exact: true (old bug)",
@@ -2553,7 +2623,7 @@ describe("mergeWatchedChatIntoCaches", () => {
});
seedInfiniteChats(queryClient, [cachedChat]);
queryClient.setQueryData(chatKey(chatId), cachedChat);
queryClient.setQueryData(chatEntityKey(chatId), cachedChat);
mergeWatchedChatIntoCaches(queryClient, watchedChat, {
eventKind: "status_change",
@@ -2565,7 +2635,7 @@ describe("mergeWatchedChatIntoCaches", () => {
updated_at: "2025-01-01T00:05:00.000Z",
});
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId)),
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId)),
).toMatchObject({
status: "running",
last_model_config_id: "model-new",
@@ -2593,7 +2663,7 @@ describe("mergeWatchedChatIntoCaches", () => {
});
seedInfiniteChats(queryClient, [parent]);
queryClient.setQueryData(chatKey(childId), cachedChild);
queryClient.setQueryData(chatEntityKey(childId), cachedChild);
mergeWatchedChatIntoCaches(queryClient, watchedChild, {
eventKind: "status_change",
@@ -2605,7 +2675,7 @@ describe("mergeWatchedChatIntoCaches", () => {
updated_at: "2025-01-01T00:05:00.000Z",
});
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(childId)),
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(childId)),
).toMatchObject({
status: "running",
last_model_config_id: "model-new",
@@ -2634,7 +2704,7 @@ describe("mergeWatchedChatIntoCaches", () => {
});
seedInfiniteChats(queryClient, [cachedChat]);
queryClient.setQueryData(chatKey(chatId), cachedChat);
queryClient.setQueryData(chatEntityKey(chatId), cachedChat);
mergeWatchedChatIntoCaches(queryClient, staleWatchChat, {
eventKind: "status_change",
@@ -2649,7 +2719,7 @@ describe("mergeWatchedChatIntoCaches", () => {
updated_at: "2025-01-01T00:05:00.000Z",
});
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId)),
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId)),
).toMatchObject({
status: "waiting",
title: "Fresh title",
@@ -2745,7 +2815,7 @@ describe("chat ACL query factories", () => {
const query = chatACL(chatId);
expect(chatACLKey(chatId)).toEqual(["chats", chatId, "acl"]);
expect(chatACLKey(chatId)).toEqual(["chats", "entities", chatId, "acl"]);
expect(query.queryKey).toEqual(chatACLKey(chatId));
await expect(query.queryFn()).resolves.toEqual(acl);
expect(API.experimental.getChatACL).toHaveBeenCalledWith(chatId);
File diff suppressed because it is too large Load Diff
@@ -1,19 +1,15 @@
import type { FC } from "react";
import {
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from "react-query";
import { API } from "#/api/api";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatAdvisorConfig,
chatComputerUseProvider,
chatModelConfigs,
chatModelOverride,
chatPersonalModelOverridesAdminSettings,
chatProviderConfigs,
updateChatAdvisorConfig,
updateChatComputerUseProvider,
updateChatModelOverride,
updateChatPersonalModelOverridesAdminSettings,
} from "#/api/queries/chats";
import type * as TypesGen from "#/api/typesGenerated";
@@ -31,30 +27,6 @@ const titleGenerationOverrideContext: TypesGen.ChatModelOverrideContext =
const compactionOverrideContext: TypesGen.ChatModelOverrideContext =
"compaction";
const chatModelOverrideKey = (context: TypesGen.ChatModelOverrideContext) =>
["chat-model-override", context] as const;
const chatModelOverrideQuery = (
context: TypesGen.ChatModelOverrideContext,
) => ({
queryKey: chatModelOverrideKey(context),
queryFn: () => API.experimental.getChatModelOverride(context),
});
const updateChatModelOverrideMutation = (
queryClient: QueryClient,
context: TypesGen.ChatModelOverrideContext,
) => ({
mutationFn: (req: TypesGen.UpdateChatModelOverrideRequest) =>
API.experimental.updateChatModelOverride(context, req),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: chatModelOverrideKey(context),
exact: true,
});
},
});
const CoderAgentsPage: FC = () => {
const { permissions } = useAuthenticated();
const { experiments } = useDashboard();
@@ -70,19 +42,19 @@ const CoderAgentsPage: FC = () => {
enabled: canEditDeploymentConfig,
});
const generalModelOverrideQuery = useQuery({
...chatModelOverrideQuery(generalOverrideContext),
...chatModelOverride(generalOverrideContext),
enabled: canEditDeploymentConfig,
});
const exploreModelOverrideQuery = useQuery({
...chatModelOverrideQuery(exploreOverrideContext),
...chatModelOverride(exploreOverrideContext),
enabled: canEditDeploymentConfig,
});
const titleGenerationModelQuery = useQuery({
...chatModelOverrideQuery(titleGenerationOverrideContext),
...chatModelOverride(titleGenerationOverrideContext),
enabled: canEditDeploymentConfig,
});
const compactionModelQuery = useQuery({
...chatModelOverrideQuery(compactionOverrideContext),
...chatModelOverride(compactionOverrideContext),
enabled: canEditDeploymentConfig,
});
const modelConfigsQuery = useQuery(chatModelConfigs());
@@ -102,19 +74,16 @@ const CoderAgentsPage: FC = () => {
updateChatPersonalModelOverridesAdminSettings(queryClient),
);
const saveGeneralModelOverrideMutation = useMutation(
updateChatModelOverrideMutation(queryClient, generalOverrideContext),
updateChatModelOverride(queryClient, generalOverrideContext),
);
const saveTitleGenerationModelMutation = useMutation(
updateChatModelOverrideMutation(
queryClient,
titleGenerationOverrideContext,
),
updateChatModelOverride(queryClient, titleGenerationOverrideContext),
);
const saveCompactionModelMutation = useMutation(
updateChatModelOverrideMutation(queryClient, compactionOverrideContext),
updateChatModelOverride(queryClient, compactionOverrideContext),
);
const saveExploreModelOverrideMutation = useMutation(
updateChatModelOverrideMutation(queryClient, exploreOverrideContext),
updateChatModelOverride(queryClient, exploreOverrideContext),
);
const saveAdvisorConfigMutation = useMutation(
updateChatAdvisorConfig(queryClient),
@@ -3,10 +3,10 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { useNavigate, useSearchParams } from "react-router";
import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModels,
chatProviderConfigs,
createChatModelConfig,
} from "#/api/queries/chats";
import { useAuthenticated } from "#/hooks/useAuthenticated";
@@ -1,10 +1,7 @@
import type { FC } from "react";
import { useQuery } from "react-query";
import {
chatModelConfigs,
chatModels,
chatProviderConfigs,
} from "#/api/queries/chats";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import { chatModelConfigs, chatModels } from "#/api/queries/chats";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { deriveProviderStates } from "#/modules/aiModels/providerStates";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
@@ -3,10 +3,10 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { Navigate, useNavigate, useParams } from "react-router";
import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModels,
chatProviderConfigs,
deleteChatModelConfig,
updateChatModelConfig,
} from "#/api/queries/chats";
@@ -12,13 +12,14 @@ import { API } from "#/api/api";
import { getAuthorizationKey } from "#/api/queries/authCheck";
import {
chatDiffContentsKey,
chatKey,
chatEntityKey,
chatListKey,
chatMessagesKey,
chatModelConfigs,
chatModelsKey,
chatPromptsKey,
chatsKey,
mcpServerConfigsKey,
mcpServersKey,
toChatListParams,
} from "#/api/queries/chats";
import { workspaceByIdKey } from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
@@ -273,7 +274,7 @@ const buildQueries = (
diff_status: diffStatus,
};
return [
{ key: chatKey(CHAT_ID), data: chatWithDiffStatus },
{ key: chatEntityKey(CHAT_ID), data: chatWithDiffStatus },
{
key: chatMessagesKey(CHAT_ID),
data: { pages: [messagesData], pageParams: [undefined] },
@@ -284,7 +285,10 @@ const buildQueries = (
prompts: extractPromptsFromMessages(messagesData.messages),
} satisfies TypesGen.ChatPromptsResponse,
},
{ key: chatsKey, data: [chatWithDiffStatus] },
{
key: chatListKey(toChatListParams()),
data: { pages: [[chatWithDiffStatus]], pageParams: [0] },
},
{
key: chatDiffContentsKey(CHAT_ID),
data: {
@@ -299,7 +303,7 @@ const buildQueries = (
},
{ key: chatModelsKey, data: mockModelCatalog },
{ key: chatModelConfigs().queryKey, data: mockModelConfigs },
{ key: mcpServerConfigsKey, data: [] },
{ key: mcpServersKey, data: [] },
buildChatAuthorizationQuery(chat, {
canShareChat: {
action: "share",
@@ -2986,7 +2990,7 @@ export const SendResponseAfterChatSwitch: Story = {
{ messages: [], queued_messages: [], has_more: false },
{ diffUrl: undefined },
),
{ key: chatKey(SWITCHED_CHAT_ID), data: switchedChat },
{ key: chatEntityKey(SWITCHED_CHAT_ID), data: switchedChat },
{
key: chatMessagesKey(SWITCHED_CHAT_ID),
data: {
@@ -3087,7 +3091,7 @@ export const DetailQueryError: Story = {
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
chatEntityKey(CHAT_ID),
),
},
beforeEach: () => {
@@ -3134,7 +3138,7 @@ export const ErrorRetryRecovers: Story = {
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
chatEntityKey(CHAT_ID),
),
},
beforeEach: ({ parameters }) => {
@@ -3164,7 +3168,7 @@ export const ChatNotFound: Story = {
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
chatEntityKey(CHAT_ID),
),
},
beforeEach: () => {
+8 -6
View File
@@ -23,15 +23,15 @@ import {
watchWorkspace,
} from "#/api/api";
import { getErrorMessage, getErrorStatus, isApiError } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import { checkAuthorization } from "#/api/queries/authCheck";
import { buildOptimisticEditedMessage } from "#/api/queries/chatMessageEdits";
import {
chat,
chatKey,
chatEntityKey,
chatMessagesForInfiniteScroll,
chatModelConfigs,
chatModels,
chatProviderConfigs,
chatQueueConvergence,
compactChat,
createChatMessage,
@@ -1182,8 +1182,10 @@ const AgentChatPage: FC = () => {
chat.id === chatId ? { ...chat, plan_mode: planMode } : chat,
),
);
queryClient.setQueryData<TypesGen.Chat>(chatKey(chatId), (previousChat) =>
previousChat ? { ...previousChat, plan_mode: planMode } : previousChat,
queryClient.setQueryData<TypesGen.Chat>(
chatEntityKey(chatId),
(previousChat) =>
previousChat ? { ...previousChat, plan_mode: planMode } : previousChat,
);
};
@@ -1693,7 +1695,7 @@ const AgentChatPage: FC = () => {
// Hook dispatch failures can park an idle chat in error before returning the request error.
acceptServerChatStatus();
void queryClient.invalidateQueries({
queryKey: chatKey(agentId),
queryKey: chatEntityKey(agentId),
exact: true,
});
},
@@ -1744,7 +1746,7 @@ const AgentChatPage: FC = () => {
// Hook dispatch failures can park an idle chat in error before returning the request error.
acceptServerChatStatus();
void queryClient.invalidateQueries({
queryKey: chatKey(agentId),
queryKey: chatEntityKey(agentId),
exact: true,
});
throw error;
@@ -3,10 +3,10 @@ import { useMutation, useQuery, useQueryClient } from "react-query";
import { useLocation, useNavigate } from "react-router";
import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import { chatProviderConfigs } from "#/api/queries/aiProviders";
import {
chatModelConfigs,
chatModels,
chatProviderConfigs,
createChat,
mcpServerConfigs,
userChatPersonalModelOverrides,
+12 -12
View File
@@ -20,12 +20,12 @@ import {
applyChatArchiveStateToCaches,
archiveChat,
cancelChatListRefetches,
chatCostKey,
chatCostTreeKey,
chatDiffContentsKey,
chatKey,
chatEntityKey,
chatModelConfigs,
chatModels,
chatsByWorkspaceKeyPrefix,
chatsByWorkspaceFamilyKey,
infiniteChats,
invalidateChatListQueries,
mergeWatchedChatIntoCaches,
@@ -304,11 +304,11 @@ const AgentsPageLayout: FC = () => {
clearPersistedRightPanelState(chatId);
void invalidateChatListQueries(queryClient);
void queryClient.invalidateQueries({
queryKey: chatKey(chatId),
queryKey: chatEntityKey(chatId),
exact: true,
});
void queryClient.invalidateQueries({
queryKey: chatsByWorkspaceKeyPrefix,
queryKey: chatsByWorkspaceFamilyKey,
});
void invalidateWorkspaceMutationQueries(queryClient, {
organizationName,
@@ -410,7 +410,7 @@ const AgentsPageLayout: FC = () => {
return;
}
const chat =
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId)) ??
queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(chatId)) ??
chatList.find((candidate) => candidate.id === chatId);
if (chat === undefined || isActiveChat(chat)) {
setPendingArchiveChatId(chatId);
@@ -445,7 +445,7 @@ const AgentsPageLayout: FC = () => {
// callback time so it reflects the user's current
// location.
activeChatId
? queryClient.getQueryData<TypesGen.Chat>(chatKey(activeChatId))
? queryClient.getQueryData<TypesGen.Chat>(chatEntityKey(activeChatId))
?.root_chat_id
: undefined,
)
@@ -617,7 +617,7 @@ const AgentsPageLayout: FC = () => {
);
removeChildFromParentInCache(queryClient, updatedChat.id);
queryClient.removeQueries({
queryKey: chatKey(updatedChat.id),
queryKey: chatEntityKey(updatedChat.id),
exact: true,
});
return;
@@ -648,9 +648,9 @@ const AgentsPageLayout: FC = () => {
// reverts the query to pending/idle with no data
// and no retry, which AgentChatPage shows as
// "Chat not found".
if (queryClient.getQueryData(chatKey(updatedChat.id))) {
if (queryClient.getQueryData(chatEntityKey(updatedChat.id))) {
void queryClient.cancelQueries({
queryKey: chatKey(updatedChat.id),
queryKey: chatEntityKey(updatedChat.id),
exact: true,
});
}
@@ -683,7 +683,7 @@ const AgentsPageLayout: FC = () => {
);
if (costChatId) {
void queryClient.invalidateQueries({
queryKey: chatCostKey(costChatId),
queryKey: chatCostTreeKey(costChatId),
exact: true,
});
}
@@ -695,7 +695,7 @@ const AgentsPageLayout: FC = () => {
// active chat has an observer, so other chats are
// merely marked stale.
void queryClient.invalidateQueries({
queryKey: chatKey(updatedChat.id),
queryKey: chatEntityKey(updatedChat.id),
exact: true,
});
}
@@ -1,10 +1,12 @@
import { act, render, renderHook, waitFor } from "@testing-library/react";
import { watchChat } from "#/api/api";
import { chatMessagesKey, chatsKey } from "#/api/queries/chats";
import {
chatListKey,
chatMessagesKey,
toChatListParams,
} from "#/api/queries/chats";
// The infinite query key used by useInfiniteQuery(infiniteChats())
// is [...chatsKey, undefined] = ["chats", undefined].
const infiniteChatsTestKey = [...chatsKey, undefined];
const infiniteChatsTestKey = chatListKey(toChatListParams());
type InfiniteData = {
pages: TypesGen.Chat[][];
@@ -3,6 +3,7 @@ import type { FC, PropsWithChildren } from "react";
import { act } from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { chatEntityKey } from "#/api/queries/chats";
import { getWorkspaceQuotaQueryKey } from "#/api/queries/workspaceQuota";
import { workspacesQueryKeyPrefix } from "#/api/queries/workspaces";
import { createChatStore } from "./chatStore";
@@ -106,7 +107,7 @@ describe("useChatToolInvalidations", () => {
await waitFor(() => {
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: ["chats", "chat-1"],
queryKey: chatEntityKey("chat-1"),
});
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -144,7 +145,7 @@ describe("useChatToolInvalidations", () => {
});
expect(invalidateSpy).not.toHaveBeenCalledWith({
queryKey: ["chats", "chat-1"],
queryKey: chatEntityKey("chat-1"),
});
});
@@ -183,7 +184,7 @@ describe("useChatToolInvalidations", () => {
await waitFor(() => {
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: ["chats", "chat-1"],
queryKey: chatEntityKey("chat-1"),
});
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -250,7 +251,7 @@ describe("useChatToolInvalidations", () => {
await waitFor(() => {
expect(invalidateSpy).toHaveBeenCalledTimes(6);
expect(invalidateSpy).toHaveBeenCalledWith({
queryKey: ["chats", "chat-2"],
queryKey: chatEntityKey("chat-2"),
});
});
});
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
import { useQueryClient } from "react-query";
import { chatKey } from "#/api/queries/chats";
import { chatEntityKey } from "#/api/queries/chats";
import { invalidateWorkspaceMutationQueries } from "#/api/queries/workspaces";
import { type ChatStore, useChatSelector } from "./chatStore";
import type { StreamState } from "./types";
@@ -88,7 +88,7 @@ export function useChatToolInvalidations({
if (shouldInvalidateChat) {
void queryClient.invalidateQueries({
queryKey: chatKey(chatID),
queryKey: chatEntityKey(chatID),
});
}
@@ -1,7 +1,8 @@
import { LoaderIcon, PlayIcon } from "lucide-react";
import type React from "react";
import { useMutation, useQuery } from "react-query";
import { skipToken, useMutation, useQuery } from "react-query";
import { API } from "#/api/api";
import { chatFilesKey, chatFileTextKey } from "#/api/queries/chats";
import { Button } from "#/components/Button/Button";
import { CopyButton } from "#/components/CopyButton/CopyButton";
import {
@@ -34,15 +35,11 @@ export const ProposePlanTool: React.FC<{
}) => {
const hasInlineContent = (inlineContent?.trim().length ?? 0) > 0;
const fileQuery = useQuery({
queryKey: ["chatFile", fileID],
queryFn: async () => {
if (!fileID) {
throw new Error("Missing file ID");
}
return API.experimental.getChatFileText(fileID);
},
enabled: Boolean(fileID) && !hasInlineContent,
queryKey: fileID ? chatFileTextKey(fileID) : chatFilesKey,
queryFn:
fileID && !hasInlineContent
? () => API.experimental.getChatFileText(fileID)
: skipToken,
staleTime: Number.POSITIVE_INFINITY,
});
@@ -203,7 +203,7 @@ const ChatSearchDialogContent: FC<ChatSearchDialogContentProps> = ({
const hasQuery = hasActiveSearch && normalizedQuery !== undefined;
const searchQuery = useQuery({
...chatSearch(normalizedQuery ?? ""),
...chatSearch({ q: normalizedQuery ?? "" }),
enabled: open && hasQuery,
placeholderData: keepPreviousData,
});
@@ -3,6 +3,7 @@ import { toast } from "sonner";
import { expect, fn, spyOn, userEvent, waitFor, within } from "storybook/test";
import type { Mock } from "vitest";
import { API } from "#/api/api";
import { chatDebugRunKey, chatDebugRunsKey } from "#/api/queries/chats";
import type * as TypesGen from "#/api/typesGenerated";
import { DebugPanel } from "./DebugPanel";
import { CHAT_ID, MockRun, MockStep } from "./debugFixtures";
@@ -456,7 +457,7 @@ const getAllRunSummaries = () =>
const getDebugRunDetailById = () =>
new Map(getAllRunDetails().map((run) => [run.id, run]));
const debugRunsQueryKey = ["chats", CHAT_ID, "debug-runs"] as const;
const debugRunsQueryKey = chatDebugRunsKey(CHAT_ID);
const getSeededRunSummaries = (
queries: readonly { key: readonly unknown[]; data: unknown }[] | undefined,
@@ -535,7 +536,7 @@ export const Empty: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [],
},
],
@@ -615,7 +616,7 @@ export const RunDetailLoading: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [detailProbeSummary],
},
],
@@ -649,7 +650,7 @@ export const RunDetailError: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [detailProbeSummary],
},
],
@@ -682,11 +683,11 @@ export const RunWithNoSteps: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [detailProbeSummary],
},
{
key: ["chats", CHAT_ID, "debug-runs", detailProbeRunId],
key: chatDebugRunKey(CHAT_ID, detailProbeRunId),
data: {
...MockRun,
id: detailProbeRunId,
@@ -719,7 +720,7 @@ export const SingleStepSuccessfulRun: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: successfulRunDetail.id,
@@ -728,7 +729,7 @@ export const SingleStepSuccessfulRun: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", successfulRunDetail.id],
key: chatDebugRunKey(CHAT_ID, successfulRunDetail.id),
data: successfulRunDetail,
},
],
@@ -770,7 +771,7 @@ export const ExportAllRuns: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: successfulRunDetail.id,
@@ -819,7 +820,7 @@ export const ExportAllRunsUsesCachedTerminalRunDetails: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: successfulRunDetail.id,
@@ -828,7 +829,7 @@ export const ExportAllRunsUsesCachedTerminalRunDetails: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", successfulRunDetail.id],
key: chatDebugRunKey(CHAT_ID, successfulRunDetail.id),
data: successfulRunDetail,
},
],
@@ -979,7 +980,7 @@ export const ExportSingleRun: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: successfulRunDetail.id,
@@ -988,7 +989,7 @@ export const ExportSingleRun: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", successfulRunDetail.id],
key: chatDebugRunKey(CHAT_ID, successfulRunDetail.id),
data: successfulRunDetail,
},
],
@@ -1060,7 +1061,7 @@ export const MultiStepRunWithRetries: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: multiStepRunDetail.id,
@@ -1073,7 +1074,7 @@ export const MultiStepRunWithRetries: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", multiStepRunDetail.id],
key: chatDebugRunKey(CHAT_ID, multiStepRunDetail.id),
data: multiStepRunDetail,
},
],
@@ -1118,7 +1119,7 @@ export const ErrorStateWithRedactedHeaders: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: errorRunDetail.id,
@@ -1131,7 +1132,7 @@ export const ErrorStateWithRedactedHeaders: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", errorRunDetail.id],
key: chatDebugRunKey(CHAT_ID, errorRunDetail.id),
data: errorRunDetail,
},
],
@@ -1174,7 +1175,7 @@ export const CompactionAndTitleGenerationBadges: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: "run-compaction",
@@ -1222,7 +1223,7 @@ export const LongRawPayloads: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: longPayloadRunDetail.id,
@@ -1234,7 +1235,7 @@ export const LongRawPayloads: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", longPayloadRunDetail.id],
key: chatDebugRunKey(CHAT_ID, longPayloadRunDetail.id),
data: longPayloadRunDetail,
},
],
@@ -1266,7 +1267,7 @@ export const RichPayloadWithTranscript: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: richRunDetail.id,
@@ -1275,7 +1276,7 @@ export const RichPayloadWithTranscript: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", richRunDetail.id],
key: chatDebugRunKey(CHAT_ID, richRunDetail.id),
data: richRunDetail,
},
],
@@ -1343,7 +1344,7 @@ export const ToolCallStep: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: toolCallRunDetail.id,
@@ -1352,7 +1353,7 @@ export const ToolCallStep: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", toolCallRunDetail.id],
key: chatDebugRunKey(CHAT_ID, toolCallRunDetail.id),
data: toolCallRunDetail,
},
],
@@ -1380,7 +1381,7 @@ export const FallbackLabeledRun: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: "run-fallback",
@@ -1409,7 +1410,7 @@ export const InProgressRun: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: "run-progress",
@@ -1586,7 +1587,7 @@ export const BackendNormalizedShape: Story = {
parameters: {
queries: [
{
key: ["chats", CHAT_ID, "debug-runs"],
key: chatDebugRunsKey(CHAT_ID),
data: [
buildRunSummary({
id: backendShapeRunDetail.id,
@@ -1597,7 +1598,7 @@ export const BackendNormalizedShape: Story = {
],
},
{
key: ["chats", CHAT_ID, "debug-runs", backendShapeRunDetail.id],
key: chatDebugRunKey(CHAT_ID, backendShapeRunDetail.id),
data: backendShapeRunDetail,
},
],
@@ -1,6 +1,7 @@
import type { SetURLSearchParams } from "react-router";
import {
CHAT_LIST_PR_STATUS_ORDER,
CHAT_SOURCE_ORDER,
type ChatListPRStatusFilter,
type ChatListStatusFilter,
canonicalizeChatListPRStatuses,
@@ -12,7 +13,7 @@ export const AGENT_CHAT_STATUS_ORDER = [
"read",
] as const satisfies readonly ChatListStatusFilter[];
export const AGENT_PR_STATUS_ORDER = CHAT_LIST_PR_STATUS_ORDER;
export const AGENT_SOURCE_ORDER = ["created_by_me", "shared_with_me"] as const;
export const AGENT_SOURCE_ORDER = CHAT_SOURCE_ORDER;
export type AgentArchiveStatusFilter =
(typeof AGENT_ARCHIVE_STATUS_ORDER)[number];