diff --git a/src/app/conversations/[projectId]/goals/page.tsx b/src/app/conversations/[projectId]/goals/page.tsx index 4c27ad9..1bd01e1 100644 --- a/src/app/conversations/[projectId]/goals/page.tsx +++ b/src/app/conversations/[projectId]/goals/page.tsx @@ -26,7 +26,11 @@ export default async function GoalsPage({ return ( - +
diff --git a/src/app/conversations/[projectId]/versions/page.tsx b/src/app/conversations/[projectId]/versions/page.tsx index a4edcc4..2974ad4 100644 --- a/src/app/conversations/[projectId]/versions/page.tsx +++ b/src/app/conversations/[projectId]/versions/page.tsx @@ -26,6 +26,7 @@ export default async function VersionsPage({ diff --git a/src/components/app-runtime.tsx b/src/components/app-runtime.tsx index 081c388..592ff9d 100644 --- a/src/components/app-runtime.tsx +++ b/src/components/app-runtime.tsx @@ -254,9 +254,11 @@ export function NativeAppBridge() { export function RealtimeRefresh({ events, projectId, + conversationUpdatedNotes, }: { events: BossEventName[]; projectId?: string; + conversationUpdatedNotes?: string[]; }) { const router = useRouter(); @@ -268,21 +270,40 @@ export function RealtimeRefresh({ ...events, ])); const shouldRefresh = (event: Event) => { - if (!projectId || !("data" in event) || typeof event.data !== "string" || !event.data.trim()) { - return true; + let payload: { projectId?: string; note?: string } | null = null; + const eventData = "data" in event && typeof event.data === "string" ? event.data : ""; + const hasPayloadData = Boolean(eventData.trim()); + + if (hasPayloadData) { + try { + const parsed = JSON.parse(eventData) as { projectId?: string; note?: string }; + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + payload = parsed; + } + } catch { + payload = null; + } } - try { - const payload = JSON.parse(event.data) as { projectId?: string }; - if (!payload || typeof payload !== "object" || Array.isArray(payload)) { + + if (projectId) { + if (!payload || typeof payload.projectId !== "string" || !payload.projectId.trim()) { return true; } - if (typeof payload.projectId !== "string" || !payload.projectId.trim()) { - return true; + if (payload.projectId !== projectId) { + return false; } - return payload.projectId === projectId; - } catch { - return true; } + + if (event.type === "conversation.updated" && conversationUpdatedNotes?.length) { + if (!payload || typeof payload.note !== "string" || !payload.note.trim()) { + return false; + } + if ((payload.note) && !conversationUpdatedNotes.includes(payload.note)) { + return false; + } + } + + return true; }; const listenerMap = new Map void>(); @@ -306,7 +327,7 @@ export function RealtimeRefresh({ } source.close(); }; - }, [events, projectId, router]); + }, [conversationUpdatedNotes, events, projectId, router]); return null; } diff --git a/tests/project-scoped-realtime-refresh.test.ts b/tests/project-scoped-realtime-refresh.test.ts index 22eaca9..d4dc3d2 100644 --- a/tests/project-scoped-realtime-refresh.test.ts +++ b/tests/project-scoped-realtime-refresh.test.ts @@ -14,7 +14,21 @@ 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, /payload\.projectId === projectId/, "expected RealtimeRefresh to filter by matching projectId"); + assert.match( + source, + /conversationUpdatedNotes\?: string\[]/, + "expected RealtimeRefresh to accept optional conversationUpdatedNotes", + ); + assert.match( + source, + /payload\.projectId !== projectId[\s\S]*return false/, + "expected RealtimeRefresh to filter by matching projectId", + ); + assert.match( + source, + /payload\.note\)\s*&&\s*!conversationUpdatedNotes\.includes\(payload\.note\)/, + "expected RealtimeRefresh to ignore unmatched conversation.updated notes", + ); }); test("project conversation pages wire project-scoped realtime refresh", async () => { @@ -36,4 +50,15 @@ test("project conversation pages wire project-scoped realtime refresh", async () assert.match(source, /projectId=\{projectId\}/, `expected ${label} page to scope refreshes to the current project`); assert.match(source, /"conversation\.updated"/, `expected ${label} page to listen to conversation updates`); } + + assert.match( + goalsPage, + /conversationUpdatedNotes=\{\["project_goals\.updated"\]\}/, + "expected goals page to refresh only on project goal markers", + ); + assert.match( + versionsPage, + /conversationUpdatedNotes=\{\["project_goals\.updated"\]\}/, + "expected versions page to refresh only on project goal markers", + ); });