mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Fix disappearing issue comments (#4557)
## Thinking Path > - Paperclip is a control plane for AI-agent companies, so issue detail pages are a primary surface for understanding agent work and human feedback. > - The relevant subsystem here is the issue comments/chat experience across the React issue detail page and the server comment pagination API. > - Long issue threads were only surfacing the newest page of comments at first render, which hid earlier human and agent messages behind extra pagination. > - The first UI fix exposed that the descending cursor path on the server could also fail for older-page fetches, leaving the chat tab stuck on an infinite "Loading earlier comments..." state. > - This needed to be addressed in both layers so the chat tab can surface earlier conversation history without manual recovery and without server errors. > - This pull request auto-loads earlier comment pages in the issue detail chat view and fixes the descending cursor predicate used by issue comment pagination. > - The benefit is that long-running issues like `PAPA-103` now show the missing conversation history near the top of the chat surface instead of hiding it or failing to load it. ## What Changed - Auto-load earlier issue comment pages in the issue detail chat tab until the thread reaches a 150-comment cap or there are no older comments left. - Add UI-side guard logic and regression coverage for optimistic issue comment pagination so the autoload behavior stops cleanly. - Replace the raw SQL descending cursor predicate in `issueService.listComments` with typed Drizzle comparisons for the `(createdAt, id)` anchor tuple. - Add a server regression test that paginates earlier comments in descending order from an anchor comment. - Smoke-test the exact previously failing seeded `PAPA-103` cursor path on the isolated dev instance used for review. ## Verification - `pnpm --filter @paperclipai/server exec vitest run src/__tests__/issues-service.test.ts` - `pnpm --filter @paperclipai/server typecheck` - Manual smoke against seeded `PAPA-103` data on the isolated dev server: - `GET /api/issues/PAPA-103/comments?order=desc&limit=50` returns `200` - `GET /api/issues/PAPA-103/comments?after=765d3609-edc6-4d11-a8fe-d466affbe85d&order=desc&limit=50` now returns `200` with 50 comments instead of `500` ## Risks - Moderate UI/perf risk on very large threads because the chat tab now prefetches multiple earlier pages on mount; the cap is intentionally limited to 150 comments to bound that work. - Low API risk because the server fix only changes the cursor predicate construction for anchor-based comment pagination, but any mistake there would affect older-comment paging order. > I checked `ROADMAP.md` before opening this PR and this bug fix does not duplicate planned core work. ## Model Used - OpenAI Codex coding agent in the Paperclip local adapter environment. The exact backend model ID and context window were not exposed in-session. Tool-assisted workflow included shell execution, git/GitHub CLI, local test execution, and targeted code edits. ## 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 - [ ] If this change affects the UI, I have included before/after screenshots - [ ] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
b2496c8067
commit
54ab0d24cd
5 changed files with 140 additions and 9 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
matchesIssueRef,
|
||||
mergeIssueComments,
|
||||
removeIssueCommentFromPages,
|
||||
shouldAutoloadOlderIssueComments,
|
||||
takeOptimisticIssueComment,
|
||||
upsertIssueComment,
|
||||
upsertIssueCommentInPages,
|
||||
|
|
@ -233,6 +234,45 @@ describe("optimistic issue comments", () => {
|
|||
).toBe("comment-1");
|
||||
});
|
||||
|
||||
it("autoloads older chat comments while the initial thread is still under the threshold", () => {
|
||||
expect(
|
||||
shouldAutoloadOlderIssueComments({
|
||||
activeDetailTab: "chat",
|
||||
hasOlderComments: true,
|
||||
loadedCommentCount: 50,
|
||||
initialPageLoading: false,
|
||||
olderPageLoading: false,
|
||||
autoLoadLimit: 150,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not autoload older comments outside the chat tab", () => {
|
||||
expect(
|
||||
shouldAutoloadOlderIssueComments({
|
||||
activeDetailTab: "activity",
|
||||
hasOlderComments: true,
|
||||
loadedCommentCount: 50,
|
||||
initialPageLoading: false,
|
||||
olderPageLoading: false,
|
||||
autoLoadLimit: 150,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("stops autoloading once the initial comment window reaches the cap", () => {
|
||||
expect(
|
||||
shouldAutoloadOlderIssueComments({
|
||||
activeDetailTab: "chat",
|
||||
hasOlderComments: true,
|
||||
loadedCommentCount: 150,
|
||||
initialPageLoading: false,
|
||||
olderPageLoading: false,
|
||||
autoLoadLimit: 150,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("upserts paged comments without dropping older pages", () => {
|
||||
const nextPages = upsertIssueCommentInPages(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -150,6 +150,21 @@ export function getNextIssueCommentPageParam(
|
|||
return lastPage[lastPage.length - 1]?.id;
|
||||
}
|
||||
|
||||
export function shouldAutoloadOlderIssueComments(params: {
|
||||
activeDetailTab: string;
|
||||
hasOlderComments: boolean;
|
||||
loadedCommentCount: number;
|
||||
initialPageLoading: boolean;
|
||||
olderPageLoading: boolean;
|
||||
autoLoadLimit: number;
|
||||
}) {
|
||||
if (params.activeDetailTab !== "chat") return false;
|
||||
if (!params.hasOlderComments) return false;
|
||||
if (params.initialPageLoading || params.olderPageLoading) return false;
|
||||
if (params.loadedCommentCount === 0) return false;
|
||||
return params.loadedCommentCount < params.autoLoadLimit;
|
||||
}
|
||||
|
||||
export function upsertIssueComment(
|
||||
comments: IssueComment[] | undefined,
|
||||
nextComment: IssueComment,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import {
|
|||
matchesIssueRef,
|
||||
mergeIssueComments,
|
||||
removeIssueCommentFromPages,
|
||||
shouldAutoloadOlderIssueComments,
|
||||
takeOptimisticIssueComment,
|
||||
upsertIssueCommentInPages,
|
||||
type IssueCommentReassignment,
|
||||
|
|
@ -152,6 +153,7 @@ type IssueDetailComment = (IssueComment | OptimisticIssueComment) & {
|
|||
|
||||
const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos";
|
||||
const ISSUE_COMMENT_PAGE_SIZE = 50;
|
||||
const ISSUE_COMMENT_AUTOLOAD_LIMIT = ISSUE_COMMENT_PAGE_SIZE * 3;
|
||||
const TREE_CONTROL_MODE_LABEL: Record<IssueTreeControlMode, string> = {
|
||||
pause: "Pause subtree",
|
||||
resume: "Resume subtree",
|
||||
|
|
@ -1103,6 +1105,18 @@ export function IssueDetail() {
|
|||
() => flattenIssueCommentPages(commentPages?.pages),
|
||||
[commentPages?.pages],
|
||||
);
|
||||
const shouldPrefetchOlderComments = useMemo(
|
||||
() =>
|
||||
shouldAutoloadOlderIssueComments({
|
||||
activeDetailTab: detailTab,
|
||||
hasOlderComments: hasOlderComments ?? false,
|
||||
loadedCommentCount: comments.length,
|
||||
initialPageLoading: commentsLoading,
|
||||
olderPageLoading: commentsLoadingOlder,
|
||||
autoLoadLimit: ISSUE_COMMENT_AUTOLOAD_LIMIT,
|
||||
}),
|
||||
[comments.length, commentsLoading, commentsLoadingOlder, detailTab, hasOlderComments],
|
||||
);
|
||||
const { data: interactions = [] } = useQuery({
|
||||
queryKey: queryKeys.issues.interactions(issueId!),
|
||||
queryFn: () => issuesApi.listInteractions(issueId!),
|
||||
|
|
@ -2537,6 +2551,10 @@ export function IssueDetail() {
|
|||
const loadOlderComments = useCallback(() => {
|
||||
void fetchOlderComments();
|
||||
}, [fetchOlderComments]);
|
||||
useEffect(() => {
|
||||
if (!shouldPrefetchOlderComments) return;
|
||||
void fetchOlderComments();
|
||||
}, [fetchOlderComments, shouldPrefetchOlderComments]);
|
||||
const handleCommentVote = useCallback(async (commentId: string, vote: "up" | "down", options?: { allowSharing?: boolean; reason?: string }) => {
|
||||
await feedbackVoteMutation.mutateAsync({
|
||||
targetType: "issue_comment",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue