From dfcebf082b71237e4f02eddf1512d617412c43c3 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 15 May 2026 08:55:54 -0500 Subject: [PATCH] [codex] Refresh issue documents from live updates (#6005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The board UI keeps issue pages responsive by subscribing to live activity events and invalidating TanStack Query caches. > - Issue documents are first-class issue artifacts, but document activity events were not refreshing the document list, active document, or revision caches. > - That meant a user could update a document on an issue and another open board would keep showing stale document content until a page reload. > - This pull request routes issue document activity events through the same live invalidation path used for issue and comment updates. > - The benefit is that issue document changes become visible automatically on active issue pages without forcing operators to reload the board. ## What Changed - Added live-update cache invalidation for `issue.document_created`, `issue.document_updated`, `issue.document_restored`, and `issue.document_deleted` activity events. - Invalidated the issue document list, the active document cache, and document revisions for both issue id and identifier references when the activity payload includes a document key. - Added regression coverage for document activity events so active issue pages refetch document caches without inactive-only behavior. - Simplified the document invalidation test mock after Greptile feedback so the test only models the cache reads it actually uses. ## Verification - `git rebase public-gh/master` reported the branch was up to date after fetching `public-gh/master`. - `pnpm run preflight:workspace-links` passed. - `pnpm exec vitest run --project @paperclipai/ui ui/src/context/LiveUpdatesProvider.test.ts` passed: 1 file, 16 tests. - `pnpm --filter @paperclipai/ui typecheck` passed. - PR checks passed on `eecd19f7b0355490f17314c94bffa06aada8f9e3`: `policy`, `verify`, `e2e`, all 4 serialized server shards, `Canary Dry Run`, `security/snyk`, and `Greptile Review`. - Greptile completed with `5/5` confidence and no unresolved review threads. ## Risks - Low risk. This expands cache invalidation for existing live activity events and does not change API contracts, database schema, migrations, or document persistence behavior. - No migrations or `pnpm-lock.yaml` changes are included. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled local repository workflow. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] No visible UI layout changed; screenshots are not applicable for live cache invalidation behavior - [x] No documentation changes were needed for this internal UI cache refresh fix - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- ui/src/context/LiveUpdatesProvider.test.ts | 88 ++++++++++++++++++++++ ui/src/context/LiveUpdatesProvider.tsx | 17 +++++ 2 files changed, 105 insertions(+) diff --git a/ui/src/context/LiveUpdatesProvider.test.ts b/ui/src/context/LiveUpdatesProvider.test.ts index 5aa7654e..673b7032 100644 --- a/ui/src/context/LiveUpdatesProvider.test.ts +++ b/ui/src/context/LiveUpdatesProvider.test.ts @@ -100,6 +100,94 @@ describe("LiveUpdatesProvider issue invalidation", () => { }); }); + it("refreshes issue document caches when a document activity event arrives", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: () => undefined, + }; + + __liveUpdatesTestUtils.invalidateActivityQueries( + queryClient as never, + "company-1", + { + entityType: "issue", + entityId: "issue-1", + action: "issue.document_updated", + actorType: "agent", + actorId: "agent-1", + details: { + identifier: "PAP-9403", + key: "plan", + }, + }, + { userId: "user-1", agentId: null }, + { pathname: "/PAP/issues/PAP-9403", isForegrounded: true }, + ); + + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.detail("issue-1"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.documents("issue-1"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.document("issue-1", "plan"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.documentRevisions("issue-1", "plan"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.documents("PAP-9403"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.document("PAP-9403", "plan"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.documentRevisions("PAP-9403", "plan"), + }); + expect(invalidations).not.toContainEqual({ + queryKey: queryKeys.issues.documents("issue-1"), + refetchType: "inactive", + }); + }); + + it("refreshes all issue document caches when document activity omits a document key", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: () => undefined, + }; + + __liveUpdatesTestUtils.invalidateActivityQueries( + queryClient as never, + "company-1", + { + entityType: "issue", + entityId: "issue-1", + action: "issue.document_deleted", + actorType: "agent", + actorId: "agent-1", + details: null, + }, + { userId: "user-1", agentId: null }, + ); + + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.documents("issue-1"), + }); + expect(invalidations).toContainEqual({ + queryKey: ["issues", "document", "issue-1"], + }); + expect(invalidations).toContainEqual({ + queryKey: ["issues", "document-revisions", "issue-1"], + }); + }); + it("keeps self-authored comment events from refetching the active issue tree", () => { const invalidations: unknown[] = []; const queryClient = { diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 99f3a2fc..783537e0 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -408,6 +408,12 @@ async function hydrateVisibleIssueComment( } const ISSUE_TOAST_ACTIONS = new Set(["issue.created", "issue.updated", "issue.comment_added"]); +const ISSUE_DOCUMENT_ACTIVITY_ACTIONS = new Set([ + "issue.document_created", + "issue.document_updated", + "issue.document_restored", + "issue.document_deleted", +]); const AGENT_TOAST_STATUSES = new Set(["error"]); const RUN_TOAST_STATUSES = new Set(["failed", "timed_out", "cancelled"]); @@ -685,6 +691,17 @@ function invalidateActivityQueries( if (action === "issue.comment_added") { queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(ref), ...invalidationOptions }); } + if (action && ISSUE_DOCUMENT_ACTIVITY_ACTIONS.has(action)) { + const documentKey = readString(details?.key); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(ref), ...invalidationOptions }); + if (documentKey) { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.document(ref, documentKey), ...invalidationOptions }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.documentRevisions(ref, documentKey), ...invalidationOptions }); + } else { + queryClient.invalidateQueries({ queryKey: ["issues", "document", ref], ...invalidationOptions }); + queryClient.invalidateQueries({ queryKey: ["issues", "document-revisions", ref], ...invalidationOptions }); + } + } if (action?.startsWith("issue.thread_interaction_")) { queryClient.invalidateQueries({ queryKey: queryKeys.issues.interactions(ref), ...invalidationOptions }); }