Compare commits

...

6 Commits

Author SHA1 Message Date
Danielle Maywood 2fff5a9f90 fix(site): handle delete errors in wrappers to prevent unhandled rejections
The async wrappers in ProvidersSection and ModelsSection await the
parent's mutateAsync and then navigate. If the delete fails, the
async function rejects and the leaf's onClick discards the Promise
without catching it, producing an unhandled rejection.

Add try/catch in the wrappers so the returned Promise never rejects.
On success: navigate to list. On failure: stay on the form (the
mutation's error state surfaces the error in the parent). The leaf
call sites remain plain sync calls with no error handling ceremony.
2026-03-19 12:02:01 +00:00
Danielle Maywood 66a2fb73b7 fix(site): simplify delete callbacks to plain mutate without onSuccess threading
Drop the onSuccess callback parameter from onDeleteModel and
onDeleteProvider. The wrappers in ModelsSection and ProvidersSection
now navigate optimistically after firing mutate — no callback
threading through the component tree.
2026-03-19 11:59:04 +00:00
Danielle Maywood be80dbe2db fix(site): fix duplicate agent messages from REST/stream race
When a REST refetch (window focus, stale time, invalidation) delivers
a finalized assistant message while the WebSocket stream is still
active, both the durable message list and the streaming output render
the same content, causing a visible duplicate.

The WebSocket 'message' handler calls scheduleStreamReset to clear
streamState after upserting a durable message. But when REST delivers
the message first, the subsequent WebSocket 'message' event returns
changed=false (already in store), so scheduleStreamReset is never
called and streamState persists indefinitely.

Fix: in the REST hydration useEffect, if any upserted message changed
the store while streamState is active, clear streamState immediately.
This ensures the streaming output is removed once the durable version
is available, regardless of whether the message arrived via REST or
WebSocket first.
2026-03-19 11:47:25 +00:00
Danielle Maywood b4b9cccbc2 fix(site): use mutate for fire-and-forget callbacks in ChatModelAdminPanel
Refactor delete and set-default callbacks to use mutate() instead
of mutateAsync(), eliminating the need for void, .catch(), or
async/try/catch ceremony at leaf call sites.

- ChatModelAdminPanel: Pass mutate() with onSuccess callback param
  for onDeleteProvider and onDeleteModel. Add new onSetDefaultModel
  prop using mutate() for the set-default action.
- ProvidersSection/ModelsSection: Wrappers pass setView as the
  onSuccess callback instead of awaiting a Promise.
- ProviderForm/ModelForm: Delete button handlers are now simple
  sync calls with no error handling ceremony — errors are surfaced
  via mutation state in the parent.
- ModelsSection: handleSetDefault uses the new onSetDefaultModel
  prop, a plain sync call.
2026-03-19 11:39:54 +00:00
Danielle Maywood 13a31c96c2 fix(site): address review feedback on React Query cleanup
- chats.ts: Drop 'satisfies UseInfiniteQueryOptions<Chat[]>' so
  TypeScript infers TPageParam = number from initialPageParam: 0.
  This eliminates the 'as number' casts on lastPageParam and
  pageParam.
- ModelForm.tsx, ProviderForm.tsx: Replace 'void mutateAsync().catch()'
  with async onClick handlers using try/catch for proper error
  handling.
- ModelsSection.tsx: Make handleSetDefault async with try/catch
  instead of void + .catch().
2026-03-19 11:39:54 +00:00
Danielle Maywood 70892c38e4 fix(site): correct React Query usage in AgentsPage
Fix multiple React Query anti-patterns and bugs in the AgentsPage
feature:

**chats.ts (query factory):**
- Fix broken JSDoc comment that swallowed docs for
  invalidateChatListQueries.
- Fix typeof null === 'object' passing the isChatListQuery predicate.
- Split isChatListQuery into two predicates: isChatListQuery (for
  invalidation, matches flat + infinite) and isInfiniteChatListQuery
  (for cache manipulation, infinite only). This prevents type
  mismatches when setQueriesData runs on the flat query.
- Deduplicate inline predicate copies in archiveChat/unarchiveChat
  onMutate handlers.
- Fix confusing pageParam sequence (0->2->3->4) by using offset as
  the page param directly. Now uses lastPageParam (3rd arg) per
  TanStack docs.
- Await cache invalidation in editChatMessage and createChat
  onSuccess handlers (were fire-and-forget via void).
- Remove redundant invalidateChatListQueries from onError in
  archiveChat/unarchiveChat (onSettled already handles it).
- Snapshot infinite cache in onMutate for proper synchronous
  rollback on error, eliminating stale-data flash.

**AgentDetail.tsx:**
- Replace void interruptMutation.mutateAsync() with mutate() to
  prevent unhandled promise rejections.

**AgentsPage.tsx:**
- Move archiveAndDeleteMutation cache invalidation to onSettled so
  it fires even on partial failure (archive succeeds, delete fails).
- Guard fetchNextPage behind !isFetching to prevent races with
  background refetches per TanStack docs.

**ChatModelAdminPanel (ProviderForm, ModelForm, ModelsSection):**
- Add .catch(() => {}) to void mutateAsync() calls in delete and
  set-default handlers to prevent unhandled promise rejections.

**SettingsPageContent.tsx:**
- Replace 'me' fallback with empty string in disabled query to avoid
  creating a phantom cache entry.

**InsightsContent.tsx:**
- Round timestamps to startOf('minute') so staleTime is effective
  across remounts.
2026-03-19 11:39:54 +00:00
12 changed files with 154 additions and 105 deletions
+11 -39
View File
@@ -239,7 +239,6 @@ describe("archiveChat optimistic update", () => {
const initialChats = [makeChat(chatId)];
seedInfiniteChats(queryClient, initialChats);
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
const mutation = archiveChat(queryClient);
const context = await mutation.onMutate(chatId);
@@ -247,13 +246,12 @@ describe("archiveChat optimistic update", () => {
// Verify the optimistic update took effect.
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(true);
// Simulate an error — the onError handler invalidates the
// cache so a re-fetch restores the correct state.
// Simulate an error — the onError handler restores the
// previous cache state synchronously.
mutation.onError(new Error("server error"), chatId, context);
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
);
// The infinite cache should be restored to the pre-mutation state.
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(false);
});
it("rolls back the individual chat cache on error", async () => {
@@ -279,7 +277,6 @@ describe("archiveChat optimistic update", () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedInfiniteChats(queryClient, [makeChat(chatId, { archived: true })]);
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
const mutation = archiveChat(queryClient);
@@ -287,11 +284,6 @@ describe("archiveChat optimistic update", () => {
expect(() => {
mutation.onError(new Error("fail"), chatId, undefined);
}).not.toThrow();
// The handler should still invalidate to trigger a refetch.
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
);
});
it("handles onMutate when no individual chat cache exists", async () => {
@@ -364,7 +356,6 @@ describe("unarchiveChat optimistic update", () => {
chatKey(chatId),
makeChat(chatId, { archived: true }),
);
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
const mutation = unarchiveChat(queryClient);
const context = await mutation.onMutate(chatId);
@@ -378,10 +369,8 @@ describe("unarchiveChat optimistic update", () => {
// Roll back.
mutation.onError(new Error("server error"), chatId, context);
// The chats list is rolled back via invalidation.
expect(invalidateSpy).toHaveBeenCalledWith(
expect.objectContaining({ queryKey: chatsKey }),
);
// The infinite cache is restored synchronously.
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(true);
// The individual chat cache is restored directly.
expect(
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
@@ -694,16 +683,16 @@ describe("infiniteChats", () => {
const lastPage = Array.from({ length: PAGE_LIMIT - 1 }, (_, i) =>
makeChat(`chat-${i}`),
);
expect(getNextPageParam(lastPage, [lastPage])).toBeUndefined();
expect(getNextPageParam(lastPage, [lastPage], 0)).toBeUndefined();
});
it("returns pages.length + 1 when lastPage has exactly the limit", () => {
it("returns lastPageParam + limit when lastPage has exactly the limit", () => {
const { getNextPageParam } = infiniteChats();
const lastPage = Array.from({ length: PAGE_LIMIT }, (_, i) =>
makeChat(`chat-${i}`),
);
const pages = [lastPage];
expect(getNextPageParam(lastPage, pages)).toBe(pages.length + 1);
expect(getNextPageParam(lastPage, pages, 0)).toBe(PAGE_LIMIT);
});
});
@@ -718,39 +707,22 @@ describe("infiniteChats", () => {
});
});
it("computes offset 0 for pageParam <= 0", async () => {
vi.mocked(API.getChats).mockResolvedValue([]);
const { queryFn } = infiniteChats();
await queryFn({ pageParam: -1 });
expect(API.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: 0,
});
});
it("computes correct offset for subsequent pages", async () => {
vi.mocked(API.getChats).mockResolvedValue([]);
const { queryFn } = infiniteChats();
await queryFn({ pageParam: 2 });
await queryFn({ pageParam: PAGE_LIMIT });
expect(API.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: PAGE_LIMIT,
});
await queryFn({ pageParam: 3 });
await queryFn({ pageParam: PAGE_LIMIT * 2 });
expect(API.getChats).toHaveBeenCalledWith({
limit: PAGE_LIMIT,
offset: PAGE_LIMIT * 2,
});
});
it("throws when pageParam is not a number", () => {
const { queryFn } = infiniteChats();
expect(() => queryFn({ pageParam: "bad" })).toThrow(
"pageParam must be a number",
);
});
});
});
+83 -44
View File
@@ -1,6 +1,6 @@
import { API } from "api/api";
import type * as TypesGen from "api/typesGenerated";
import type { QueryClient, UseInfiniteQueryOptions } from "react-query";
import type { QueryClient } from "react-query";
export const chatsKey = ["chats"] as const;
export const chatKey = (chatId: string) => ["chats", chatId] as const;
@@ -20,7 +20,7 @@ export const updateInfiniteChatsCache = (
queryClient.setQueriesData<{
pages: TypesGen.Chat[][];
pageParams: unknown[];
}>({ queryKey: chatsKey, predicate: isChatListQuery }, (prev) => {
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery }, (prev) => {
if (!prev) return prev;
if (!prev.pages) return prev;
const nextPages = prev.pages.map((page) => updater(page));
@@ -44,7 +44,7 @@ export const prependToInfiniteChatsCache = (
queryClient.setQueriesData<{
pages: TypesGen.Chat[][];
pageParams: unknown[];
}>({ queryKey: chatsKey, predicate: isChatListQuery }, (prev) => {
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery }, (prev) => {
if (!prev?.pages) return prev;
// Check across ALL pages to avoid duplicates.
const exists = prev.pages.some((page) =>
@@ -69,7 +69,7 @@ export const readInfiniteChatsCache = (
const queries = queryClient.getQueriesData<{
pages: TypesGen.Chat[][];
pageParams: unknown[];
}>({ queryKey: chatsKey, predicate: isChatListQuery });
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
for (const [, data] of queries) {
if (data?.pages) {
return data.pages.flat();
@@ -79,24 +79,46 @@ export const readInfiniteChatsCache = (
};
/**
* Invalidate only the sidebar chat-list queries (flat + infinite)
/**
* Predicate that matches only chat-list queries (the sidebar), not
* per-chat queries (detail, messages, diffs, cost).
* Predicate that matches only infinite chat-list queries, excluding
* the flat ["chats"] query. Used for direct cache manipulation
* (setQueriesData) where the data structure must be { pages,
* pageParams }.
*
* Sidebar keys look like ["chats"] or ["chats", <object|undefined>].
* Infinite keys look like ["chats", <object | undefined>].
* Per-chat keys look like ["chats", <string-id>, ...].
*/
const isInfiniteChatListQuery = (query: {
queryKey: readonly unknown[];
}): boolean => {
const key = query.queryKey;
// Exclude: ["chats"] (flat list) — its data is Chat[], not
// { pages, pageParams }, so cache helpers must not touch it.
if (key.length <= 1) return false;
// Match: ["chats", <object | undefined>] (infinite query
// with optional filter opts like {archived, q}).
const segment = key[1];
return (
segment === undefined || (segment !== null && typeof segment === "object")
);
};
/**
* Predicate that matches both flat and infinite chat-list queries
* (the sidebar). Used for invalidation where we want to refresh
* all list views.
*/
const isChatListQuery = (query: { queryKey: readonly unknown[] }): boolean => {
const key = query.queryKey;
// Match: ["chats"] (flat list).
if (key.length <= 1) return true;
// Match: ["chats", <object | undefined>] (infinite query
// with optional filter opts like {archived, q}).
const segment = key[1];
return segment === undefined || typeof segment === "object";
// Delegate to the infinite predicate for longer keys.
return isInfiniteChatListQuery(query);
};
/**
* Invalidate only the sidebar chat-list queries (flat + infinite).
*/
export const invalidateChatListQueries = (queryClient: QueryClient) => {
return queryClient.invalidateQueries({
queryKey: chatsKey,
@@ -121,25 +143,26 @@ export const infiniteChats = (opts?: { q?: string; archived?: boolean }) => {
return {
queryKey: [...chatsKey, opts],
getNextPageParam: (lastPage: TypesGen.Chat[], pages: TypesGen.Chat[][]) => {
getNextPageParam: (
lastPage: TypesGen.Chat[],
_allPages: TypesGen.Chat[][],
lastPageParam: number,
) => {
if (lastPage.length < limit) {
return undefined;
}
return pages.length + 1;
return lastPageParam + limit;
},
initialPageParam: 0,
queryFn: ({ pageParam }: { pageParam: unknown }) => {
if (typeof pageParam !== "number") {
throw new Error("pageParam must be a number");
}
queryFn: ({ pageParam }: { pageParam: number }) => {
return API.getChats({
limit,
offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit,
offset: pageParam,
q,
});
},
refetchOnWindowFocus: true as const,
} satisfies UseInfiniteQueryOptions<TypesGen.Chat[]>;
};
};
export const chats = () => ({
@@ -179,12 +202,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
onMutate: async (chatId: string) => {
await queryClient.cancelQueries({
queryKey: chatsKey,
predicate: (query) => {
const key = query.queryKey;
if (key.length <= 1) return true;
const segment = key[1];
return segment === undefined || typeof segment === "object";
},
predicate: isChatListQuery,
});
await queryClient.cancelQueries({
queryKey: chatKey(chatId),
@@ -193,6 +211,12 @@ export const archiveChat = (queryClient: QueryClient) => ({
const previousChat = queryClient.getQueryData<TypesGen.Chat>(
chatKey(chatId),
);
// Snapshot infinite cache before optimistic update so we
// can restore it on error without a flash of stale data.
const previousInfiniteData = queryClient.getQueriesData<{
pages: TypesGen.Chat[][];
pageParams: unknown[];
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
updateInfiniteChatsCache(queryClient, (chats) =>
chats.map((chat) =>
chat.id === chatId ? { ...chat, archived: true } : chat,
@@ -204,7 +228,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
archived: true,
});
}
return { previousChat };
return { previousChat, previousInfiniteData };
},
onError: (
_error: unknown,
@@ -212,11 +236,18 @@ export const archiveChat = (queryClient: QueryClient) => ({
context:
| {
previousChat?: TypesGen.Chat;
previousInfiniteData?: [
readonly unknown[],
{ pages: TypesGen.Chat[][]; pageParams: unknown[] } | undefined,
][];
}
| undefined,
) => {
// Rollback: invalidate to re-fetch the correct state.
void invalidateChatListQueries(queryClient);
// Rollback: restore both caches synchronously. The
// onSettled handler will refetch for server consistency.
for (const [key, data] of context?.previousInfiniteData ?? []) {
queryClient.setQueryData(key, data);
}
if (context?.previousChat) {
queryClient.setQueryData<TypesGen.Chat>(
chatKey(chatId),
@@ -238,12 +269,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
onMutate: async (chatId: string) => {
await queryClient.cancelQueries({
queryKey: chatsKey,
predicate: (query) => {
const key = query.queryKey;
if (key.length <= 1) return true;
const segment = key[1];
return segment === undefined || typeof segment === "object";
},
predicate: isChatListQuery,
});
await queryClient.cancelQueries({
queryKey: chatKey(chatId),
@@ -252,6 +278,12 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
const previousChat = queryClient.getQueryData<TypesGen.Chat>(
chatKey(chatId),
);
// Snapshot infinite cache before optimistic update so we
// can restore it on error without a flash of stale data.
const previousInfiniteData = queryClient.getQueriesData<{
pages: TypesGen.Chat[][];
pageParams: unknown[];
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
updateInfiniteChatsCache(queryClient, (chats) =>
chats.map((chat) =>
chat.id === chatId ? { ...chat, archived: false } : chat,
@@ -263,7 +295,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
archived: false,
});
}
return { previousChat };
return { previousChat, previousInfiniteData };
},
onError: (
_error: unknown,
@@ -271,11 +303,18 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
context:
| {
previousChat?: TypesGen.Chat;
previousInfiniteData?: [
readonly unknown[],
{ pages: TypesGen.Chat[][]; pageParams: unknown[] } | undefined,
][];
}
| undefined,
) => {
// Rollback: invalidate to re-fetch the correct state.
void invalidateChatListQueries(queryClient);
// Rollback: restore both caches synchronously. The
// onSettled handler will refetch for server consistency.
for (const [key, data] of context?.previousInfiniteData ?? []) {
queryClient.setQueryData(key, data);
}
if (context?.previousChat) {
queryClient.setQueryData<TypesGen.Chat>(
chatKey(chatId),
@@ -294,8 +333,8 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
export const createChat = (queryClient: QueryClient) => ({
mutationFn: (req: TypesGen.CreateChatRequest) => API.createChat(req),
onSuccess: () => {
void invalidateChatListQueries(queryClient);
onSuccess: async () => {
await invalidateChatListQueries(queryClient);
},
});
@@ -318,17 +357,17 @@ type EditChatMessageMutationArgs = {
export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({
mutationFn: ({ messageId, req }: EditChatMessageMutationArgs) =>
API.editChatMessage(chatId, messageId, req),
onSuccess: () => {
onSuccess: async () => {
// Editing truncates all messages after the edited one on the
// server. The WebSocket can insert/update messages but cannot
// remove stale ones, so a full messages refetch is required.
// Use exact matching to avoid cascading to unrelated queries
// (diff-status, diff-contents, cost summaries, etc.).
void queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: chatKey(chatId),
exact: true,
});
void queryClient.invalidateQueries({
await queryClient.invalidateQueries({
queryKey: chatMessagesKey(chatId),
exact: true,
});
+1 -1
View File
@@ -675,7 +675,7 @@ const AgentDetail: FC = () => {
if (!agentId || interruptMutation.isPending) {
return;
}
void interruptMutation.mutateAsync();
interruptMutation.mutate();
};
const handleDeleteQueuedMessage = useCallback(
@@ -563,8 +563,19 @@ export const useChatStore = (
if (hasStaleEntries) {
store.replaceMessages(chatMessages);
} else {
let anyChanged = false;
for (const message of chatMessages) {
store.upsertDurableMessage(message);
const { changed } = store.upsertDurableMessage(message);
if (changed) {
anyChanged = true;
}
}
// If a REST refetch delivered a new or updated message
// while streaming is still active, clear stream state
// to avoid showing the same content in both the durable
// message list and the streaming output.
if (anyChanged && storeSnap.streamState !== null) {
store.clearStreamState();
}
}
}
+17 -7
View File
@@ -155,17 +155,23 @@ const AgentsPage: FC = () => {
await API.deleteWorkspace(workspaceId);
return { chatId, workspaceId };
},
onSuccess: async ({ chatId }) => {
onSuccess: ({ chatId }) => {
clearChatErrorReason(chatId);
await invalidateChatListQueries(queryClient);
await queryClient.invalidateQueries({
queryKey: chatKey(chatId),
exact: true,
});
},
onError: (error) => {
toast.error(getErrorMessage(error, "Failed to archive agent."));
},
onSettled: async (_data, _error, variables) => {
// Always invalidate — even on partial failure the chat
// may have been archived server-side.
if (variables) {
await invalidateChatListQueries(queryClient);
await queryClient.invalidateQueries({
queryKey: chatKey(variables.chatId),
exact: true,
});
}
},
});
const unarchiveChatBase = unarchiveChat(queryClient);
const unarchiveAgentMutation = useMutation({
@@ -503,7 +509,11 @@ const AgentsPage: FC = () => {
modelCatalogError={chatModelsQuery.error}
isAgentsAdmin={isAgentsAdmin}
hasNextPage={chatsQuery.hasNextPage}
onLoadMore={() => void chatsQuery.fetchNextPage()}
onLoadMore={() => {
if (!chatsQuery.isFetching) {
void chatsQuery.fetchNextPage();
}
}}
isFetchingNextPage={chatsQuery.isFetchingNextPage}
archivedFilter={archivedFilter}
onArchivedFilterChange={setArchivedFilter}
@@ -352,6 +352,12 @@ export const ChatModelAdminPanel: FC<ChatModelAdminPanelProps> = ({
})
}
onDeleteModel={(id) => deleteModelMut.mutateAsync(id)}
onSetDefaultModel={(modelConfigId) =>
updateModelMut.mutate({
modelConfigId,
req: { is_default: true },
})
}
/>
)}
</div>
@@ -76,7 +76,7 @@ interface ModelFormProps {
req: TypesGen.UpdateChatModelConfigRequest,
) => Promise<unknown>;
onCancel: () => void;
onDeleteModel?: (modelConfigId: string) => Promise<void>;
onDeleteModel?: (modelConfigId: string) => void;
}
export const ModelForm: FC<ModelFormProps> = ({
@@ -528,7 +528,7 @@ export const ModelForm: FC<ModelFormProps> = ({
size="lg"
type="button"
disabled={isDeleting}
onClick={() => void onDeleteModel(editingModel.id)}
onClick={() => onDeleteModel(editingModel.id)}
>
{isDeleting && <Spinner className="h-4 w-4" loading />}
Delete model
@@ -52,7 +52,8 @@ interface ModelsSectionProps {
modelConfigId: string,
req: TypesGen.UpdateChatModelConfigRequest,
) => Promise<unknown>;
onDeleteModel: (modelConfigId: string) => Promise<void>;
onDeleteModel: (modelConfigId: string) => Promise<unknown>;
onSetDefaultModel: (modelConfigId: string) => void;
}
export const ModelsSection: FC<ModelsSectionProps> = ({
@@ -71,6 +72,7 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
onCreateModel,
onUpdateModel,
onDeleteModel,
onSetDefaultModel,
}) => {
const [view, setView] = useState<ModelView>({ mode: "list" });
@@ -116,8 +118,13 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
onDeleteModel={
editingModel
? async (id) => {
await onDeleteModel(id);
setView({ mode: "list" });
try {
await onDeleteModel(id);
setView({ mode: "list" });
} catch {
// Error is surfaced via mutation state
// in the parent.
}
}
: undefined
}
@@ -161,7 +168,7 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
const handleSetDefault = (modelConfig: TypesGen.ChatModelConfig) => {
if (modelConfig.is_default) return;
void onUpdateModel(modelConfig.id, { is_default: true });
onSetDefaultModel(modelConfig.id);
};
return (
@@ -31,7 +31,7 @@ interface ProviderFormProps {
providerConfigId: string,
req: TypesGen.UpdateChatProviderConfigRequest,
) => Promise<unknown>;
onDeleteProvider: (providerConfigId: string) => Promise<void>;
onDeleteProvider: (providerConfigId: string) => void;
onBack: () => void;
}
@@ -273,7 +273,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
size="lg"
type="button"
disabled={isProviderMutationPending}
onClick={() => void onDeleteProvider(providerConfig.id)}
onClick={() => onDeleteProvider(providerConfig.id)}
>
{isProviderMutationPending && (
<Spinner className="h-4 w-4" loading />
@@ -23,7 +23,7 @@ interface ProvidersSectionProps {
providerConfigId: string,
req: TypesGen.UpdateChatProviderConfigRequest,
) => Promise<unknown>;
onDeleteProvider: (providerConfigId: string) => Promise<void>;
onDeleteProvider: (providerConfigId: string) => Promise<unknown>;
onSelectedProviderChange: (provider: string) => void;
}
@@ -61,8 +61,12 @@ export const ProvidersSection: FC<ProvidersSectionProps> = ({
onCreateProvider={onCreateProvider}
onUpdateProvider={onUpdateProvider}
onDeleteProvider={async (id) => {
await onDeleteProvider(id);
setView({ mode: "list" });
try {
await onDeleteProvider(id);
setView({ mode: "list" });
} catch {
// Error is surfaced via mutation state in the parent.
}
}}
onBack={() => setView({ mode: "list" })}
/>
@@ -6,7 +6,7 @@ import { useQuery } from "react-query";
import { type PRInsightsTimeRange, PRInsightsView } from "./PRInsightsView";
function timeRangeToDates(range: PRInsightsTimeRange) {
const end = dayjs();
const end = dayjs().startOf("minute");
const days = Number.parseInt(range, 10);
const start = end.subtract(days, "day");
return {
@@ -148,7 +148,7 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
placeholderData: keepPreviousData,
});
const summaryQuery = useQuery({
...chatCostSummary(selectedUser?.user_id ?? "me", {
...chatCostSummary(selectedUser?.user_id ?? "", {
start_date: dateRange.startDate,
end_date: dateRange.endDate,
}),