From 4093c41949958e9a74879ba968737d8c147800cc Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 7 Apr 2026 17:33:37 +0800 Subject: [PATCH] Refresh folder pages for scoped thread updates --- src/app/conversations/folders/[folderKey]/page.tsx | 7 +++++++ src/components/app-runtime.tsx | 13 ++++++++++--- tests/project-scoped-realtime-refresh.test.ts | 11 ++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/app/conversations/folders/[folderKey]/page.tsx b/src/app/conversations/folders/[folderKey]/page.tsx index 3271e48..ee19180 100644 --- a/src/app/conversations/folders/[folderKey]/page.tsx +++ b/src/app/conversations/folders/[folderKey]/page.tsx @@ -1,3 +1,4 @@ +import { RealtimeRefresh } from "@/components/app-runtime"; import { AppShell, ConversationList, @@ -22,6 +23,12 @@ export default async function ConversationFolderPage({ return ( + {folder ? ( + thread.projectId)} + events={["conversation.updated", "project.messages.updated"]} + /> + ) : null}
diff --git a/src/components/app-runtime.tsx b/src/components/app-runtime.tsx index 592ff9d..965a885 100644 --- a/src/components/app-runtime.tsx +++ b/src/components/app-runtime.tsx @@ -254,16 +254,23 @@ export function NativeAppBridge() { export function RealtimeRefresh({ events, projectId, + projectIds, conversationUpdatedNotes, }: { events: BossEventName[]; projectId?: string; + projectIds?: string[]; conversationUpdatedNotes?: string[]; }) { const router = useRouter(); useEffect(() => { const source = new EventSource("/api/v1/events"); + const projectScopeIds = new Set( + [projectId, ...(projectIds ?? [])] + .filter((value): value is string => Boolean(value?.trim())) + .map((value) => value.trim()), + ); const listeners = Array.from(new Set([ "conversation.context_indicator.updated", "project.context_risk.updated", @@ -285,11 +292,11 @@ export function RealtimeRefresh({ } } - if (projectId) { + if (projectScopeIds.size > 0) { if (!payload || typeof payload.projectId !== "string" || !payload.projectId.trim()) { return true; } - if (payload.projectId !== projectId) { + if (!projectScopeIds.has(payload.projectId)) { return false; } } @@ -327,7 +334,7 @@ export function RealtimeRefresh({ } source.close(); }; - }, [conversationUpdatedNotes, events, projectId, router]); + }, [conversationUpdatedNotes, events, projectId, projectIds, router]); return null; } diff --git a/tests/project-scoped-realtime-refresh.test.ts b/tests/project-scoped-realtime-refresh.test.ts index d4dc3d2..7e10b56 100644 --- a/tests/project-scoped-realtime-refresh.test.ts +++ b/tests/project-scoped-realtime-refresh.test.ts @@ -14,6 +14,7 @@ test("RealtimeRefresh supports project-scoped refresh filtering", async () => { const source = await readWorkspaceFile("src/components/app-runtime.tsx"); assert.match(source, /projectId\?: string/, "expected RealtimeRefresh to accept an optional projectId"); + assert.match(source, /projectIds\?: string\[]/, "expected RealtimeRefresh to accept optional projectIds"); assert.match( source, /conversationUpdatedNotes\?: string\[]/, @@ -21,7 +22,7 @@ test("RealtimeRefresh supports project-scoped refresh filtering", async () => { ); assert.match( source, - /payload\.projectId !== projectId[\s\S]*return false/, + /projectScopeIds\.has\(payload\.projectId\)/, "expected RealtimeRefresh to filter by matching projectId", ); assert.match( @@ -62,3 +63,11 @@ test("project conversation pages wire project-scoped realtime refresh", async () "expected versions page to refresh only on project goal markers", ); }); + +test("folder conversation page wires folder thread ids into realtime refresh", async () => { + const folderPage = await readWorkspaceFile("src/app/conversations/folders/[folderKey]/page.tsx"); + + assert.match(folderPage, / thread\.projectId\)\}/, "expected folder page to scope refreshes to folder thread project ids"); + assert.match(folderPage, /"conversation\.updated"/, "expected folder page to listen to conversation updates"); +});