Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion site/.knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@
"./test/**/*.ts",
"./e2e/**/*.ts"
],
"ignore": ["**/*Generated.ts", "src/api/chatModelOptions.ts"],
"ignore": [
"**/*Generated.ts",
"src/api/chatModelOptions.ts",
// TODO(devtools): debugPanelUtils.ts is staged in PR 7; its exports are
// consumed by the Debug panel components in PRs 8 and 9. Remove this
// exclusion once the panel components land.
"src/pages/AgentsPage/components/RightPanel/DebugPanel/debugPanelUtils.ts",
// TODO(devtools): chatDebugLogging.ts queries are staged in PR 7;
// they are consumed by the Debug settings UI in PR 8. Remove this
// exclusion once the settings page lands.
"src/api/queries/chatDebugLogging.ts"
],
"ignoreBinaries": ["protoc"],
"ignoreDependencies": [
"@babel/plugin-syntax-typescript",
Expand Down
51 changes: 51 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3301,6 +3301,57 @@ class ExperimentalApiMethods {
);
};

getChatDebugLogging =
async (): Promise<TypesGen.ChatDebugLoggingAdminSettings> => {
const response =
await this.axios.get<TypesGen.ChatDebugLoggingAdminSettings>(
"/api/experimental/chats/config/debug-logging",
);
return response.data;
};

updateChatDebugLogging = async (
req: TypesGen.UpdateChatDebugLoggingAllowUsersRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/debug-logging", req);
};

getUserChatDebugLogging =
async (): Promise<TypesGen.UserChatDebugLoggingSettings> => {
const response =
await this.axios.get<TypesGen.UserChatDebugLoggingSettings>(
"/api/experimental/chats/config/user-debug-logging",
);
return response.data;
};

updateUserChatDebugLogging = async (
req: TypesGen.UpdateUserChatDebugLoggingRequest,
): Promise<void> => {
await this.axios.put(
"/api/experimental/chats/config/user-debug-logging",
req,
);
};

getChatDebugRuns = async (
chatId: string,
): Promise<TypesGen.ChatDebugRunSummary[]> => {
const response = await this.axios.get<TypesGen.ChatDebugRunSummary[]>(
`/api/experimental/chats/${chatId}/debug/runs`,
Comment thread
ThomasK33 marked this conversation as resolved.
Comment thread
ThomasK33 marked this conversation as resolved.
Comment thread
ThomasK33 marked this conversation as resolved.
);
return response.data;
};

getChatDebugRun = async (
chatId: string,
runId: string,
): Promise<TypesGen.ChatDebugRun> => {
const response = await this.axios.get<TypesGen.ChatDebugRun>(
`/api/experimental/chats/${chatId}/debug/runs/${runId}`,
);
return response.data;
};
getChatDesktopEnabled =
async (): Promise<TypesGen.ChatDesktopEnabledResponse> => {
const response =
Expand Down
36 changes: 36 additions & 0 deletions site/src/api/queries/chatDebugLogging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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,
});
},
});
73 changes: 56 additions & 17 deletions site/src/api/queries/chats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
cancelChatListRefetches,
chatCostSummary,
chatCostSummaryKey,
chatDebugRunsKey,
chatDiffContentsKey,
chatKey,
chatMessagesKey,
Expand Down Expand Up @@ -737,6 +738,8 @@ describe("mutation invalidation scope", () => {
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
// Messages: ["chats", chatId, "messages"]
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: [] });
// Cost summary: ["chats", "costSummary", "me", undefined]
Expand All @@ -758,13 +761,9 @@ describe("mutation invalidation scope", () => {
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);

// createChatMessage has no onSuccess handler — the WebSocket
// stream covers all real-time updates. Verify that constructing
// the mutation config does not define one.
const mutation = createChatMessage(queryClient, chatId);
expect(mutation).not.toHaveProperty("onSuccess");
await mutation.onSuccess?.();

// Since there is no onSuccess, no queries should be invalidated.
for (const { label, key } of unrelatedKeys(chatId)) {
const state = queryClient.getQueryState(key);
expect(
Expand All @@ -774,14 +773,18 @@ describe("mutation invalidation scope", () => {
}
});

it("createChatMessage does not invalidate chat detail or messages (WebSocket handles these)", async () => {
it("createChatMessage invalidates only debug runs, not chat detail or messages", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);

// No onSuccess handler exists.
const mutation = createChatMessage(queryClient, chatId);
expect(mutation).not.toHaveProperty("onSuccess");
await mutation.onSuccess?.();

expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);

const chatState = queryClient.getQueryState(chatKey(chatId));
expect(
Expand Down Expand Up @@ -815,7 +818,7 @@ describe("mutation invalidation scope", () => {
}
});

it("editChatMessage invalidates only chat detail and messages", async () => {
it("editChatMessage invalidates chat detail, messages, and debug runs", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);
Expand All @@ -825,8 +828,9 @@ describe("mutation invalidation scope", () => {

await new Promise((r) => setTimeout(r, 0));

// These two should still be invalidated — editing changes
// message content and potentially the chat's updated_at.
// These queries should be invalidated -- editing changes
// message content, may update the chat record, and can start
// a new debug run.
const chatState = queryClient.getQueryState(chatKey(chatId));
expect(chatState?.isInvalidated, "chatKey should be invalidated").toBe(
true,
Expand All @@ -837,6 +841,11 @@ describe("mutation invalidation scope", () => {
messagesState?.isInvalidated,
"chatMessagesKey should be invalidated",
).toBe(true);

expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);
});

// Shared type for the infinite messages cache shape used by
Expand Down Expand Up @@ -1170,15 +1179,18 @@ describe("mutation invalidation scope", () => {
);
});

it("interruptChat does not invalidate unrelated queries", async () => {
it("interruptChat invalidates debug runs without touching unrelated queries", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);

// interruptChat has no onSuccess handler — the WebSocket
// delivers status changes in real-time.
const mutation = interruptChat(queryClient, chatId);
expect(mutation).not.toHaveProperty("onSuccess");
await mutation.onSuccess?.();

expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);

for (const { label, key } of unrelatedKeys(chatId)) {
const state = queryClient.getQueryState(key);
Expand All @@ -1189,13 +1201,18 @@ describe("mutation invalidation scope", () => {
}
});

it("promoteChatQueuedMessage does not invalidate unrelated queries", async () => {
it("promoteChatQueuedMessage invalidates debug runs without touching unrelated queries", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);

const mutation = promoteChatQueuedMessage(queryClient, chatId);
expect(mutation).not.toHaveProperty("onSuccess");
await mutation.onSuccess?.();

expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);

for (const { label, key } of unrelatedKeys(chatId)) {
const state = queryClient.getQueryState(key);
Expand All @@ -1206,6 +1223,28 @@ describe("mutation invalidation scope", () => {
}
});

it("regenerateChatTitle invalidates debug runs so the title_generation run surfaces immediately", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
seedAllActiveQueries(queryClient, chatId);

const mutation = regenerateChatTitle(queryClient);
await mutation.onSettled(undefined, undefined, chatId);

expect(
queryClient.getQueryState(chatDebugRunsKey(chatId))?.isInvalidated,
"chatDebugRunsKey should be invalidated",
).toBe(true);

for (const { label, key } of unrelatedKeys(chatId)) {
const state = queryClient.getQueryState(key);
expect(
state?.isInvalidated,
`${label} should NOT be invalidated by regenerateChatTitle`,
).not.toBe(true);
}
});

it("createChat invalidates only sidebar queries on success", async () => {
const queryClient = createTestQueryClient();
const chatId = "chat-1";
Expand Down
37 changes: 25 additions & 12 deletions site/src/api/queries/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ export const regenerateChatTitle = (queryClient: QueryClient) => ({
queryKey: chatKey(chatId),
exact: true,
});
void invalidateChatDebugRuns(queryClient, chatId);
},
});

Expand Down Expand Up @@ -858,6 +859,15 @@ export const updateChatTitle = (queryClient: QueryClient) => ({
},
});

export const chatDebugRunsKey = (chatId: string) =>
["chats", chatId, "debug-runs"] as const;

const invalidateChatDebugRuns = (queryClient: QueryClient, chatId: string) => {
return queryClient.invalidateQueries({
queryKey: chatDebugRunsKey(chatId),
});
};

export const createChat = (queryClient: QueryClient) => ({
mutationFn: (req: TypesGen.CreateChatRequest) =>
API.experimental.createChat(req),
Expand All @@ -870,14 +880,14 @@ export const createChat = (queryClient: QueryClient) => ({
});

export const createChatMessage = (
_queryClient: QueryClient,
queryClient: QueryClient,
chatId: string,
) => ({
mutationFn: (req: CreateChatMessageRequestWithClearablePlanMode) =>
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.
onSuccess: () => {
void invalidateChatDebugRuns(queryClient, chatId);
Comment thread
ThomasK33 marked this conversation as resolved.
},
});

type EditChatMessageMutationArgs = {
Expand Down Expand Up @@ -961,14 +971,15 @@ export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({
queryKey: chatMessagesKey(chatId),
exact: true,
});
void invalidateChatDebugRuns(queryClient, chatId);
},
});

export const interruptChat = (_queryClient: QueryClient, chatId: string) => ({
export const interruptChat = (queryClient: QueryClient, chatId: string) => ({
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.
onSuccess: () => {
void invalidateChatDebugRuns(queryClient, chatId);
},
});

export const deleteChatQueuedMessage = (
Expand All @@ -990,14 +1001,14 @@ export const deleteChatQueuedMessage = (
});

export const promoteChatQueuedMessage = (
_queryClient: QueryClient,
queryClient: QueryClient,
chatId: string,
) => ({
mutationFn: (queuedMessageId: number) =>
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.
onSuccess: () => {
void invalidateChatDebugRuns(queryClient, chatId);
},
});

export const chatDiffContentsKey = (chatId: string) =>
Expand Down Expand Up @@ -1075,6 +1086,8 @@ export const updateChatDesktopEnabled = (queryClient: QueryClient) => ({
},
});

export * from "./chatDebugLogging";

const chatWorkspaceTTLKey = ["chat-workspace-ttl"] as const;

export const chatWorkspaceTTL = () => ({
Expand Down
Loading
Loading