Skip to content

Commit fe41cbe

Browse files
feat(site/src): reintroduce chat search cache invalidation
1 parent f96671d commit fe41cbe

5 files changed

Lines changed: 151 additions & 0 deletions

File tree

site/src/api/queries/chats.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
invalidateChatListQueries,
4747
invalidateChatMessages,
4848
invalidateChatPrompts,
49+
invalidateChatSearches,
4950
invalidateChatsByWorkspace,
5051
mergeWatchedChatIntoCaches,
5152
mergeWatchedChatSummary,
@@ -945,6 +946,7 @@ describe("mutation invalidation scope", () => {
945946
const queryClient = createTestQueryClient();
946947
const chatId = "chat-1";
947948
seedAllActiveQueries(queryClient, chatId);
949+
queryClient.setQueryData(chatSearch({ q: "alpha" }).queryKey, []);
948950

949951
const mutation = createChatMessage(queryClient, chatId);
950952
await mutation.onSuccess?.();
@@ -956,6 +958,14 @@ describe("mutation invalidation scope", () => {
956958
`${label} should NOT be invalidated by createChatMessage`,
957959
).not.toBe(true);
958960
}
961+
// The send path invalidates searches through
962+
// useChatStore.upsertCacheMessages; doing it here too would
963+
// double-invalidate every send.
964+
expect(
965+
queryClient.getQueryState(chatSearch({ q: "alpha" }).queryKey)
966+
?.isInvalidated,
967+
"chat searches should NOT be invalidated by createChatMessage",
968+
).not.toBe(true);
959969
});
960970

961971
it("createChatMessage invalidates debug runs and chat detail, not messages", async () => {
@@ -1550,6 +1560,51 @@ describe("mutation invalidation scope", () => {
15501560
"chat list should NOT be invalidated",
15511561
).not.toBe(true);
15521562
});
1563+
1564+
it.each<{
1565+
name: string;
1566+
settle: (queryClient: QueryClient) => unknown;
1567+
}>([
1568+
{
1569+
name: "archiveChat onSettled",
1570+
settle: (queryClient) =>
1571+
archiveChat(queryClient).onSettled(undefined, undefined, "chat-1"),
1572+
},
1573+
{
1574+
name: "unarchiveChat onSettled",
1575+
settle: (queryClient) =>
1576+
unarchiveChat(queryClient).onSettled(undefined, undefined, "chat-1"),
1577+
},
1578+
{
1579+
name: "updateChatTitle onSettled",
1580+
settle: (queryClient) =>
1581+
updateChatTitle(queryClient).onSettled(undefined, undefined, {
1582+
chatId: "chat-1",
1583+
title: "New",
1584+
}),
1585+
},
1586+
{
1587+
name: "editChatMessage onSettled",
1588+
settle: (queryClient) =>
1589+
editChatMessage(queryClient, "chat-1").onSettled(),
1590+
},
1591+
{
1592+
name: "createChat onSuccess",
1593+
settle: (queryClient) => createChat(queryClient).onSuccess(),
1594+
},
1595+
])("$name invalidates chat searches", async ({ settle }) => {
1596+
const queryClient = createTestQueryClient();
1597+
queryClient.setQueryData(chatSearch({ q: "alpha" }).queryKey, []);
1598+
1599+
settle(queryClient);
1600+
await new Promise((r) => setTimeout(r, 0));
1601+
1602+
expect(
1603+
queryClient.getQueryState(chatSearch({ q: "alpha" }).queryKey)
1604+
?.isInvalidated,
1605+
"chat search entry should be invalidated",
1606+
).toBe(true);
1607+
});
15531608
});
15541609

15551610
describe("chatListKey shape", () => {
@@ -3057,6 +3112,40 @@ describe("semantic cache operations: prefix invalidations", () => {
30573112
"messages entry should NOT be invalidated",
30583113
).not.toBe(true);
30593114
});
3115+
3116+
it("invalidateChatSearches touches every search entry and nothing outside the family", async () => {
3117+
const queryClient = createTestQueryClient();
3118+
queryClient.setQueryData(chatSearch({ q: "alpha" }).queryKey, []);
3119+
queryClient.setQueryData(chatSearch({ q: "beta" }).queryKey, []);
3120+
seedInfiniteChats(queryClient, [makeChat("chat-1")]);
3121+
queryClient.setQueryData(chatsByWorkspace(["ws-1"]).queryKey, {});
3122+
queryClient.setQueryData(chatEntityKey("chat-1"), makeChat("chat-1"));
3123+
queryClient.setQueryData(chatMessagesKey("chat-1"), []);
3124+
queryClient.setQueryData(chatCostTreeKey("chat-1"), {});
3125+
3126+
await invalidateChatSearches(queryClient);
3127+
3128+
expect(
3129+
queryClient.getQueryState(chatSearch({ q: "alpha" }).queryKey)
3130+
?.isInvalidated,
3131+
).toBe(true);
3132+
expect(
3133+
queryClient.getQueryState(chatSearch({ q: "beta" }).queryKey)
3134+
?.isInvalidated,
3135+
).toBe(true);
3136+
for (const [label, key] of [
3137+
["chat list", infiniteChatsTestKey],
3138+
["by-workspace", chatsByWorkspace(["ws-1"]).queryKey],
3139+
["chat detail", chatEntityKey("chat-1")],
3140+
["messages", chatMessagesKey("chat-1")],
3141+
["cost tree", chatCostTreeKey("chat-1")],
3142+
] as const) {
3143+
expect(
3144+
queryClient.getQueryState(key)?.isInvalidated,
3145+
`${label} entry should NOT be invalidated`,
3146+
).not.toBe(true);
3147+
}
3148+
});
30603149
});
30613150

30623151
describe("semantic cache operations: cancellation", () => {

site/src/api/queries/chats.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,11 @@ export const invalidateChatsByWorkspace = (queryClient: QueryClient) =>
648648
queryKey: chatsByWorkspaceFamilyKey,
649649
});
650650

651+
export const invalidateChatSearches = (queryClient: QueryClient) =>
652+
queryClient.invalidateQueries({
653+
queryKey: chatSearchFamilyKey,
654+
});
655+
651656
export const invalidateChatDebugRuns = (
652657
queryClient: QueryClient,
653658
chatId: string,
@@ -993,6 +998,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
993998
void invalidateChatListQueries(queryClient);
994999
void invalidateChatEntity(queryClient, chatId);
9951000
void invalidateChatsByWorkspace(queryClient);
1001+
void invalidateChatSearches(queryClient);
9961002
},
9971003
});
9981004

@@ -1042,6 +1048,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
10421048
void invalidateChatListQueries(queryClient);
10431049
void invalidateChatEntity(queryClient, chatId);
10441050
void invalidateChatsByWorkspace(queryClient);
1051+
void invalidateChatSearches(queryClient);
10451052
},
10461053
});
10471054

@@ -1329,6 +1336,7 @@ export const updateChatTitle = (queryClient: QueryClient) => ({
13291336
) => {
13301337
void invalidateChatListQueries(queryClient);
13311338
void invalidateChatEntity(queryClient, chatId);
1339+
void invalidateChatSearches(queryClient);
13321340
},
13331341
});
13341342

@@ -1409,6 +1417,7 @@ export const createChat = (queryClient: QueryClient) => ({
14091417
onSuccess: () => {
14101418
void invalidateChatListQueries(queryClient);
14111419
void invalidateChatsByWorkspace(queryClient);
1420+
void invalidateChatSearches(queryClient);
14121421
},
14131422
});
14141423

@@ -1501,6 +1510,7 @@ export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({
15011510
void invalidateChatEntity(queryClient, chatId);
15021511
void invalidateChatPrompts(queryClient, chatId);
15031512
void invalidateChatDebugRuns(queryClient, chatId);
1513+
void invalidateChatSearches(queryClient);
15041514
},
15051515
});
15061516

site/src/pages/AgentsPage/AgentsPageLayout.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { act, renderHook } from "@testing-library/react";
22
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33
import type * as TypesGen from "#/api/typesGenerated";
4+
import { ChatWatchEventKinds } from "#/api/typesGenerated";
45
import {
56
chatCostIdToInvalidate,
7+
shouldInvalidateChatSearches,
68
shouldInvalidateFilteredChatList,
79
} from "./AgentsPageLayout";
810
import {
@@ -933,6 +935,29 @@ describe(shouldInvalidateFilteredChatList.name, () => {
933935
});
934936
});
935937

938+
describe(shouldInvalidateChatSearches.name, () => {
939+
// Search results render title, status, diff status, and the
940+
// action-required badge. Summary and context events are excluded:
941+
// stale last_turn_summary subtitles are accepted until
942+
// reconciliation lands. The created and deleted kinds are handled
943+
// by their own watch branches before the merge path runs.
944+
const expectedByKind: Record<TypesGen.ChatWatchEventKind, boolean> = {
945+
action_required: true,
946+
chat_summary_change: false,
947+
context_dirty: false,
948+
created: false,
949+
deleted: false,
950+
diff_status_change: true,
951+
status_change: true,
952+
summary_change: false,
953+
title_change: true,
954+
};
955+
956+
it.each(ChatWatchEventKinds)("%s", (kind) => {
957+
expect(shouldInvalidateChatSearches(kind)).toBe(expectedByKind[kind]);
958+
});
959+
});
960+
936961
describe(chatCostIdToInvalidate.name, () => {
937962
it.each<{
938963
name: string;

site/src/pages/AgentsPage/AgentsPageLayout.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
invalidateChatDiffContents,
3030
invalidateChatEntity,
3131
invalidateChatListQueries,
32+
invalidateChatSearches,
3233
invalidateChatsByWorkspace,
3334
mergeWatchedChatIntoCaches,
3435
pinChat,
@@ -130,6 +131,21 @@ export const shouldInvalidateFilteredChatList = (
130131
): boolean =>
131132
!chat.parent_chat_id && FILTER_MEMBERSHIP_EVENT_KINDS.has(eventKind);
132133

134+
// Watch events that change fields rendered in search results (title,
135+
// status, diff status, action-required badge). Summary events are
136+
// deliberately excluded: stale last_turn_summary subtitles are accepted
137+
// until reconciliation lands.
138+
const SEARCH_AFFECTING_EVENT_KINDS = new Set<TypesGen.ChatWatchEventKind>([
139+
"title_change",
140+
"status_change",
141+
"diff_status_change",
142+
"action_required",
143+
]);
144+
145+
export const shouldInvalidateChatSearches = (
146+
eventKind: TypesGen.ChatWatchEventKind,
147+
): boolean => SEARCH_AFFECTING_EVENT_KINDS.has(eventKind);
148+
133149
// Summary and title generation can bill after the turn reports a non-active
134150
// status, so invalidate the root-keyed cost query when those events arrive.
135151
const POST_TURN_BILLED_EVENT_KINDS = new Set<TypesGen.ChatWatchEventKind>([
@@ -308,6 +324,7 @@ const AgentsPageLayout: FC = () => {
308324
void invalidateChatListQueries(queryClient);
309325
void invalidateChatEntity(queryClient, chatId);
310326
void invalidateChatsByWorkspace(queryClient);
327+
void invalidateChatSearches(queryClient);
311328
void invalidateWorkspaceMutationQueries(queryClient, {
312329
organizationName,
313330
username: user.username,
@@ -576,6 +593,7 @@ const AgentsPageLayout: FC = () => {
576593
return changed ? next : chats;
577594
});
578595
void invalidateChatListQueries(queryClient);
596+
void invalidateChatSearches(queryClient);
579597
}, [agentId, queryClient]);
580598
useEffect(() => {
581599
return createReconnectingWebSocket({
@@ -615,6 +633,7 @@ const AgentsPageLayout: FC = () => {
615633
);
616634
removeChildFromParentInCache(queryClient, updatedChat.id);
617635
removeChatEntity(queryClient, updatedChat.id);
636+
void invalidateChatSearches(queryClient);
618637
return;
619638
}
620639
if (chatEvent.kind === "diff_status_change") {
@@ -650,6 +669,7 @@ const AgentsPageLayout: FC = () => {
650669
} else {
651670
prependToInfiniteChatsCache(queryClient, updatedChat);
652671
void invalidateChatListQueries(queryClient);
672+
void invalidateChatSearches(queryClient);
653673
}
654674
} else {
655675
mergeWatchedChatIntoCaches(queryClient, updatedChat, {
@@ -659,6 +679,9 @@ const AgentsPageLayout: FC = () => {
659679
if (shouldInvalidateFilteredChatList(updatedChat, chatEvent.kind)) {
660680
void invalidateChatListQueries(queryClient);
661681
}
682+
if (shouldInvalidateChatSearches(chatEvent.kind)) {
683+
void invalidateChatSearches(queryClient);
684+
}
662685
const costChatId = chatCostIdToInvalidate(
663686
updatedChat,
664687
chatEvent.kind,
@@ -681,6 +704,7 @@ const AgentsPageLayout: FC = () => {
681704
},
682705
onOpen() {
683706
void invalidateChatListQueries(queryClient);
707+
void invalidateChatSearches(queryClient);
684708
},
685709
});
686710
}, [queryClient]);

site/src/pages/AgentsPage/components/ChatConversation/useChatStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { watchChat } from "#/api/api";
1414
import {
1515
chatMessagesKey,
1616
invalidateChatPrompts,
17+
invalidateChatSearches,
1718
patchChatMessages,
1819
updateInfiniteChatsCache,
1920
} from "#/api/queries/chats";
@@ -234,6 +235,7 @@ export const useChatStore = (
234235
if (hasNewUserPrompt) {
235236
void invalidateChatPrompts(queryClient, chatID);
236237
}
238+
void invalidateChatSearches(queryClient);
237239
},
238240
[chatID, queryClient],
239241
);
@@ -255,6 +257,7 @@ export const useChatStore = (
255257
pageParams: currentData.pageParams.slice(0, 1),
256258
};
257259
});
260+
void invalidateChatSearches(queryClient);
258261
},
259262
[chatID, queryClient],
260263
);

0 commit comments

Comments
 (0)