From 0c88c2accc6c119e31cd388bae1462447e9c0b9e Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 5 Aug 2026 21:19:41 +0100 Subject: [PATCH] refactor(site): migrate chats query keys to collections/entities taxonomy (#27841) --- site/src/api/queries/aiProviders.ts | 29 +- site/src/api/queries/chatDebugLogging.ts | 36 -- site/src/api/queries/chats.test.ts | 362 +++++++------ site/src/api/queries/chats.ts | 494 +++++++++++------- .../CoderAgentsPage/CoderAgentsPage.tsx | 55 +- .../ModelsPage/AddModelPage/AddModelPage.tsx | 2 +- .../AISettingsPage/ModelsPage/ModelsPage.tsx | 7 +- .../UpdateModelPage/UpdateModelPage.tsx | 2 +- .../AgentsPage/AgentChatPage.stories.tsx | 24 +- site/src/pages/AgentsPage/AgentChatPage.tsx | 14 +- site/src/pages/AgentsPage/AgentCreatePage.tsx | 2 +- .../src/pages/AgentsPage/AgentsPageLayout.tsx | 24 +- .../ChatConversation/chatStore.test.tsx | 10 +- .../useChatToolInvalidations.test.tsx | 9 +- .../useChatToolInvalidations.ts | 4 +- .../ChatElements/tools/ProposePlanTool.tsx | 17 +- .../ChatsSidebar/dialogs/ChatSearchDialog.tsx | 2 +- .../DebugPanel/DebugPanel.stories.tsx | 57 +- .../AgentsPage/utils/agentSidebarFilters.ts | 3 +- 19 files changed, 642 insertions(+), 511 deletions(-) delete mode 100644 site/src/api/queries/chatDebugLogging.ts diff --git a/site/src/api/queries/aiProviders.ts b/site/src/api/queries/aiProviders.ts index 3b5b279314..ffb218076e 100644 --- a/site/src/api/queries/aiProviders.ts +++ b/site/src/api/queries/aiProviders.ts @@ -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 => 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 => API.getAIProviders(), + select: selectChatProviderConfigs, + }); + export const aiProvider = (idOrName: string) => ({ queryKey: aiProviderKeyFor(idOrName), queryFn: (): Promise => API.getAIProvider(idOrName), diff --git a/site/src/api/queries/chatDebugLogging.ts b/site/src/api/queries/chatDebugLogging.ts deleted file mode 100644 index dd53f0c0dd..0000000000 --- a/site/src/api/queries/chatDebugLogging.ts +++ /dev/null @@ -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, - }); - }, -}); diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts index a85e1b8ad7..64ec5979b2 100644 --- a/site/src/api/queries/chats.test.ts +++ b/site/src/api/queries/chats.test.ts @@ -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[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(infiniteChatsKey(opts), { + queryClient.setQueryData(chatListKey(toChatListParams(opts)), { pages: [chats], pageParams: [0], }); @@ -98,7 +102,9 @@ const readInfiniteChats = ( queryClient: QueryClient, opts?: InfiniteChatsTestOptions, ): TypesGen.Chat[] | undefined => { - const data = queryClient.getQueryData(infiniteChatsKey(opts)); + const data = queryClient.getQueryData( + 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(chatKey(chatId))?.title, + queryClient.getQueryData(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(chatKey(chatId)); + const cachedChat = queryClient.getQueryData( + 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(chatKey(chatId)), + queryClient.getQueryData(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(chatKey(chatId))?.archived, + queryClient.getQueryData(chatEntityKey(chatId))?.archived, ).toBe(true); mutation.onError(new Error("server error"), chatId, context); - const rolledBack = queryClient.getQueryData(chatKey(chatId)); + const rolledBack = queryClient.getQueryData( + 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(chatKey(chatId))?.archived, + queryClient.getQueryData(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(chatKey(chatId)), + queryClient.getQueryData(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(chatKey(chatId))?.archived, + queryClient.getQueryData(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(chatKey(chatId))?.archived, + queryClient.getQueryData(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(chatKey(chatId))?.pin_order, + queryClient.getQueryData(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(chatKey(chatId))?.pin_order, + queryClient.getQueryData(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(chatKey(chatId))?.pin_order, + queryClient.getQueryData(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(chatKey(chatId))?.pin_order, + queryClient.getQueryData(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(chatKey(chatId)), + queryClient.getQueryData(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(chatKey(childId)), + queryClient.getQueryData(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(chatKey(chatId)), + queryClient.getQueryData(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); diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index 4bef4598a6..b9b02b1f19 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -10,35 +10,53 @@ import { type CreateChatMessageRequestWithClearablePlanMode, } from "#/api/api"; import type * as TypesGen from "#/api/typesGenerated"; +import { ChatListSources } from "#/api/typesGenerated"; import { projectEditedConversationIntoCache, reconcileEditedMessageInCache, } from "./chatMessageEdits"; -export const chatsKey = ["chats"] as const; -export const chatKey = (chatId: string) => ["chats", chatId] as const; -export const chatMessagesKey = (chatId: string) => - ["chats", chatId, "messages"] as const; -export const chatPromptsKey = (chatId: string) => - ["chats", chatId, "prompts"] as const; +const chatCollectionsKey = ["chats", "collections"] as const; -const chatQueueConvergenceKey = (chatId: string) => - ["chats", chatId, "queue-convergence"] as const; +export const chatListFamilyKey = [...chatCollectionsKey, "list"] as const; -export const chatACLKey = (chatId: string) => ["chats", chatId, "acl"] as const; +const chatSearchFamilyKey = [...chatCollectionsKey, "search"] as const; + +export const chatsByWorkspaceFamilyKey = [ + ...chatCollectionsKey, + "by-workspace", +] as const; + +export const chatEntityKey = (chatId: string) => + ["chats", "entities", chatId] as const; + +export const chatFilesKey = ["chats", "files"] as const; + +const chatAnalyticsKey = ["chats", "analytics"] as const; + +const chatConfigKey = ["chats", "config"] as const; export type ChatListPRStatusFilter = "draft" | "open" | "merged" | "closed"; export type ChatListStatusFilter = "read" | "unread"; -type InfiniteChatsFilters = Readonly<{ +type ChatListParams = Readonly<{ + archived: boolean; + prStatuses: readonly ChatListPRStatusFilter[]; + status: ChatListStatusFilter | "all"; + sources: readonly TypesGen.ChatListSource[]; +}>; + +export type ChatListInput = Readonly<{ archived?: boolean; prStatuses?: readonly ChatListPRStatusFilter[]; chatStatus?: ChatListStatusFilter; sources?: readonly TypesGen.ChatListSource[]; }>; -export const infiniteChatsKey = (filters?: InfiniteChatsFilters) => - [...chatsKey, filters] as const; +type ChatSearchParams = Readonly<{ q: string }>; + +const chatsByWorkspaceKey = (workspaceIds: readonly string[]) => + [...chatsByWorkspaceFamilyKey, workspaceIds] as const; export const CHAT_LIST_PR_STATUS_ORDER = [ "draft", @@ -70,29 +88,33 @@ export const canonicalizeChatListPRStatuses = ( return CHAT_LIST_PR_STATUS_ORDER.filter((status) => selected.has(status)); }; -export const chatsByWorkspaceKeyPrefix = [...chatsKey, "by-workspace"] as const; +const canonicalWorkspaceIds = ( + workspaceIds: readonly string[], +): readonly string[] => { + return [...new Set(workspaceIds)].sort(); +}; -export const chatsByWorkspace = (workspaceIds: string[]) => { - const sorted = workspaceIds.toSorted(); +export const chatsByWorkspace = (workspaceIds: readonly string[]) => { + const sorted = canonicalWorkspaceIds(workspaceIds); return { - queryKey: [...chatsKey, "by-workspace", sorted], + queryKey: chatsByWorkspaceKey(sorted), queryFn: () => API.experimental.getChatsByWorkspace(sorted), - enabled: workspaceIds.length > 0, + enabled: sorted.length > 0, }; }; /** - * Updates a single chat inside every page of the infinite chats query - * cache. Use this instead of setQueryData(chatsKey, ...) which writes - * to the wrong key (the flat list key, not the infinite query key). + * Writes an updater across every cached chat list entry by targeting the + * list family prefix. Each filter combination is a separate query whose + * key starts with that prefix, so setQueriesData hits them all at once; + * setQueryData on a single key would silently miss the sibling variants. */ export const updateInfiniteChatsCache = ( queryClient: QueryClient, updater: (chats: TypesGen.Chat[]) => TypesGen.Chat[], ) => { - // Update ALL infinite chat queries regardless of their filter opts. queryClient.setQueriesData( - { queryKey: chatsKey, predicate: isChatListQuery }, + { queryKey: chatListFamilyKey }, (prev) => { if (!prev?.pages) return prev; const nextPages = prev.pages.map((page) => updater(page)); @@ -115,7 +137,7 @@ export const prependToInfiniteChatsCache = ( chat: TypesGen.Chat, ) => { queryClient.setQueriesData( - { queryKey: chatsKey, predicate: isChatListQuery }, + { queryKey: chatListFamilyKey }, (prev) => { if (!prev?.pages) return prev; // Check across ALL pages to avoid duplicates. @@ -140,8 +162,7 @@ export const readInfiniteChatsCache = ( queryClient: QueryClient, ): TypesGen.Chat[] | undefined => { const queries = queryClient.getQueriesData({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); for (const [, data] of queries) { if (data?.pages) { @@ -235,22 +256,22 @@ export const removeChildFromParentInCache = ( return found; }; -// Inverse of infiniteChatsKey, which builds keys as [...chatsKey, filters?]. -// The optional filter object lives in the slot immediately after the -// chatsKey prefix, so derive both the expected length and the filter index -// from chatsKey. If infiniteChatsKey's shape changes, this must change with -// it; the "infiniteChatsKey shape" test in chats.test.ts guards that contract. +// Inverse of chatListKey, which builds keys as [...chatListFamilyKey, params]. +// The params object lives in the slot immediately after the list family +// prefix, so derive both the expected length and the params index from +// chatListFamilyKey. If chatListKey's shape changes, this must change with +// it; the "chatListKey shape" test in chats.test.ts guards that contract. const archivedFilterForChatListKey = ( queryKey: readonly unknown[], ): boolean | undefined => { - if (queryKey.length !== chatsKey.length + 1) { + if (queryKey.length !== chatListFamilyKey.length + 1) { return undefined; } - const filters = queryKey[chatsKey.length]; - if (!filters || typeof filters !== "object") { + const params = queryKey[chatListFamilyKey.length]; + if (!params || typeof params !== "object") { return undefined; } - const archived = (filters as { archived?: unknown }).archived; + const archived = (params as { archived?: unknown }).archived; return typeof archived === "boolean" ? archived : undefined; }; @@ -286,7 +307,7 @@ export const applyChatArchiveStateToCaches = ( archived: boolean, ) => { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), (chat) => (chat ? patchChatArchiveState(chat, archived) : chat), ); @@ -301,8 +322,7 @@ export const applyChatArchiveStateToCaches = ( } const queries = queryClient.getQueriesData({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); for (const [queryKey, data] of queries) { @@ -541,7 +561,7 @@ export const mergeWatchedChatIntoCaches = ( updateChildInParentCache(queryClient, mergeCachedChat, watchedChat.id); queryClient.setQueryData( - chatKey(watchedChat.id), + chatEntityKey(watchedChat.id), (cachedChat) => { if (!cachedChat) { return cachedChat; @@ -556,8 +576,7 @@ const getNextOptimisticPinOrder = (queryClient: QueryClient): number => { const queries = queryClient.getQueriesData< TypesGen.Chat[] | { pages: TypesGen.Chat[][]; pageParams: unknown[] } >({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); for (const [, data] of queries) { @@ -582,27 +601,9 @@ const getNextOptimisticPinOrder = (queryClient: QueryClient): number => { return maxPinOrder + 1; }; -/** - * Predicate that matches only chat-list queries (the sidebar), not - * per-chat queries (detail, messages, diffs, cost). - * - * Sidebar keys look like ["chats"] or ["chats", ]. - * Per-chat keys look like ["chats", , ...]. - */ -const isChatListQuery = (query: { queryKey: readonly unknown[] }): boolean => { - const key = query.queryKey; - // Match: ["chats"] (flat list). - if (key.length <= 1) return true; - // Match: ["chats", ] (infinite query - // with optional filter opts like {archived, q}). - const segment = key[1]; - return segment === undefined || typeof segment === "object"; -}; - export const invalidateChatListQueries = (queryClient: QueryClient) => { return queryClient.invalidateQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); }; @@ -623,7 +624,6 @@ const isChatListRefetch = (query: { queryKey: readonly unknown[]; state: { data: unknown; fetchMeta: unknown }; }): boolean => { - if (!isChatListQuery(query)) return false; // Never cancel the initial load. Reverting a first-ever // fetch produces a stuck pending/idle state that react-query // does not automatically recover from. @@ -645,16 +645,10 @@ const isChatListRefetch = (query: { * Pagination fetches are intentionally excluded because * cancelling them would prevent the sidebar from loading * additional pages when WebSocket events arrive frequently. - * - * Mutation onMutate handlers should keep the broad - * isChatListQuery predicate instead: mutations are infrequent - * and must cancel pagination fetches to protect optimistic - * updates from being overwritten by the oldPages snapshot - * that fetchNextPage captured before the mutation. */ export const cancelChatListRefetches = (queryClient: QueryClient) => { return queryClient.cancelQueries({ - queryKey: chatsKey, + queryKey: chatListFamilyKey, predicate: isChatListRefetch, }); }; @@ -681,31 +675,59 @@ const toChatPlanModePayload = ( return planMode ?? CLEAR_PLAN_MODE_WIRE_VALUE; }; -const getInfiniteChatsQueryString = ( - filters: InfiniteChatsFilters | undefined, -): string | undefined => { +export const CHAT_SOURCE_ORDER = [ + ...ChatListSources, +] as const satisfies readonly TypesGen.ChatListSource[]; + +const chatSourceSet = new Set(CHAT_SOURCE_ORDER); + +const canonicalizeChatSources = ( + sources: Iterable, +): readonly TypesGen.ChatListSource[] => { + const selected = new Set(); + for (const source of sources) { + if ( + typeof source === "string" && + chatSourceSet.has(source as TypesGen.ChatListSource) + ) { + selected.add(source as TypesGen.ChatListSource); + } + } + return CHAT_SOURCE_ORDER.filter((source) => selected.has(source)); +}; + +export const toChatListParams = (input?: ChatListInput): ChatListParams => ({ + archived: input?.archived ?? false, + prStatuses: canonicalizeChatListPRStatuses(input?.prStatuses ?? []), + status: input?.chatStatus ?? "all", + sources: canonicalizeChatSources(input?.sources ?? []), +}); + +const getChatListQueryString = (params: ChatListParams): string | undefined => { const qParts: string[] = []; - if (filters?.archived !== undefined) { - qParts.push(`archived:${filters.archived}`); + qParts.push(`archived:${params.archived}`); + if (params.prStatuses.length) { + qParts.push(`pr_status:${params.prStatuses.join(",")}`); } - if (filters?.prStatuses?.length) { - qParts.push(`pr_status:${filters.prStatuses.join(",")}`); + if (params.status !== "all") { + qParts.push(`has_unread:${params.status === "unread"}`); } - if (filters?.chatStatus) { - qParts.push(`has_unread:${filters.chatStatus === "unread"}`); - } - if (filters?.sources?.length) { - qParts.push(`source:${filters.sources.join(",")}`); + if (params.sources.length) { + qParts.push(`source:${params.sources.join(",")}`); } return qParts.length > 0 ? qParts.join(" ") : undefined; }; -export const infiniteChats = (filters?: InfiniteChatsFilters) => { +export const chatListKey = (params: ChatListParams) => + [...chatListFamilyKey, params] as const; + +export const infiniteChats = (input?: ChatListInput) => { const limit = DEFAULT_CHAT_PAGE_LIMIT; - const q = getInfiniteChatsQueryString(filters); + const params = toChatListParams(input); + const q = getChatListQueryString(params); return { - queryKey: infiniteChatsKey(filters), + queryKey: chatListKey(params), getNextPageParam: (lastPage: TypesGen.Chat[], pages: TypesGen.Chat[][]) => { if (lastPage.length < limit) { return undefined; @@ -728,21 +750,27 @@ export const infiniteChats = (filters?: InfiniteChatsFilters) => { } satisfies UseInfiniteQueryOptions; }; -export const chatSearch = (q: string) => +const chatSearchKey = (params: ChatSearchParams) => + [...chatSearchFamilyKey, params] as const; + +export const chatSearch = (params: ChatSearchParams) => queryOptions({ - queryKey: [...chatsKey, "search", { q }], + queryKey: chatSearchKey(params), queryFn: () => API.experimental.getChats({ limit: CHAT_SEARCH_LIMIT, - q, + q: params.q, }), }); export const chat = (chatId: string) => ({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), queryFn: () => API.experimental.getChat(chatId), }); +export const chatACLKey = (chatId: string) => + [...chatEntityKey(chatId), "acl"] as const; + export const chatACL = (chatId: string) => ({ queryKey: chatACLKey(chatId), queryFn: () => API.experimental.getChatACL(chatId), @@ -750,6 +778,12 @@ export const chatACL = (chatId: string) => ({ const MESSAGES_PAGE_SIZE = 50; +export const chatMessagesKey = (chatId: string) => + [...chatEntityKey(chatId), "messages"] as const; + +const chatQueueConvergenceKey = (chatId: string) => + [...chatEntityKey(chatId), "queue-convergence"] as const; + // The queued messages ride on the uncursored page of the messages endpoint, // so settling the queue after a promote needs its own request. Refetching // chatMessagesForInfiniteScroll would reload every page already scrolled. @@ -783,6 +817,9 @@ const PROMPT_HISTORY_LIMIT = 500; const PROMPTS_STALE_MS = 30_000; +export const chatPromptsKey = (chatId: string) => + [...chatEntityKey(chatId), "prompts"] as const; + export const chatPromptsQuery = (chatId: string) => ({ queryKey: chatPromptsKey(chatId), queryFn: () => @@ -796,15 +833,14 @@ export const archiveChat = (queryClient: QueryClient) => ({ API.experimental.updateChat(chatId, { archived: true }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); // Flip archived flag in the flat root list; strip the // chat from any parent's embedded children (individual @@ -819,7 +855,7 @@ export const archiveChat = (queryClient: QueryClient) => ({ removeChildFromParentInCache(queryClient, chatId); if (previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), patchChatArchiveState(previousChat, true), ); } @@ -838,7 +874,7 @@ export const archiveChat = (queryClient: QueryClient) => ({ void invalidateChatListQueries(queryClient); if (context?.previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), context.previousChat, ); } @@ -849,11 +885,11 @@ export const archiveChat = (queryClient: QueryClient) => ({ onSettled: (_data: unknown, _error: unknown, chatId: string) => { void invalidateChatListQueries(queryClient); void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); void queryClient.invalidateQueries({ - queryKey: chatsByWorkspaceKeyPrefix, + queryKey: chatsByWorkspaceFamilyKey, }); }, }); @@ -863,15 +899,14 @@ export const unarchiveChat = (queryClient: QueryClient) => ({ API.experimental.updateChat(chatId, { archived: false }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); // Reuse patchChatArchiveState so the optimistic snapshot // matches the confirmed onSuccess state. @@ -882,7 +917,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({ ); if (previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), patchChatArchiveState(previousChat, false), ); } @@ -901,7 +936,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({ void invalidateChatListQueries(queryClient); if (context?.previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), context.previousChat, ); } @@ -912,11 +947,11 @@ export const unarchiveChat = (queryClient: QueryClient) => ({ onSettled: (_data: unknown, _error: unknown, chatId: string) => { void invalidateChatListQueries(queryClient); void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); void queryClient.invalidateQueries({ - queryKey: chatsByWorkspaceKeyPrefix, + queryKey: chatsByWorkspaceFamilyKey, }); }, }); @@ -928,15 +963,14 @@ export const updateChatPlanMode = (queryClient: QueryClient) => ({ }), onMutate: async ({ chatId, planMode }: UpdateChatPlanModeVariables) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); updateInfiniteChatsCache(queryClient, (chats) => chats.map((chat) => @@ -944,7 +978,7 @@ export const updateChatPlanMode = (queryClient: QueryClient) => ({ ), ); if (previousChat) { - queryClient.setQueryData(chatKey(chatId), { + queryClient.setQueryData(chatEntityKey(chatId), { ...previousChat, plan_mode: planMode, }); @@ -975,7 +1009,10 @@ export const updateChatPlanMode = (queryClient: QueryClient) => ({ : chat, ), ); - queryClient.setQueryData(chatKey(chatId), previousChat); + queryClient.setQueryData( + chatEntityKey(chatId), + previousChat, + ); }, }); @@ -989,15 +1026,14 @@ export const updateChatWorkspace = (queryClient: QueryClient) => ({ }), onMutate: async ({ chatId, workspaceId }: UpdateChatWorkspaceVariables) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); updateInfiniteChatsCache(queryClient, (chats) => chats.map((chat) => @@ -1007,7 +1043,7 @@ export const updateChatWorkspace = (queryClient: QueryClient) => ({ ), ); if (previousChat) { - queryClient.setQueryData(chatKey(chatId), { + queryClient.setQueryData(chatEntityKey(chatId), { ...previousChat, workspace_id: workspaceId ?? undefined, }); @@ -1036,7 +1072,10 @@ export const updateChatWorkspace = (queryClient: QueryClient) => ({ : chat, ), ); - queryClient.setQueryData(chatKey(chatId), previousChat); + queryClient.setQueryData( + chatEntityKey(chatId), + previousChat, + ); } }, onSettled: async ( @@ -1046,11 +1085,11 @@ export const updateChatWorkspace = (queryClient: QueryClient) => ({ ) => { await invalidateChatListQueries(queryClient); await queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); await queryClient.invalidateQueries({ - queryKey: chatsByWorkspaceKeyPrefix, + queryKey: chatsByWorkspaceFamilyKey, }); }, }); @@ -1060,15 +1099,14 @@ export const pinChat = (queryClient: QueryClient) => ({ API.experimental.updateChat(chatId, { pin_order: 1 }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); const optimisticPinOrder = getNextOptimisticPinOrder(queryClient); updateInfiniteChatsCache(queryClient, (chats) => @@ -1077,7 +1115,7 @@ export const pinChat = (queryClient: QueryClient) => ({ ), ); if (previousChat) { - queryClient.setQueryData(chatKey(chatId), { + queryClient.setQueryData(chatEntityKey(chatId), { ...previousChat, pin_order: optimisticPinOrder, }); @@ -1097,7 +1135,7 @@ export const pinChat = (queryClient: QueryClient) => ({ void invalidateChatListQueries(queryClient); if (context?.previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), context.previousChat, ); } @@ -1105,7 +1143,7 @@ export const pinChat = (queryClient: QueryClient) => ({ onSettled: async (_data: unknown, _error: unknown, chatId: string) => { await invalidateChatListQueries(queryClient); await queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); }, @@ -1116,15 +1154,14 @@ export const unpinChat = (queryClient: QueryClient) => ({ API.experimental.updateChat(chatId, { pin_order: 0 }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); const previousChat = queryClient.getQueryData( - chatKey(chatId), + chatEntityKey(chatId), ); updateInfiniteChatsCache(queryClient, (chats) => chats.map((chat) => @@ -1132,7 +1169,7 @@ export const unpinChat = (queryClient: QueryClient) => ({ ), ); if (previousChat) { - queryClient.setQueryData(chatKey(chatId), { + queryClient.setQueryData(chatEntityKey(chatId), { ...previousChat, pin_order: 0, }); @@ -1152,7 +1189,7 @@ export const unpinChat = (queryClient: QueryClient) => ({ void invalidateChatListQueries(queryClient); if (context?.previousChat) { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), context.previousChat, ); } @@ -1160,7 +1197,7 @@ export const unpinChat = (queryClient: QueryClient) => ({ onSettled: async (_data: unknown, _error: unknown, chatId: string) => { await invalidateChatListQueries(queryClient); await queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); }, @@ -1177,11 +1214,10 @@ export const reorderPinnedChat = (queryClient: QueryClient) => ({ pinOrder: number; }) => { await queryClient.cancelQueries({ - queryKey: chatsKey, - predicate: isChatListQuery, + queryKey: chatListFamilyKey, }); await queryClient.cancelQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); @@ -1212,7 +1248,7 @@ export const reorderPinnedChat = (queryClient: QueryClient) => ({ ) => { await invalidateChatListQueries(queryClient); await queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); }, @@ -1241,7 +1277,7 @@ export const updateChatTitle = (queryClient: QueryClient) => ({ onSuccess: (_data: unknown, { chatId, title }: UpdateChatTitleVariables) => { queryClient.setQueryData( - chatKey(chatId), + chatEntityKey(chatId), (chat) => (chat ? { ...chat, title } : chat), ); updateInfiniteChatsCache(queryClient, (chats) => @@ -1256,16 +1292,16 @@ export const updateChatTitle = (queryClient: QueryClient) => ({ ) => { void invalidateChatListQueries(queryClient); void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); }, }); export const chatDebugRunsKey = (chatId: string) => - [...chatKey(chatId), "debug-runs"] as const; + [...chatEntityKey(chatId), "debug-runs"] as const; -const chatDebugRunKey = (chatId: string, runId: string) => +export const chatDebugRunKey = (chatId: string, runId: string) => [...chatDebugRunsKey(chatId), runId] as const; // Foreground poll cadence when the Debug tab is open. The error cadence @@ -1345,7 +1381,7 @@ export const createChat = (queryClient: QueryClient) => ({ onSuccess: () => { void invalidateChatListQueries(queryClient); void queryClient.invalidateQueries({ - queryKey: chatsByWorkspaceKeyPrefix, + queryKey: chatsByWorkspaceFamilyKey, }); }, }); @@ -1359,7 +1395,7 @@ export const createChatMessage = ( onSuccess: () => { void invalidateChatDebugRuns(queryClient, chatId); void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); void queryClient.invalidateQueries({ @@ -1453,7 +1489,7 @@ export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({ // sticky user message is settling after the optimistic // truncation. void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); void queryClient.invalidateQueries({ @@ -1477,7 +1513,7 @@ export const compactChat = (queryClient: QueryClient, chatId: string) => ({ // The compaction transitions the chat to running; the summary // rows stream in over the websocket like any other turn. void queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); void invalidateChatDebugRuns(queryClient, chatId); @@ -1497,7 +1533,7 @@ export const refreshChatContext = ( ) => ({ mutationFn: () => API.experimental.refreshChatContext(chatId), onSuccess: (updatedChat: TypesGen.Chat) => { - queryClient.setQueryData(chatKey(chatId), (cached) => + queryClient.setQueryData(chatEntityKey(chatId), (cached) => cached ? { ...cached, context: updatedChat.context } : updatedChat, ); const applyContext = (chat: TypesGen.Chat): TypesGen.Chat => @@ -1525,7 +1561,7 @@ export const deleteChatQueuedMessage = ( API.experimental.deleteChatQueuedMessage(chatId, queuedMessageId), onSuccess: async () => { await queryClient.invalidateQueries({ - queryKey: chatKey(chatId), + queryKey: chatEntityKey(chatId), exact: true, }); await queryClient.invalidateQueries({ @@ -1547,14 +1583,14 @@ export const promoteChatQueuedMessage = ( }); export const chatDiffContentsKey = (chatId: string) => - ["chats", chatId, "diff-contents"] as const; + [...chatEntityKey(chatId), "diff-contents"] as const; export const chatDiffContents = (chatId: string) => ({ queryKey: chatDiffContentsKey(chatId), queryFn: () => API.experimental.getChatDiffContents(chatId), }); -const chatSystemPromptKey = ["chat-system-prompt"] as const; +const chatSystemPromptKey = [...chatConfigKey, "system-prompt"] as const; export const chatSystemPrompt = () => ({ queryKey: chatSystemPromptKey, @@ -1571,7 +1607,10 @@ export const updateChatSystemPrompt = (queryClient: QueryClient) => ({ }, }); -const chatPlanModeInstructionsKey = ["chat-plan-mode-instructions"] as const; +const chatPlanModeInstructionsKey = [ + ...chatConfigKey, + "plan-mode-instructions", +] as const; export const chatPlanModeInstructions = () => ({ queryKey: chatPlanModeInstructionsKey, @@ -1589,8 +1628,9 @@ export const updateChatPlanModeInstructions = (queryClient: QueryClient) => ({ }); const chatPersonalModelOverridesAdminSettingsKey = [ - ...chatsKey, - "admin-personal-model-overrides", + ...chatConfigKey, + "personal-model-overrides", + "admin", ] as const; export const chatPersonalModelOverridesAdminSettings = () => ({ @@ -1614,8 +1654,48 @@ export const updateChatPersonalModelOverridesAdminSettings = ( }, }); -export * from "./chatDebugLogging"; -export const chatAdvisorConfigKey = ["chat-advisor-config"] as const; +const chatDebugLoggingAdminKey = [ + ...chatConfigKey, + "debug-logging", + "admin", +] as const; +const chatDebugLoggingMeKey = [ + ...chatConfigKey, + "debug-logging", + "me", +] as const; + +export const chatDebugLogging = () => ({ + queryKey: chatDebugLoggingAdminKey, + queryFn: () => API.experimental.getChatDebugLogging(), +}); + +export const userChatDebugLogging = () => ({ + queryKey: chatDebugLoggingMeKey, + queryFn: () => API.experimental.getUserChatDebugLogging(), +}); + +export const updateChatDebugLogging = (queryClient: QueryClient) => ({ + mutationFn: API.experimental.updateChatDebugLogging, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: chatDebugLoggingAdminKey, + }); + await queryClient.invalidateQueries({ + queryKey: chatDebugLoggingMeKey, + }); + }, +}); + +export const updateUserChatDebugLogging = (queryClient: QueryClient) => ({ + mutationFn: API.experimental.updateUserChatDebugLogging, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: chatDebugLoggingMeKey, + }); + }, +}); +export const chatAdvisorConfigKey = [...chatConfigKey, "advisor"] as const; export const chatAdvisorConfig = () => ({ queryKey: chatAdvisorConfigKey, @@ -1633,7 +1713,10 @@ export const updateChatAdvisorConfig = (queryClient: QueryClient) => ({ }, }); -const chatComputerUseProviderKey = ["chat-computer-use-provider"] as const; +const chatComputerUseProviderKey = [ + ...chatConfigKey, + "computer-use-provider", +] as const; export const chatComputerUseProvider = () => ({ queryKey: chatComputerUseProviderKey, @@ -1649,7 +1732,7 @@ export const updateChatComputerUseProvider = (queryClient: QueryClient) => ({ }, }); -const chatWorkspaceTTLKey = ["chat-workspace-ttl"] as const; +const chatWorkspaceTTLKey = [...chatConfigKey, "workspace-ttl"] as const; export const chatWorkspaceTTL = () => ({ queryKey: chatWorkspaceTTLKey, @@ -1665,7 +1748,7 @@ export const updateChatWorkspaceTTL = (queryClient: QueryClient) => ({ }, }); -const chatRetentionDaysKey = ["chat-retention-days"] as const; +const chatRetentionDaysKey = [...chatConfigKey, "retention-days"] as const; export const chatRetentionDays = () => ({ queryKey: chatRetentionDaysKey, @@ -1681,7 +1764,10 @@ export const updateChatRetentionDays = (queryClient: QueryClient) => ({ }, }); -const chatDebugRetentionDaysKey = ["chat-debug-retention-days"] as const; +const chatDebugRetentionDaysKey = [ + ...chatConfigKey, + "debug-retention-days", +] as const; export const chatDebugRetentionDays = () => ({ queryKey: chatDebugRetentionDaysKey, @@ -1697,7 +1783,7 @@ export const updateChatDebugRetentionDays = (queryClient: QueryClient) => ({ }, }); -const chatAutoArchiveDaysKey = ["chat-auto-archive-days"] as const; +const chatAutoArchiveDaysKey = [...chatConfigKey, "auto-archive-days"] as const; export const chatAutoArchiveDays = () => ({ queryKey: chatAutoArchiveDaysKey, @@ -1713,7 +1799,10 @@ export const updateChatAutoArchiveDays = (queryClient: QueryClient) => ({ }, }); -const chatTemplateAllowlistKey = ["chat-template-allowlist"] as const; +const chatTemplateAllowlistKey = [ + ...chatConfigKey, + "template-allowlist", +] as const; export const chatTemplateAllowlist = () => ({ queryKey: chatTemplateAllowlistKey, @@ -1729,7 +1818,7 @@ export const updateChatTemplateAllowlist = (queryClient: QueryClient) => ({ }, }); -const chatUserCustomPromptKey = ["chat-user-custom-prompt"] as const; +const chatUserCustomPromptKey = [...chatConfigKey, "prompt", "me"] as const; export const chatUserCustomPrompt = () => ({ queryKey: chatUserCustomPromptKey, @@ -1746,8 +1835,9 @@ export const updateUserChatCustomPrompt = (queryClient: QueryClient) => ({ }); const userChatPersonalModelOverridesKey = [ - ...chatsKey, - "user-personal-model-overrides", + ...chatConfigKey, + "personal-model-overrides", + "me", ] as const; export const userChatPersonalModelOverrides = () => ({ @@ -1774,7 +1864,9 @@ export const updateUserChatPersonalModelOverride = ( }); const userCompactionThresholdsKey = [ - "chat-user-compaction-thresholds", + ...chatConfigKey, + "compaction-thresholds", + "me", ] as const; export const userCompactionThresholds = () => ({ @@ -1808,7 +1900,7 @@ export const deleteUserCompactionThreshold = (queryClient: QueryClient) => ({ }, }); -export const chatModelsKey = ["chat-models"] as const; +export const chatModelsKey = [...chatConfigKey, "models", "catalog"] as const; export const chatModels = () => ({ queryKey: chatModelsKey, @@ -1816,35 +1908,11 @@ export const chatModels = () => ({ API.experimental.getChatModels(), }); -const chatProviderConfigsKey = ["chat-provider-configs"] as const; - -const toChatProviderConfig = ( - provider: TypesGen.AIProvider, -): TypesGen.ChatProviderConfig => ({ - 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 = () => ({ - queryKey: chatProviderConfigsKey, - queryFn: async (): Promise => { - const providers = await API.experimental.listAIProviders(); - return providers.map(toChatProviderConfig); - }, -}); - -export const chatModelConfigsKey = ["chat-model-configs"] as const; +export const chatModelConfigsKey = [ + ...chatConfigKey, + "models", + "definitions", +] as const; export const chatModelConfigs = () => ({ queryKey: chatModelConfigsKey, @@ -1853,7 +1921,9 @@ export const chatModelConfigs = () => ({ }); export const userChatProviderConfigsKey = [ - "user-chat-provider-configs", + "ai", + "provider-keys", + "me", ] as const; export const userChatProviderConfigs = () => ({ @@ -1906,7 +1976,6 @@ export const deleteUserChatProviderKey = (queryClient: QueryClient) => ({ const invalidateChatConfigurationQueries = async (queryClient: QueryClient) => { await Promise.all([ - queryClient.invalidateQueries({ queryKey: chatProviderConfigsKey }), queryClient.invalidateQueries({ queryKey: chatModelConfigsKey }), queryClient.invalidateQueries({ queryKey: chatModelsKey }), ]); @@ -1951,29 +2020,56 @@ export const deleteChatModelConfig = (queryClient: QueryClient) => ({ }, }); -export const chatCostKey = (rootChatId: string) => - [...chatsKey, rootChatId, "cost"] as const; +export const chatFileTextKey = (fileId: string) => + [...chatFilesKey, fileId, "text"] as const; const GATEWAY_REQUEST_STALE_MS = 30_000; +export const chatCostTreeKey = (rootChatId: string) => + [...chatAnalyticsKey, "cost", "tree", rootChatId] as const; + export const chatCost = (rootChatId: string) => ({ - queryKey: chatCostKey(rootChatId), + queryKey: chatCostTreeKey(rootChatId), queryFn: () => API.experimental.getChatCost(rootChatId), staleTime: GATEWAY_REQUEST_STALE_MS, }); +const chatModelOverrideKey = (context: TypesGen.ChatModelOverrideContext) => + [...chatConfigKey, "model-overrides", context] as const; + +export const chatModelOverride = ( + context: TypesGen.ChatModelOverrideContext, +) => ({ + queryKey: chatModelOverrideKey(context), + queryFn: () => API.experimental.getChatModelOverride(context), +}); + +export const updateChatModelOverride = ( + queryClient: QueryClient, + context: TypesGen.ChatModelOverrideContext, +) => ({ + mutationFn: (req: TypesGen.UpdateChatModelOverrideRequest) => + API.experimental.updateChatModelOverride(context, req), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: chatModelOverrideKey(context), + exact: true, + }); + }, +}); + // ── MCP Server Configs ─────────────────────────────────────── -export const mcpServerConfigsKey = ["mcp-server-configs"] as const; +export const mcpServersKey = ["mcp", "servers"] as const; export const mcpServerConfigs = () => ({ - queryKey: mcpServerConfigsKey, + queryKey: mcpServersKey, queryFn: (): Promise => API.experimental.getMCPServerConfigs(), }); const invalidateMCPServerConfigQueries = async (queryClient: QueryClient) => { - await queryClient.invalidateQueries({ queryKey: mcpServerConfigsKey }); + await queryClient.invalidateQueries({ queryKey: mcpServersKey }); }; export const createMCPServerConfig = (queryClient: QueryClient) => ({ diff --git a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx index f825aab183..11bd82c236 100644 --- a/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx +++ b/site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPage.tsx @@ -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), diff --git a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPage.tsx b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPage.tsx index e65120b6a7..4f18d01fca 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPage.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPage.tsx @@ -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"; diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPage.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPage.tsx index 67b54f7cea..d4a5e030fc 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPage.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPage.tsx @@ -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"; diff --git a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPage.tsx b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPage.tsx index e7d95aaabf..c405f0990b 100644 --- a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPage.tsx +++ b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPage.tsx @@ -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"; diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx index eb9f1f631b..4f7ca1c562 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx @@ -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: () => { diff --git a/site/src/pages/AgentsPage/AgentChatPage.tsx b/site/src/pages/AgentsPage/AgentChatPage.tsx index 8242f78d5a..c9d00b7ac8 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.tsx @@ -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(chatKey(chatId), (previousChat) => - previousChat ? { ...previousChat, plan_mode: planMode } : previousChat, + queryClient.setQueryData( + 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; diff --git a/site/src/pages/AgentsPage/AgentCreatePage.tsx b/site/src/pages/AgentsPage/AgentCreatePage.tsx index 23fa489a23..38f019e9cd 100644 --- a/site/src/pages/AgentsPage/AgentCreatePage.tsx +++ b/site/src/pages/AgentsPage/AgentCreatePage.tsx @@ -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, diff --git a/site/src/pages/AgentsPage/AgentsPageLayout.tsx b/site/src/pages/AgentsPage/AgentsPageLayout.tsx index a5e043f206..bacd512843 100644 --- a/site/src/pages/AgentsPage/AgentsPageLayout.tsx +++ b/site/src/pages/AgentsPage/AgentsPageLayout.tsx @@ -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(chatKey(chatId)) ?? + queryClient.getQueryData(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(chatKey(activeChatId)) + ? queryClient.getQueryData(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, }); } diff --git a/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx b/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx index 427d0180c4..ae9db95cd1 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/chatStore.test.tsx @@ -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[][]; diff --git a/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.test.tsx b/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.test.tsx index f6d393cb25..562f7ba1c4 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.test.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.test.tsx @@ -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"), }); }); }); diff --git a/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.ts b/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.ts index 41f90139b1..a6b57fca2c 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.ts +++ b/site/src/pages/AgentsPage/components/ChatConversation/useChatToolInvalidations.ts @@ -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), }); } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx index c08da0db5c..7b968178c7 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx @@ -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, }); diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx index 963da2e9f5..ceea431cd5 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/ChatSearchDialog.tsx @@ -203,7 +203,7 @@ const ChatSearchDialogContent: FC = ({ const hasQuery = hasActiveSearch && normalizedQuery !== undefined; const searchQuery = useQuery({ - ...chatSearch(normalizedQuery ?? ""), + ...chatSearch({ q: normalizedQuery ?? "" }), enabled: open && hasQuery, placeholderData: keepPreviousData, }); diff --git a/site/src/pages/AgentsPage/components/RightPanel/DebugPanel/DebugPanel.stories.tsx b/site/src/pages/AgentsPage/components/RightPanel/DebugPanel/DebugPanel.stories.tsx index 81a31cdcb1..b452a6fe30 100644 --- a/site/src/pages/AgentsPage/components/RightPanel/DebugPanel/DebugPanel.stories.tsx +++ b/site/src/pages/AgentsPage/components/RightPanel/DebugPanel/DebugPanel.stories.tsx @@ -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, }, ], diff --git a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts index 86bbd5de4c..55597b2990 100644 --- a/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts +++ b/site/src/pages/AgentsPage/utils/agentSidebarFilters.ts @@ -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];