Files
PaperClipAI/ui/src/lib/prefetchIssueComments.test.ts
T
DottaandPaperclip 1452d5f413 perf(ui): warm issue detail navigation (#10416)
## Thinking Path

> - Paperclip is the open source control plane people use to coordinate
AI-agent companies
> - The board UI opens issue details from lists, quicklooks, and the
inbox
> - Those navigation paths already have enough issue data to paint the
header immediately, but the comment feed still waits for its first
request
> - That delay makes repeat navigation feel cold and can briefly show an
empty thread before comments arrive
> - This pull request centralizes the comment page shape, prefetches
issue detail plus the first comment page, and renders a reserved
skeleton while an uncached page is loading
> - The benefit is faster, stable warm navigation without coupling this
change to the separate aggregate issue-detail API work

## Linked Issues or Issue Description

- **Subsystem affected:** `ui/` — React + Vite board UI
- **Problem or motivation:** Opening an issue from an already-loaded
list still waits for the first comments request and may flash the
empty-thread state, making warm navigation feel slower than necessary.
- **Proposed solution:** Prefetch the issue-detail snapshot and first
comments page from every issue navigation entry point, reuse one
page-size constant for prefetch and render queries, and show the
existing chat skeleton until the uncached initial page resolves.
- **Alternatives considered:** Relying only on detail-query prefetch
leaves comments cold; bundling this with the aggregate issue-detail
endpoint would make the UI improvement harder to review and land
independently.
- **Roadmap alignment:** `ROADMAP.md` has no overlapping
issue-navigation initiative. This is a focused board responsiveness
improvement.
- **Related pull requests:** #10409 establishes the issue-detail
performance baseline; #10414 reduces server-side issue-detail request
overhead.

## What Changed

- Added one shared issue-comment page-size constant used by rendering
and prefetching.
- Added first-page comment prefetching and a combined navigation
prefetch helper.
- Wired quicklook, issue-list keyboard navigation, and inbox navigation
to warm both caches.
- Kept the existing chat skeleton visible while the first uncached
comment page loads.
- Added focused cache behavior tests for comment and combined navigation
prefetching.

## Verification

- `vitest run ui/src/lib/prefetchIssueComments.test.ts` — 3 tests
passed.
- `tsc -b ui` — passed.
- `pnpm check:token-gates` — passed.

## Risks

- Low risk: this adds background prefetch requests on intentional issue
navigation/hover paths. React Query stale-time deduplication prevents
repeat requests while the cache is fresh.
- The change intentionally remains independent of the separate aggregate
`getView` work and composes with it through the same query keys.

> 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 coding agent, exact model `gpt-5.6-sol`; context-window
size was not exposed; reasoning mode with repository tool use and code
execution.

## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-29 13:37:22 -05:00

101 lines
3.4 KiB
TypeScript

import { QueryClient } from "@tanstack/react-query";
import type { Issue, IssueComment } from "@paperclipai/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { issuesApi } from "@/api/issues";
import { prefetchIssueComments, prefetchIssueDetailForNavigation } from "./issueDetailCache";
import { queryKeys } from "./queryKeys";
vi.mock("@/api/issues", () => ({
issuesApi: {
get: vi.fn(),
listComments: vi.fn(),
},
}));
function createComment(overrides: Partial<IssueComment> = {}): IssueComment {
return {
id: "comment-1",
issueId: "issue-1",
body: "hello",
createdAt: new Date("2026-04-11T00:00:00.000Z"),
...overrides,
} as IssueComment;
}
function createIssue(overrides: Partial<Issue> = {}): Issue {
return {
id: "issue-1",
identifier: "PAP-1",
companyId: "company-1",
projectId: null,
parentId: null,
title: "Fast link target",
description: "A complete snapshot.",
status: "todo",
priority: "medium",
assigneeAgentId: null,
assigneeUserId: null,
executionRunId: null,
issueNumber: 1,
requestDepth: 0,
createdAt: new Date("2026-04-11T00:00:00.000Z"),
updatedAt: new Date("2026-04-11T00:00:00.000Z"),
...overrides,
workMode: overrides.workMode ?? "standard",
} as Issue;
}
describe("prefetchIssueComments", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
vi.clearAllMocks();
});
it("warms the first comments page under the infinite query key IssueDetail mounts", async () => {
const firstPage = [createComment({ id: "c1" }), createComment({ id: "c2" })];
vi.mocked(issuesApi.listComments).mockResolvedValue(firstPage);
await prefetchIssueComments(queryClient, "PAP-1");
expect(issuesApi.listComments).toHaveBeenCalledWith("PAP-1", { order: "desc", limit: 50 });
const cached = queryClient.getQueryData<{ pages: IssueComment[][]; pageParams: unknown[] }>(
queryKeys.issues.comments("PAP-1"),
);
expect(cached?.pages).toEqual([firstPage]);
expect(cached?.pageParams).toEqual([null]);
});
it("does not refetch comments that are already fresh in cache", async () => {
const firstPage = [createComment({ id: "c1" })];
vi.mocked(issuesApi.listComments).mockResolvedValue(firstPage);
await prefetchIssueComments(queryClient, "PAP-1");
await prefetchIssueComments(queryClient, "PAP-1");
// Second prefetch sees fresh data within staleTime and is a no-op.
expect(issuesApi.listComments).toHaveBeenCalledTimes(1);
});
it("seeds both the detail snapshot and the first comments page for navigation", async () => {
const issue = createIssue();
const firstPage = [createComment({ id: "c1" })];
vi.mocked(issuesApi.listComments).mockResolvedValue(firstPage);
await prefetchIssueDetailForNavigation(queryClient, issue.identifier!, { issue });
// Complete snapshot seeds the detail cache without a network fetch.
expect(issuesApi.get).not.toHaveBeenCalled();
expect(queryClient.getQueryData(queryKeys.issues.detail(issue.identifier!))).toEqual(issue);
expect(queryClient.getQueryData(queryKeys.issues.detail(issue.id))).toEqual(issue);
const cachedComments = queryClient.getQueryData<{ pages: IssueComment[][] }>(
queryKeys.issues.comments(issue.identifier!),
);
expect(cachedComments?.pages).toEqual([firstPage]);
});
});