[codex] Refresh issue documents from live updates (#6005)
## 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 <noreply@paperclip.ing>
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user