From ed19a3a08e9205843902021fc0726fbb5f7fd041 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 23 Mar 2026 16:29:07 +0000 Subject: [PATCH] refactor(site): move experimental endpoints to ExperimentalApiMethods (#23449) --- site/src/api/api.test.ts | 18 ++- site/src/api/api.ts | 88 +++++------ site/src/api/queries/chats.test.ts | 51 ++++--- site/src/api/queries/chats.ts | 93 +++++------ .../AgentSettingsPageView.stories.tsx | 43 +++--- site/src/pages/AgentsPage/AgentsPage.tsx | 2 +- .../AgentsPage/AgentsPageView.stories.tsx | 26 ++-- .../components/AgentDetailView.stories.tsx | 2 +- .../ChatModelAdminPanel.stories.tsx | 142 +++++++++-------- .../components/GitPanel/GitPanel.stories.tsx | 16 +- .../MCPServerAdminPanel.stories.tsx | 144 ++++++++++-------- .../AgentsPage/hooks/useFileAttachments.ts | 5 +- 12 files changed, 346 insertions(+), 284 deletions(-) diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index 12a0a82919..adff022c17 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -285,19 +285,19 @@ describe("api.ts", () => { it.each<[string, () => Promise, unknown]>([ [ "/api/experimental/chats/models", - () => API.getChatModels(), + () => API.experimental.getChatModels(), { providers: [], }, ], [ "/api/experimental/chats/providers", - () => API.getChatProviderConfigs(), + () => API.experimental.getChatProviderConfigs(), [], ], [ "/api/experimental/chats/model-configs", - () => API.getChatModelConfigs(), + () => API.experimental.getChatModelConfigs(), [], ], ])("returns response data for %s", async (path, request, responseData) => { @@ -312,11 +312,17 @@ describe("api.ts", () => { }); it.each<[string, () => Promise]>([ - ["/api/experimental/chats/models", () => API.getChatModels()], - ["/api/experimental/chats/providers", () => API.getChatProviderConfigs()], + [ + "/api/experimental/chats/models", + () => API.experimental.getChatModels(), + ], + [ + "/api/experimental/chats/providers", + () => API.experimental.getChatProviderConfigs(), + ], [ "/api/experimental/chats/model-configs", - () => API.getChatModelConfigs(), + () => API.experimental.getChatModelConfigs(), ], ])("rethrows axios errors for %s", async (path, request) => { const expectedError = new Error("request failed"); diff --git a/site/src/api/api.ts b/site/src/api/api.ts index f80693f070..3e7a7988e6 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2369,27 +2369,6 @@ class ApiMethods { return response.data; }; - uploadChatFile = async ( - file: File, - organizationId: string, - ): Promise => { - const response = await this.axios.post( - `/api/experimental/chats/files?organization=${organizationId}`, - file, - { - headers: { - "Content-Type": file.type || "application/octet-stream", - // Use RFC 5987 encoding for the filename to support - // non-ASCII characters. Placing the raw name directly in - // the header causes XMLHttpRequest to throw because HTTP - // headers only allow ISO-8859-1 code points. - "Content-Disposition": `attachment; filename="file"; filename*=UTF-8''${encodeURIComponent(file.name)}`, - }, - }, - ); - return response.data; - }; - getTemplateVersionLogs = async ( versionId: string, ): Promise => { @@ -3013,6 +2992,50 @@ class ApiMethods { return response.data; }; + getAIBridgeModels = async (options: SearchParamOptions) => { + const url = getURLWithSearchParams("/api/v2/aibridge/models", options); + + const response = await this.axios.get(url); + return response.data; + }; +} + +export type TaskFeedbackRating = "good" | "okay" | "bad"; + +export type CreateTaskFeedbackRequest = { + rate: TaskFeedbackRating; + comment?: string; +}; + +// Experimental API methods call endpoints under the /api/experimental/ prefix. +// These endpoints are not stable and may change or be removed at any time. +// +// All methods must be defined with arrow function syntax. See the docstring +// above the ApiMethods class for a full explanation. +class ExperimentalApiMethods { + constructor(protected readonly axios: AxiosInstance) {} + + uploadChatFile = async ( + file: File, + organizationId: string, + ): Promise => { + const response = await this.axios.post( + `/api/experimental/chats/files?organization=${organizationId}`, + file, + { + headers: { + "Content-Type": file.type || "application/octet-stream", + // Use RFC 5987 encoding for the filename to support + // non-ASCII characters. Placing the raw name directly in + // the header causes XMLHttpRequest to throw because HTTP + // headers only allow ISO-8859-1 code points. + "Content-Disposition": `attachment; filename="file"; filename*=UTF-8''${encodeURIComponent(file.name)}`, + }, + }, + ); + return response.data; + }; + // Chat API methods getChats = async (req?: { after_id?: string; @@ -3300,13 +3323,6 @@ class ApiMethods { ); }; - getAIBridgeModels = async (options: SearchParamOptions) => { - const url = getURLWithSearchParams("/api/v2/aibridge/models", options); - - const response = await this.axios.get(url); - return response.data; - }; - getChatCostSummary = async ( user = "me", params?: ChatCostDateParams, @@ -3408,22 +3424,6 @@ class ApiMethods { }; } -export type TaskFeedbackRating = "good" | "okay" | "bad"; - -export type CreateTaskFeedbackRequest = { - rate: TaskFeedbackRating; - comment?: string; -}; - -// Experimental API methods call endpoints under the /api/experimental/ prefix. -// These endpoints are not stable and may change or be removed at any time. -// -// All methods must be defined with arrow function syntax. See the docstring -// above the ApiMethods class for a full explanation. -class ExperimentalApiMethods { - constructor(protected readonly axios: AxiosInstance) {} -} - // This is a hard coded CSRF token/cookie pair for local development. In prod, // the GoLang webserver generates a random cookie with a new token for each // document request. For local development, we don't use the Go webserver for diff --git a/site/src/api/queries/chats.test.ts b/site/src/api/queries/chats.test.ts index 8b4ac90f76..abd0cab93b 100644 --- a/site/src/api/queries/chats.test.ts +++ b/site/src/api/queries/chats.test.ts @@ -25,16 +25,18 @@ import { vi.mock("api/api", () => ({ API: { - updateChat: vi.fn(), - createChat: vi.fn(), - deleteChatQueuedMessage: vi.fn(), - getChats: vi.fn(), - getChatCostSummary: vi.fn(), - getChatCostUsers: vi.fn(), - createChatMessage: vi.fn(), - editChatMessage: vi.fn(), - interruptChat: vi.fn(), - promoteChatQueuedMessage: vi.fn(), + experimental: { + updateChat: vi.fn(), + createChat: vi.fn(), + deleteChatQueuedMessage: vi.fn(), + getChats: vi.fn(), + getChatCostSummary: vi.fn(), + getChatCostUsers: vi.fn(), + createChatMessage: vi.fn(), + editChatMessage: vi.fn(), + interruptChat: vi.fn(), + promoteChatQueuedMessage: vi.fn(), + }, }, })); @@ -207,7 +209,7 @@ describe("archiveChat optimistic update", () => { const initialChats = [makeChat(chatId), makeChat("chat-2")]; seedInfiniteChats(queryClient, initialChats); - vi.mocked(API.updateChat).mockResolvedValue(); + vi.mocked(API.experimental.updateChat).mockResolvedValue(); const mutation = archiveChat(queryClient); await mutation.onMutate(chatId); @@ -225,7 +227,7 @@ describe("archiveChat optimistic update", () => { seedInfiniteChats(queryClient, [makeChat(chatId)]); queryClient.setQueryData(chatKey(chatId), makeChat(chatId)); - vi.mocked(API.updateChat).mockResolvedValue(); + vi.mocked(API.experimental.updateChat).mockResolvedValue(); const mutation = archiveChat(queryClient); await mutation.onMutate(chatId); @@ -414,7 +416,7 @@ describe("chat cost query factories", () => { start_date: "2025-01-01", end_date: "2025-01-31", }; - vi.mocked(API.getChatCostSummary).mockResolvedValue( + vi.mocked(API.experimental.getChatCostSummary).mockResolvedValue( {} as TypesGen.ChatCostSummary, ); @@ -428,7 +430,10 @@ describe("chat cost query factories", () => { ]); expect(query.queryKey).toEqual(["chats", "costSummary", user, params]); await query.queryFn(); - expect(API.getChatCostSummary).toHaveBeenCalledWith(user, params); + expect(API.experimental.getChatCostSummary).toHaveBeenCalledWith( + user, + params, + ); }); it("builds a distinct users query key and forwards snake_case params", async () => { @@ -439,7 +444,7 @@ describe("chat cost query factories", () => { limit: 10, offset: 20, }; - vi.mocked(API.getChatCostUsers).mockResolvedValue( + vi.mocked(API.experimental.getChatCostUsers).mockResolvedValue( {} as TypesGen.ChatCostUsersResponse, ); @@ -449,7 +454,7 @@ describe("chat cost query factories", () => { expect(query.queryKey).toEqual(["chats", "costUsers", params]); expect(query.queryKey).not.toEqual(chatCostSummaryKey("me", params)); await query.queryFn(); - expect(API.getChatCostUsers).toHaveBeenCalledWith(params); + expect(API.experimental.getChatCostUsers).toHaveBeenCalledWith(params); }); }); @@ -710,37 +715,37 @@ describe("infiniteChats", () => { describe("queryFn", () => { it("computes offset 0 for pageParam 0", async () => { - vi.mocked(API.getChats).mockResolvedValue([]); + vi.mocked(API.experimental.getChats).mockResolvedValue([]); const { queryFn } = infiniteChats(); await queryFn({ pageParam: 0 }); - expect(API.getChats).toHaveBeenCalledWith({ + expect(API.experimental.getChats).toHaveBeenCalledWith({ limit: PAGE_LIMIT, offset: 0, }); }); it("computes offset 0 for pageParam <= 0", async () => { - vi.mocked(API.getChats).mockResolvedValue([]); + vi.mocked(API.experimental.getChats).mockResolvedValue([]); const { queryFn } = infiniteChats(); await queryFn({ pageParam: -1 }); - expect(API.getChats).toHaveBeenCalledWith({ + expect(API.experimental.getChats).toHaveBeenCalledWith({ limit: PAGE_LIMIT, offset: 0, }); }); it("computes correct offset for subsequent pages", async () => { - vi.mocked(API.getChats).mockResolvedValue([]); + vi.mocked(API.experimental.getChats).mockResolvedValue([]); const { queryFn } = infiniteChats(); await queryFn({ pageParam: 2 }); - expect(API.getChats).toHaveBeenCalledWith({ + expect(API.experimental.getChats).toHaveBeenCalledWith({ limit: PAGE_LIMIT, offset: PAGE_LIMIT, }); await queryFn({ pageParam: 3 }); - expect(API.getChats).toHaveBeenCalledWith({ + expect(API.experimental.getChats).toHaveBeenCalledWith({ limit: PAGE_LIMIT, offset: PAGE_LIMIT * 2, }); diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index ac8ac5f641..be3eb1e3ca 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -132,7 +132,7 @@ export const infiniteChats = (opts?: { q?: string; archived?: boolean }) => { if (typeof pageParam !== "number") { throw new Error("pageParam must be a number"); } - return API.getChats({ + return API.experimental.getChats({ limit, offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit, q, @@ -145,7 +145,7 @@ export const infiniteChats = (opts?: { q?: string; archived?: boolean }) => { export const chat = (chatId: string) => ({ queryKey: chatKey(chatId), - queryFn: () => API.getChat(chatId), + queryFn: () => API.experimental.getChat(chatId), }); const MESSAGES_PAGE_SIZE = 50; @@ -154,7 +154,7 @@ export const chatMessagesForInfiniteScroll = (chatId: string) => ({ queryKey: chatMessagesKey(chatId), initialPageParam: undefined as number | undefined, queryFn: ({ pageParam }: { pageParam: number | undefined }) => - API.getChatMessages(chatId, { + API.experimental.getChatMessages(chatId, { before_id: pageParam, limit: MESSAGES_PAGE_SIZE, }), @@ -170,7 +170,8 @@ export const chatMessagesForInfiniteScroll = (chatId: string) => ({ }); export const archiveChat = (queryClient: QueryClient) => ({ - mutationFn: (chatId: string) => API.updateChat(chatId, { archived: true }), + mutationFn: (chatId: string) => + API.experimental.updateChat(chatId, { archived: true }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ queryKey: chatsKey, @@ -229,7 +230,8 @@ export const archiveChat = (queryClient: QueryClient) => ({ }); export const unarchiveChat = (queryClient: QueryClient) => ({ - mutationFn: (chatId: string) => API.updateChat(chatId, { archived: false }), + mutationFn: (chatId: string) => + API.experimental.updateChat(chatId, { archived: false }), onMutate: async (chatId: string) => { await queryClient.cancelQueries({ queryKey: chatsKey, @@ -288,7 +290,8 @@ export const unarchiveChat = (queryClient: QueryClient) => ({ }); export const createChat = (queryClient: QueryClient) => ({ - mutationFn: (req: TypesGen.CreateChatRequest) => API.createChat(req), + mutationFn: (req: TypesGen.CreateChatRequest) => + API.experimental.createChat(req), onSuccess: () => { void invalidateChatListQueries(queryClient); }, @@ -299,7 +302,7 @@ export const createChatMessage = ( chatId: string, ) => ({ mutationFn: (req: TypesGen.CreateChatMessageRequest) => - API.createChatMessage(chatId, req), + API.experimental.createChatMessage(chatId, req), // No onSuccess invalidation needed: the per-chat WebSocket delivers // the response message via upsertDurableMessage, and the global // watchChats() WebSocket updates the sidebar sort order. @@ -312,7 +315,7 @@ type EditChatMessageMutationArgs = { export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({ mutationFn: ({ messageId, req }: EditChatMessageMutationArgs) => - API.editChatMessage(chatId, messageId, req), + API.experimental.editChatMessage(chatId, messageId, req), onSuccess: () => { // Editing truncates all messages after the edited one on the // server. The WebSocket can insert/update messages but cannot @@ -331,7 +334,7 @@ export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({ }); export const interruptChat = (_queryClient: QueryClient, chatId: string) => ({ - mutationFn: () => API.interruptChat(chatId), + mutationFn: () => API.experimental.interruptChat(chatId), // No onSuccess invalidation needed: the per-chat WebSocket // delivers the status change via setChatStatus, and the global // watchChats() WebSocket updates the sidebar. @@ -342,7 +345,7 @@ export const deleteChatQueuedMessage = ( chatId: string, ) => ({ mutationFn: (queuedMessageId: number) => - API.deleteChatQueuedMessage(chatId, queuedMessageId), + API.experimental.deleteChatQueuedMessage(chatId, queuedMessageId), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatKey(chatId), @@ -360,7 +363,7 @@ export const promoteChatQueuedMessage = ( chatId: string, ) => ({ mutationFn: (queuedMessageId: number) => - API.promoteChatQueuedMessage(chatId, queuedMessageId), + API.experimental.promoteChatQueuedMessage(chatId, queuedMessageId), // No onSuccess invalidation needed: the caller upserts the // promoted message from the response, and the per-chat // WebSocket delivers queue and status updates in real-time. @@ -371,18 +374,18 @@ export const chatDiffContentsKey = (chatId: string) => export const chatDiffContents = (chatId: string) => ({ queryKey: chatDiffContentsKey(chatId), - queryFn: () => API.getChatDiffContents(chatId), + queryFn: () => API.experimental.getChatDiffContents(chatId), }); const chatSystemPromptKey = ["chat-system-prompt"] as const; export const chatSystemPrompt = () => ({ queryKey: chatSystemPromptKey, - queryFn: () => API.getChatSystemPrompt(), + queryFn: () => API.experimental.getChatSystemPrompt(), }); export const updateChatSystemPrompt = (queryClient: QueryClient) => ({ - mutationFn: API.updateChatSystemPrompt, + mutationFn: API.experimental.updateChatSystemPrompt, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatSystemPromptKey, @@ -394,11 +397,11 @@ const chatDesktopEnabledKey = ["chat-desktop-enabled"] as const; export const chatDesktopEnabled = () => ({ queryKey: chatDesktopEnabledKey, - queryFn: () => API.getChatDesktopEnabled(), + queryFn: () => API.experimental.getChatDesktopEnabled(), }); export const updateChatDesktopEnabled = (queryClient: QueryClient) => ({ - mutationFn: API.updateChatDesktopEnabled, + mutationFn: API.experimental.updateChatDesktopEnabled, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatDesktopEnabledKey, @@ -410,11 +413,11 @@ const chatWorkspaceTTLKey = ["chat-workspace-ttl"] as const; export const chatWorkspaceTTL = () => ({ queryKey: chatWorkspaceTTLKey, - queryFn: () => API.getChatWorkspaceTTL(), + queryFn: () => API.experimental.getChatWorkspaceTTL(), }); export const updateChatWorkspaceTTL = (queryClient: QueryClient) => ({ - mutationFn: API.updateChatWorkspaceTTL, + mutationFn: API.experimental.updateChatWorkspaceTTL, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatWorkspaceTTLKey, @@ -426,11 +429,11 @@ const chatUserCustomPromptKey = ["chat-user-custom-prompt"] as const; export const chatUserCustomPrompt = () => ({ queryKey: chatUserCustomPromptKey, - queryFn: () => API.getUserChatCustomPrompt(), + queryFn: () => API.experimental.getUserChatCustomPrompt(), }); export const updateUserChatCustomPrompt = (queryClient: QueryClient) => ({ - mutationFn: API.updateUserChatCustomPrompt, + mutationFn: API.experimental.updateUserChatCustomPrompt, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUserCustomPromptKey, @@ -442,7 +445,8 @@ export const chatModelsKey = ["chat-models"] as const; export const chatModels = () => ({ queryKey: chatModelsKey, - queryFn: (): Promise => API.getChatModels(), + queryFn: (): Promise => + API.experimental.getChatModels(), }); const chatProviderConfigsKey = ["chat-provider-configs"] as const; @@ -450,14 +454,15 @@ const chatProviderConfigsKey = ["chat-provider-configs"] as const; export const chatProviderConfigs = () => ({ queryKey: chatProviderConfigsKey, queryFn: (): Promise => - API.getChatProviderConfigs(), + API.experimental.getChatProviderConfigs(), }); const chatModelConfigsKey = ["chat-model-configs"] as const; export const chatModelConfigs = () => ({ queryKey: chatModelConfigsKey, - queryFn: (): Promise => API.getChatModelConfigs(), + queryFn: (): Promise => + API.experimental.getChatModelConfigs(), }); const invalidateChatConfigurationQueries = async (queryClient: QueryClient) => { @@ -470,7 +475,7 @@ const invalidateChatConfigurationQueries = async (queryClient: QueryClient) => { export const createChatProviderConfig = (queryClient: QueryClient) => ({ mutationFn: (req: TypesGen.CreateChatProviderConfigRequest) => - API.createChatProviderConfig(req), + API.experimental.createChatProviderConfig(req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -486,7 +491,7 @@ export const updateChatProviderConfig = (queryClient: QueryClient) => ({ providerConfigId, req, }: UpdateChatProviderConfigMutationArgs) => - API.updateChatProviderConfig(providerConfigId, req), + API.experimental.updateChatProviderConfig(providerConfigId, req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -494,7 +499,7 @@ export const updateChatProviderConfig = (queryClient: QueryClient) => ({ export const deleteChatProviderConfig = (queryClient: QueryClient) => ({ mutationFn: (providerConfigId: string) => - API.deleteChatProviderConfig(providerConfigId), + API.experimental.deleteChatProviderConfig(providerConfigId), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -502,7 +507,7 @@ export const deleteChatProviderConfig = (queryClient: QueryClient) => ({ export const createChatModelConfig = (queryClient: QueryClient) => ({ mutationFn: (req: TypesGen.CreateChatModelConfigRequest) => - API.createChatModelConfig(req), + API.experimental.createChatModelConfig(req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -515,7 +520,7 @@ type UpdateChatModelConfigMutationArgs = { export const updateChatModelConfig = (queryClient: QueryClient) => ({ mutationFn: ({ modelConfigId, req }: UpdateChatModelConfigMutationArgs) => - API.updateChatModelConfig(modelConfigId, req), + API.experimental.updateChatModelConfig(modelConfigId, req), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -523,7 +528,7 @@ export const updateChatModelConfig = (queryClient: QueryClient) => ({ export const deleteChatModelConfig = (queryClient: QueryClient) => ({ mutationFn: (modelConfigId: string) => - API.deleteChatModelConfig(modelConfigId), + API.experimental.deleteChatModelConfig(modelConfigId), onSuccess: async () => { await invalidateChatConfigurationQueries(queryClient); }, @@ -545,7 +550,7 @@ export const chatCostSummaryKey = (user = "me", params?: ChatCostDateParams) => export const chatCostSummary = (user = "me", params?: ChatCostDateParams) => ({ queryKey: chatCostSummaryKey(user, params), - queryFn: () => API.getChatCostSummary(user, params), + queryFn: () => API.experimental.getChatCostSummary(user, params), staleTime: 60_000, }); @@ -554,7 +559,7 @@ export const chatCostUsersKey = (params?: ChatCostUsersParams) => export const chatCostUsers = (params?: ChatCostUsersParams) => ({ queryKey: chatCostUsersKey(params), - queryFn: () => API.getChatCostUsers(params), + queryFn: () => API.experimental.getChatCostUsers(params), staleTime: 60_000, }); @@ -566,7 +571,7 @@ export const prInsights = (params?: { end_date?: string; }) => ({ queryKey: prInsightsKey(params), - queryFn: () => API.getPRInsights(params), + queryFn: () => API.experimental.getPRInsights(params), staleTime: 60_000, }); @@ -577,7 +582,7 @@ export const chatUsageLimitStatusKey = [ export const chatUsageLimitStatus = () => ({ queryKey: chatUsageLimitStatusKey, - queryFn: () => API.getChatUsageLimitStatus(), + queryFn: () => API.experimental.getChatUsageLimitStatus(), refetchInterval: 60_000, }); @@ -585,12 +590,12 @@ const chatUsageLimitConfigKey = [...chatsKey, "usageLimitConfig"] as const; export const chatUsageLimitConfig = () => ({ queryKey: chatUsageLimitConfigKey, - queryFn: () => API.getChatUsageLimitConfig(), + queryFn: () => API.experimental.getChatUsageLimitConfig(), }); export const updateChatUsageLimitConfig = (queryClient: QueryClient) => ({ mutationFn: (req: TypesGen.ChatUsageLimitConfig) => - API.updateChatUsageLimitConfig(req), + API.experimental.updateChatUsageLimitConfig(req), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUsageLimitConfigKey, @@ -605,7 +610,7 @@ type UpsertChatUsageLimitOverrideMutationArgs = { export const upsertChatUsageLimitOverride = (queryClient: QueryClient) => ({ mutationFn: ({ userID, req }: UpsertChatUsageLimitOverrideMutationArgs) => - API.upsertChatUsageLimitOverride(userID, req), + API.experimental.upsertChatUsageLimitOverride(userID, req), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUsageLimitConfigKey, @@ -614,7 +619,8 @@ export const upsertChatUsageLimitOverride = (queryClient: QueryClient) => ({ }); export const deleteChatUsageLimitOverride = (queryClient: QueryClient) => ({ - mutationFn: (userID: string) => API.deleteChatUsageLimitOverride(userID), + mutationFn: (userID: string) => + API.experimental.deleteChatUsageLimitOverride(userID), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUsageLimitConfigKey, @@ -634,7 +640,7 @@ export const upsertChatUsageLimitGroupOverride = ( groupID, req, }: UpsertChatUsageLimitGroupOverrideMutationArgs) => - API.upsertChatUsageLimitGroupOverride(groupID, req), + API.experimental.upsertChatUsageLimitGroupOverride(groupID, req), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUsageLimitConfigKey, @@ -646,7 +652,7 @@ export const deleteChatUsageLimitGroupOverride = ( queryClient: QueryClient, ) => ({ mutationFn: (groupID: string) => - API.deleteChatUsageLimitGroupOverride(groupID), + API.experimental.deleteChatUsageLimitGroupOverride(groupID), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: chatUsageLimitConfigKey, @@ -660,7 +666,8 @@ const mcpServerConfigsKey = ["mcp-server-configs"] as const; export const mcpServerConfigs = () => ({ queryKey: mcpServerConfigsKey, - queryFn: (): Promise => API.getMCPServerConfigs(), + queryFn: (): Promise => + API.experimental.getMCPServerConfigs(), }); const invalidateMCPServerConfigQueries = async (queryClient: QueryClient) => { @@ -669,7 +676,7 @@ const invalidateMCPServerConfigQueries = async (queryClient: QueryClient) => { export const createMCPServerConfig = (queryClient: QueryClient) => ({ mutationFn: (req: TypesGen.CreateMCPServerConfigRequest) => - API.createMCPServerConfig(req), + API.experimental.createMCPServerConfig(req), onSuccess: async () => { await invalidateMCPServerConfigQueries(queryClient); }, @@ -682,14 +689,14 @@ type UpdateMCPServerConfigMutationArgs = { export const updateMCPServerConfig = (queryClient: QueryClient) => ({ mutationFn: ({ id, req }: UpdateMCPServerConfigMutationArgs) => - API.updateMCPServerConfig(id, req), + API.experimental.updateMCPServerConfig(id, req), onSuccess: async () => { await invalidateMCPServerConfigQueries(queryClient); }, }); export const deleteMCPServerConfig = (queryClient: QueryClient) => ({ - mutationFn: (id: string) => API.deleteMCPServerConfig(id), + mutationFn: (id: string) => API.experimental.deleteMCPServerConfig(id), onSuccess: async () => { await invalidateMCPServerConfigQueries(queryClient); }, diff --git a/site/src/pages/AgentsPage/AgentSettingsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentSettingsPageView.stories.tsx index 1c33dd1a84..dd4f1b4077 100644 --- a/site/src/pages/AgentsPage/AgentSettingsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentSettingsPageView.stories.tsx @@ -107,18 +107,21 @@ const mockCostSummary: TypesGen.ChatCostSummary = { const setupUsageSpies = (opts?: { usersResponse?: TypesGen.ChatCostUsersResponse; }) => { - spyOn(API, "getChatCostUsers").mockResolvedValue( + spyOn(API.experimental, "getChatCostUsers").mockResolvedValue( opts?.usersResponse ?? mockUsersResponse, ); spyOn(API, "getUser").mockResolvedValue(mockUserProfile); - spyOn(API, "getChatCostSummary").mockResolvedValue(mockCostSummary); + spyOn(API.experimental, "getChatCostSummary").mockResolvedValue( + mockCostSummary, + ); }; const getChatCostUsersCalls = () => ( - API.getChatCostUsers as typeof API.getChatCostUsers & { + API.experimental + .getChatCostUsers as typeof API.experimental.getChatCostUsers & { mock: { - calls: Array<[Parameters[0]]>; + calls: Array<[Parameters[0]]>; }; } ).mock.calls; @@ -142,24 +145,24 @@ const meta = { layout: "fullscreen", }, beforeEach: () => { - spyOn(API, "getChatSystemPrompt").mockResolvedValue({ + spyOn(API.experimental, "getChatSystemPrompt").mockResolvedValue({ system_prompt: "", }); - spyOn(API, "updateChatSystemPrompt").mockResolvedValue(); - spyOn(API, "getChatDesktopEnabled").mockResolvedValue({ + spyOn(API.experimental, "updateChatSystemPrompt").mockResolvedValue(); + spyOn(API.experimental, "getChatDesktopEnabled").mockResolvedValue({ enable_desktop: false, }); - spyOn(API, "updateChatDesktopEnabled").mockResolvedValue(); - spyOn(API, "getUserChatCustomPrompt").mockResolvedValue({ + spyOn(API.experimental, "updateChatDesktopEnabled").mockResolvedValue(); + spyOn(API.experimental, "getUserChatCustomPrompt").mockResolvedValue({ custom_prompt: "", }); - spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({ + spyOn(API.experimental, "updateUserChatCustomPrompt").mockResolvedValue({ custom_prompt: "", }); - spyOn(API, "getChatWorkspaceTTL").mockResolvedValue({ + spyOn(API.experimental, "getChatWorkspaceTTL").mockResolvedValue({ workspace_ttl_ms: 0, }); - spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue(); + spyOn(API.experimental, "updateChatWorkspaceTTL").mockResolvedValue(); }, } satisfies Meta; @@ -189,7 +192,7 @@ export const TogglesDesktop: Story = { await userEvent.click(toggle); await waitFor(() => { - expect(API.updateChatDesktopEnabled).toHaveBeenCalledWith({ + expect(API.experimental.updateChatDesktopEnabled).toHaveBeenCalledWith({ enable_desktop: true, }); }); @@ -220,7 +223,7 @@ export const DefaultAutostopDefault: Story = { export const DefaultAutostopCustomValue: Story = { beforeEach: () => { // 2h = 2 hours exactly, shows cleanly in DurationField. - spyOn(API, "getChatWorkspaceTTL").mockResolvedValue({ + spyOn(API.experimental, "getChatWorkspaceTTL").mockResolvedValue({ workspace_ttl_ms: 7_200_000, }); }, @@ -256,7 +259,7 @@ export const DefaultAutostopSave: Story = { await userEvent.click(saveButton); await waitFor(() => { - expect(API.updateChatWorkspaceTTL).toHaveBeenCalledWith({ + expect(API.experimental.updateChatWorkspaceTTL).toHaveBeenCalledWith({ workspace_ttl_ms: 10_800_000, }); }); @@ -349,7 +352,7 @@ export const UsageDateFilter: Story = { const defaultEndLabel = fixedNow.format("MMM D, YYYY"); await waitFor(() => { - expect(API.getChatCostUsers).toHaveBeenCalled(); + expect(API.experimental.getChatCostUsers).toHaveBeenCalled(); }); const initialCallCount = getChatCostUsersCalls().length; @@ -396,7 +399,7 @@ export const UsageDateFilterRefetchOverlay: Story = { }, ); - spyOn(API, "getChatCostUsers").mockImplementation(async () => { + spyOn(API.experimental, "getChatCostUsers").mockImplementation(async () => { requestCount += 1; if (requestCount === 1) { return mockUsersResponse; @@ -405,7 +408,9 @@ export const UsageDateFilterRefetchOverlay: Story = { return refetchPromise; }); spyOn(API, "getUser").mockResolvedValue(mockUserProfile); - spyOn(API, "getChatCostSummary").mockResolvedValue(mockCostSummary); + spyOn(API.experimental, "getChatCostSummary").mockResolvedValue( + mockCostSummary, + ); return () => { resolveRefetch?.({ @@ -496,7 +501,7 @@ export const UsageUserDrillIn: Story = { // The cost summary should have been fetched. await waitFor(() => { - expect(API.getChatCostSummary).toHaveBeenCalled(); + expect(API.experimental.getChatCostSummary).toHaveBeenCalled(); }); // The Back button should be visible. diff --git a/site/src/pages/AgentsPage/AgentsPage.tsx b/site/src/pages/AgentsPage/AgentsPage.tsx index 6fc13be1ba..284aa76d42 100644 --- a/site/src/pages/AgentsPage/AgentsPage.tsx +++ b/site/src/pages/AgentsPage/AgentsPage.tsx @@ -163,7 +163,7 @@ const AgentsPage: FC = () => { chatId: string; workspaceId: string; }) => { - await API.updateChat(chatId, { archived: true }); + await API.experimental.updateChat(chatId, { archived: true }); await API.deleteWorkspace(workspaceId); return { chatId, workspaceId }; }, diff --git a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx index 6dec1fe1aa..de646c40f2 100644 --- a/site/src/pages/AgentsPage/AgentsPageView.stories.tsx +++ b/site/src/pages/AgentsPage/AgentsPageView.stories.tsx @@ -191,20 +191,24 @@ const meta: Meta = { workspaces: [], count: 0, }); - spyOn(API, "getChatCostSummary").mockResolvedValue(mockAnalyticsSummary); - spyOn(API, "getChatCostUsers").mockResolvedValue(mockUsageUsers); - spyOn(API, "getChatSystemPrompt").mockResolvedValue({ + spyOn(API.experimental, "getChatCostSummary").mockResolvedValue( + mockAnalyticsSummary, + ); + spyOn(API.experimental, "getChatCostUsers").mockResolvedValue( + mockUsageUsers, + ); + spyOn(API.experimental, "getChatSystemPrompt").mockResolvedValue({ system_prompt: "", }); - spyOn(API, "updateChatSystemPrompt").mockResolvedValue(); - spyOn(API, "getUserChatCustomPrompt").mockResolvedValue({ + spyOn(API.experimental, "updateChatSystemPrompt").mockResolvedValue(); + spyOn(API.experimental, "getUserChatCustomPrompt").mockResolvedValue({ custom_prompt: "", }); - spyOn(API, "updateUserChatCustomPrompt").mockResolvedValue({ + spyOn(API.experimental, "updateUserChatCustomPrompt").mockResolvedValue({ custom_prompt: "", }); // Mocks for child route pages that fetch their own data. - spyOn(API, "getChatModels").mockResolvedValue({ + spyOn(API.experimental, "getChatModels").mockResolvedValue({ providers: [ { provider: "openai", @@ -220,7 +224,7 @@ const meta: Meta = { }, ], }); - spyOn(API, "getChatModelConfigs").mockResolvedValue([ + spyOn(API.experimental, "getChatModelConfigs").mockResolvedValue([ { id: "config-openai-gpt-4o", provider: "openai", @@ -234,13 +238,13 @@ const meta: Meta = { updated_at: "2026-02-18T00:00:00.000Z", }, ]); - spyOn(API, "getChatDesktopEnabled").mockResolvedValue({ + spyOn(API.experimental, "getChatDesktopEnabled").mockResolvedValue({ enable_desktop: false, }); - spyOn(API, "getChatWorkspaceTTL").mockResolvedValue({ + spyOn(API.experimental, "getChatWorkspaceTTL").mockResolvedValue({ workspace_ttl_ms: 0, }); - spyOn(API, "updateChatWorkspaceTTL").mockResolvedValue(); + spyOn(API.experimental, "updateChatWorkspaceTTL").mockResolvedValue(); }, }; diff --git a/site/src/pages/AgentsPage/components/AgentDetailView.stories.tsx b/site/src/pages/AgentsPage/components/AgentDetailView.stories.tsx index 0ed29ba989..4ff735f99c 100644 --- a/site/src/pages/AgentsPage/components/AgentDetailView.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentDetailView.stories.tsx @@ -207,7 +207,7 @@ export const WithSidebarPanel: Story = { } satisfies ChatDiffStatus, }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ chat_id: AGENT_ID, diff: `diff --git a/src/main.ts b/src/main.ts index abc1234..def5678 100644 diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.stories.tsx b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.stories.tsx index d52652ac84..3b656ce79f 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/ChatModelAdminPanel.stories.tsx @@ -54,33 +54,39 @@ const setupChatSpies = (state: { modelConfigs: TypesGen.ChatModelConfig[]; modelCatalog: TypesGen.ChatModelsResponse; }) => { - spyOn(API, "getChatProviderConfigs").mockImplementation(async () => { - return state.providerConfigs; - }); - spyOn(API, "getChatModelConfigs").mockImplementation(async () => { - return state.modelConfigs; - }); - spyOn(API, "getChatModels").mockImplementation(async () => { + spyOn(API.experimental, "getChatProviderConfigs").mockImplementation( + async () => { + return state.providerConfigs; + }, + ); + spyOn(API.experimental, "getChatModelConfigs").mockImplementation( + async () => { + return state.modelConfigs; + }, + ); + spyOn(API.experimental, "getChatModels").mockImplementation(async () => { return state.modelCatalog; }); - spyOn(API, "createChatProviderConfig").mockImplementation(async (req) => { - const created = createProviderConfig({ - id: `provider-${Date.now()}`, - provider: req.provider, - display_name: req.display_name ?? "", - has_api_key: (req.api_key ?? "").trim().length > 0, - base_url: req.base_url ?? "", - source: "database", - }); - state.providerConfigs = [ - ...state.providerConfigs.filter((p) => p.provider !== req.provider), - created, - ]; - return created; - }); + spyOn(API.experimental, "createChatProviderConfig").mockImplementation( + async (req) => { + const created = createProviderConfig({ + id: `provider-${Date.now()}`, + provider: req.provider, + display_name: req.display_name ?? "", + has_api_key: (req.api_key ?? "").trim().length > 0, + base_url: req.base_url ?? "", + source: "database", + }); + state.providerConfigs = [ + ...state.providerConfigs.filter((p) => p.provider !== req.provider), + created, + ]; + return created; + }, + ); - spyOn(API, "updateChatProviderConfig").mockImplementation( + spyOn(API.experimental, "updateChatProviderConfig").mockImplementation( async (providerConfigId, req) => { const idx = state.providerConfigs.findIndex( (p) => p.id === providerConfigId, @@ -110,29 +116,31 @@ const setupChatSpies = (state: { }, ); - spyOn(API, "createChatModelConfig").mockImplementation(async (req) => { - const created = createModelConfig({ - id: `model-${state.modelConfigs.length + 1}`, - provider: req.provider, - model: req.model, - display_name: req.display_name || req.model, - context_limit: - typeof req.context_limit === "number" && - Number.isFinite(req.context_limit) - ? req.context_limit - : 200000, - compression_threshold: - typeof req.compression_threshold === "number" && - Number.isFinite(req.compression_threshold) - ? req.compression_threshold - : 70, - model_config: req.model_config, - }); - state.modelConfigs = [...state.modelConfigs, created]; - return created; - }); + spyOn(API.experimental, "createChatModelConfig").mockImplementation( + async (req) => { + const created = createModelConfig({ + id: `model-${state.modelConfigs.length + 1}`, + provider: req.provider, + model: req.model, + display_name: req.display_name || req.model, + context_limit: + typeof req.context_limit === "number" && + Number.isFinite(req.context_limit) + ? req.context_limit + : 200000, + compression_threshold: + typeof req.compression_threshold === "number" && + Number.isFinite(req.compression_threshold) + ? req.compression_threshold + : 70, + model_config: req.model_config, + }); + state.modelConfigs = [...state.modelConfigs, created]; + return created; + }, + ); - spyOn(API, "deleteChatModelConfig").mockImplementation( + spyOn(API.experimental, "deleteChatModelConfig").mockImplementation( async (modelConfigId) => { state.modelConfigs = state.modelConfigs.filter( (m) => m.id !== modelConfigId, @@ -141,8 +149,10 @@ const setupChatSpies = (state: { ); // Unused but mock to avoid errors. - spyOn(API, "deleteChatProviderConfig").mockResolvedValue(undefined); - spyOn(API, "updateChatModelConfig").mockResolvedValue( + spyOn(API.experimental, "deleteChatProviderConfig").mockResolvedValue( + undefined, + ); + spyOn(API.experimental, "updateChatModelConfig").mockResolvedValue( createModelConfig({ id: "stub", provider: "stub", @@ -311,9 +321,11 @@ export const CreateAndUpdateProvider: Story = { // The create spy should have been called. await waitFor(() => { - expect(API.createChatProviderConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createChatProviderConfig).toHaveBeenCalledTimes( + 1, + ); }); - expect(API.createChatProviderConfig).toHaveBeenCalledWith( + expect(API.experimental.createChatProviderConfig).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", api_key: "sk-provider-key", @@ -342,9 +354,11 @@ export const CreateAndUpdateProvider: Story = { await userEvent.click(body.getByRole("button", { name: "Save changes" })); await waitFor(() => { - expect(API.updateChatProviderConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.updateChatProviderConfig).toHaveBeenCalledTimes( + 1, + ); }); - expect(API.updateChatProviderConfig).toHaveBeenCalledWith( + expect(API.experimental.updateChatProviderConfig).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ api_key: "sk-updated-provider-key", @@ -413,9 +427,9 @@ export const NoModelConfigByDefault: Story = { // The submit button in ModelForm also says "Add model". await userEvent.click(body.getByRole("button", { name: "Add model" })); await waitFor(() => { - expect(API.createChatModelConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createChatModelConfig).toHaveBeenCalledTimes(1); }); - expect(API.createChatModelConfig).toHaveBeenCalledWith( + expect(API.experimental.createChatModelConfig).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", model: "gpt-5-pro", @@ -423,7 +437,9 @@ export const NoModelConfigByDefault: Story = { ); // Blank pricing fields should remain unset in the payload. const callArgs = ( - API.createChatModelConfig as unknown as ReturnType + API.experimental.createChatModelConfig as unknown as ReturnType< + typeof spyOn + > ).mock.calls[0][0] as Record; expect(callArgs).not.toHaveProperty("model_config"); }, @@ -472,9 +488,9 @@ export const SubmitModelConfigExplicitly: Story = { await userEvent.click(body.getByRole("button", { name: "Add model" })); await waitFor(() => { - expect(API.createChatModelConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createChatModelConfig).toHaveBeenCalledTimes(1); }); - expect(API.createChatModelConfig).toHaveBeenCalledWith( + expect(API.experimental.createChatModelConfig).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", model: "gpt-5-pro-custom", @@ -786,9 +802,11 @@ export const ModelDeleteConfirmed: Story = { // The delete API should have been called. await waitFor(() => { - expect(API.deleteChatModelConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.deleteChatModelConfig).toHaveBeenCalledTimes(1); }); - expect(API.deleteChatModelConfig).toHaveBeenCalledWith("model-1"); + expect(API.experimental.deleteChatModelConfig).toHaveBeenCalledWith( + "model-1", + ); }, }; @@ -898,9 +916,11 @@ export const ProviderDeleteConfirmed: Story = { // The delete API should have been called. await waitFor(() => { - expect(API.deleteChatProviderConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.deleteChatProviderConfig).toHaveBeenCalledTimes( + 1, + ); }); - expect(API.deleteChatProviderConfig).toHaveBeenCalledWith( + expect(API.experimental.deleteChatProviderConfig).toHaveBeenCalledWith( "provider-openai", ); }, @@ -940,6 +960,6 @@ export const ValidatesModelConfigFields: Story = { expect(body.getByRole("button", { name: "Add model" })).toBeDisabled(); }); // No API call should have been made. - expect(API.createChatModelConfig).not.toHaveBeenCalled(); + expect(API.experimental.createChatModelConfig).not.toHaveBeenCalled(); }, }; diff --git a/site/src/pages/AgentsPage/components/GitPanel/GitPanel.stories.tsx b/site/src/pages/AgentsPage/components/GitPanel/GitPanel.stories.tsx index e30ce3c096..0bf4a544fe 100644 --- a/site/src/pages/AgentsPage/components/GitPanel/GitPanel.stories.tsx +++ b/site/src/pages/AgentsPage/components/GitPanel/GitPanel.stories.tsx @@ -111,7 +111,9 @@ const meta: Meta = { ), ], beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue(defaultDiffContents); + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue( + defaultDiffContents, + ); }, }; @@ -130,7 +132,7 @@ export const PullRequestAndWorkingChanges: Story = { repositories: new Map([["/home/coder/coder", makeRepo()]]), }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); @@ -155,7 +157,7 @@ export const DraftPullRequest: Story = { ]), }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); @@ -177,7 +179,7 @@ export const MergedPullRequest: Story = { }), }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); @@ -199,7 +201,7 @@ export const ClosedPullRequest: Story = { }), }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); @@ -251,7 +253,7 @@ export const MultipleRepos: Story = { ]), }, beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); @@ -283,7 +285,7 @@ export const InlineCommentInput: Story = { ), ], beforeEach: () => { - spyOn(API, "getChatDiffContents").mockResolvedValue({ + spyOn(API.experimental, "getChatDiffContents").mockResolvedValue({ ...defaultDiffContents, diff: sampleDiff, }); diff --git a/site/src/pages/AgentsPage/components/MCPServerAdminPanel.stories.tsx b/site/src/pages/AgentsPage/components/MCPServerAdminPanel.stories.tsx index f6090a1a8c..4e8ec93134 100644 --- a/site/src/pages/AgentsPage/components/MCPServerAdminPanel.stories.tsx +++ b/site/src/pages/AgentsPage/components/MCPServerAdminPanel.stories.tsx @@ -44,59 +44,67 @@ const createServerConfig = ( * on refetch, mimicking a real server round-trip. */ const setupMCPSpies = (state: { servers: TypesGen.MCPServerConfig[] }) => { - spyOn(API, "getMCPServerConfigs").mockImplementation(async () => { - return state.servers; - }); + spyOn(API.experimental, "getMCPServerConfigs").mockImplementation( + async () => { + return state.servers; + }, + ); - spyOn(API, "createMCPServerConfig").mockImplementation(async (req) => { - const created = createServerConfig({ - id: `mcp-${Date.now()}`, - display_name: req.display_name, - slug: req.slug, - description: req.description, - icon_url: req.icon_url, - transport: req.transport, - url: req.url, - auth_type: req.auth_type, - availability: req.availability, - enabled: req.enabled, - has_oauth2_secret: (req.oauth2_client_secret ?? "").length > 0, - has_api_key: (req.api_key_value ?? "").length > 0, - has_custom_headers: - req.custom_headers != null && - Object.keys(req.custom_headers).length > 0, - tool_allow_list: req.tool_allow_list ?? [], - tool_deny_list: req.tool_deny_list ?? [], - }); - state.servers = [...state.servers, created]; - return created; - }); + spyOn(API.experimental, "createMCPServerConfig").mockImplementation( + async (req) => { + const created = createServerConfig({ + id: `mcp-${Date.now()}`, + display_name: req.display_name, + slug: req.slug, + description: req.description, + icon_url: req.icon_url, + transport: req.transport, + url: req.url, + auth_type: req.auth_type, + availability: req.availability, + enabled: req.enabled, + has_oauth2_secret: (req.oauth2_client_secret ?? "").length > 0, + has_api_key: (req.api_key_value ?? "").length > 0, + has_custom_headers: + req.custom_headers != null && + Object.keys(req.custom_headers).length > 0, + tool_allow_list: req.tool_allow_list ?? [], + tool_deny_list: req.tool_deny_list ?? [], + }); + state.servers = [...state.servers, created]; + return created; + }, + ); - spyOn(API, "updateMCPServerConfig").mockImplementation(async (id, req) => { - const idx = state.servers.findIndex((s) => s.id === id); - if (idx < 0) { - throw new Error("MCP server config not found."); - } - const current = state.servers[idx]; - const updated: TypesGen.MCPServerConfig = { - ...current, - display_name: req.display_name ?? current.display_name, - slug: req.slug ?? current.slug, - description: req.description ?? current.description, - url: req.url ?? current.url, - transport: req.transport ?? current.transport, - auth_type: req.auth_type ?? current.auth_type, - availability: req.availability ?? current.availability, - enabled: req.enabled ?? current.enabled, - updated_at: now, - }; - state.servers = state.servers.map((s, i) => (i === idx ? updated : s)); - return updated; - }); + spyOn(API.experimental, "updateMCPServerConfig").mockImplementation( + async (id, req) => { + const idx = state.servers.findIndex((s) => s.id === id); + if (idx < 0) { + throw new Error("MCP server config not found."); + } + const current = state.servers[idx]; + const updated: TypesGen.MCPServerConfig = { + ...current, + display_name: req.display_name ?? current.display_name, + slug: req.slug ?? current.slug, + description: req.description ?? current.description, + url: req.url ?? current.url, + transport: req.transport ?? current.transport, + auth_type: req.auth_type ?? current.auth_type, + availability: req.availability ?? current.availability, + enabled: req.enabled ?? current.enabled, + updated_at: now, + }; + state.servers = state.servers.map((s, i) => (i === idx ? updated : s)); + return updated; + }, + ); - spyOn(API, "deleteMCPServerConfig").mockImplementation(async (id) => { - state.servers = state.servers.filter((s) => s.id !== id); - }); + spyOn(API.experimental, "deleteMCPServerConfig").mockImplementation( + async (id) => { + state.servers = state.servers.filter((s) => s.id !== id); + }, + ); }; // ── Meta ─────────────────────────────────────────────────────── @@ -217,9 +225,9 @@ export const CreateServer: Story = { await userEvent.click(body.getByRole("button", { name: /Create server/i })); await waitFor(() => { - expect(API.createMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.createMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledWith( expect.objectContaining({ display_name: "Sentry", slug: "sentry", @@ -268,9 +276,9 @@ export const CreateServerOAuth2: Story = { await userEvent.click(body.getByRole("button", { name: /Create server/i })); await waitFor(() => { - expect(API.createMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.createMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledWith( expect.objectContaining({ auth_type: "oauth2", oauth2_client_id: "my-client-id", @@ -316,9 +324,9 @@ export const CreateServerAPIKey: Story = { await userEvent.click(body.getByRole("button", { name: /Create server/i })); await waitFor(() => { - expect(API.createMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.createMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledWith( expect.objectContaining({ auth_type: "api_key", api_key_header: "Authorization", @@ -369,9 +377,9 @@ export const EditServer: Story = { await userEvent.click(body.getByRole("button", { name: /Save changes/i })); await waitFor(() => { - expect(API.updateMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.updateMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.updateMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.updateMCPServerConfig).toHaveBeenCalledWith( "mcp-sentry", expect.objectContaining({ description: "Sentry error tracking integration", @@ -460,9 +468,9 @@ export const EditServerWithCustomHeaders: Story = { await userEvent.click(body.getByRole("button", { name: /Save changes/i })); await waitFor(() => { - expect(API.updateMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.updateMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.updateMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.updateMCPServerConfig).toHaveBeenCalledWith( "mcp-custom", expect.objectContaining({ custom_headers: { Authorization: "Bearer tok_abc" }, @@ -552,9 +560,11 @@ export const DeleteServerConfirmed: Story = { await userEvent.click(body.getByRole("button", { name: /Delete server/i })); await waitFor(() => { - expect(API.deleteMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.deleteMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.deleteMCPServerConfig).toHaveBeenCalledWith("mcp-sentry"); + expect(API.experimental.deleteMCPServerConfig).toHaveBeenCalledWith( + "mcp-sentry", + ); }, }; @@ -586,7 +596,7 @@ export const BackToList: Story = { await body.findByRole("button", { name: /Sentry/ }), ).toBeInTheDocument(); - expect(API.createMCPServerConfig).not.toHaveBeenCalled(); + expect(API.experimental.createMCPServerConfig).not.toHaveBeenCalled(); }, }; @@ -623,9 +633,9 @@ export const CreateServerWithToolGovernance: Story = { await userEvent.click(body.getByRole("button", { name: /Create server/i })); await waitFor(() => { - expect(API.createMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.createMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledWith( expect.objectContaining({ tool_allow_list: ["search", "read_file"], tool_deny_list: ["delete_file", "execute"], @@ -676,9 +686,9 @@ export const CustomHeadersAuthType: Story = { await userEvent.click(body.getByRole("button", { name: /Create server/i })); await waitFor(() => { - expect(API.createMCPServerConfig).toHaveBeenCalledTimes(1); + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledTimes(1); }); - expect(API.createMCPServerConfig).toHaveBeenCalledWith( + expect(API.experimental.createMCPServerConfig).toHaveBeenCalledWith( expect.objectContaining({ auth_type: "custom_headers", custom_headers: { "X-Api-Token": "secret-token-123" }, diff --git a/site/src/pages/AgentsPage/hooks/useFileAttachments.ts b/site/src/pages/AgentsPage/hooks/useFileAttachments.ts index fd730d5fe2..0f19ef9a65 100644 --- a/site/src/pages/AgentsPage/hooks/useFileAttachments.ts +++ b/site/src/pages/AgentsPage/hooks/useFileAttachments.ts @@ -57,7 +57,10 @@ export function useFileAttachments( setUploadStates((prev) => new Map(prev).set(file, { status: "uploading" })); void (async () => { try { - const result = await API.uploadChatFile(file, organizationId); + const result = await API.experimental.uploadChatFile( + file, + organizationId, + ); setUploadStates((prev) => new Map(prev).set(file, { status: "uploaded",