From 093d92d1db03a412167b50b31749e1fa2b58d994 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 21 Oct 2026 04:53:18 +0000 Subject: [PATCH 01/14] perf(web): initialize diff workers while pull request code loads --- apps/web/src/components/DiffWorkerPoolProvider.tsx | 12 ++++++++---- .../components/pullRequest/PullRequestCodeTab.tsx | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/DiffWorkerPoolProvider.tsx b/apps/web/src/components/DiffWorkerPoolProvider.tsx index bbc96dd70302f..5baba922a1bc4 100644 --- a/apps/web/src/components/DiffWorkerPoolProvider.tsx +++ b/apps/web/src/components/DiffWorkerPoolProvider.tsx @@ -125,9 +125,7 @@ function DiffWorkerReady({ children }: { children?: ReactNode }) { ); } -export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { - const resolvedTheme = useRenderedAppearance(); - const diffThemeName = resolveDiffThemeName(resolvedTheme); +export function useDiffWorkerPool(diffThemeName: DiffThemeName) { const [workerPoolSize] = useState(resolveDiffWorkerPoolSize); // A new subscribe identity makes React release and reacquire the shared pool on every render. const subscribe = useStableCallback((onStoreChange: () => void) => { @@ -144,11 +142,17 @@ export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { }, DIFF_WORKER_IDLE_TTL_MS); }; }); - const workerPool = useSyncExternalStore( + return useSyncExternalStore( subscribe, () => sharedWorkerPool?.pool, () => undefined, ); +} + +export function DiffWorkerPoolProvider({ children }: { children?: ReactNode }) { + const resolvedTheme = useRenderedAppearance(); + const diffThemeName = resolveDiffThemeName(resolvedTheme); + const workerPool = useDiffWorkerPool(diffThemeName); return ( diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index feaa39ce35e81..a914910e94a0a 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -67,6 +67,7 @@ import { useEnvironmentQuery } from "~/state/query"; import { useAtomCommand } from "~/state/use-atom-command"; import { DiffPanelLoadingState } from "../DiffPanelShell"; +import { useDiffWorkerPool } from "../DiffWorkerPoolProvider"; import { DiffCommentAnnotation } from "../diffs/DiffCommentAnnotation"; import { DiffFileTree } from "../diffs/DiffFileTree"; import { useCodeViewFileReveal } from "../diffs/useCodeViewFileReveal"; @@ -271,6 +272,8 @@ function PullRequestCodeTab({ revealRequest?: { readonly path: string; readonly nonce: number } | null; }) { const resolvedTheme = useRenderedAppearance(); + // Initialize the shared tokenizer and workers while the diff request is still pending. + useDiffWorkerPool(resolveDiffThemeName(resolvedTheme)); const settings = useClientSettings((settings) => ({ diffFilesCollapsed: settings.diffFilesCollapsed, diffIgnoreWhitespace: settings.diffIgnoreWhitespace, From 333239a444deef0ae514ad329d23190c644c5498 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 21 Oct 2026 04:41:31 +0000 Subject: [PATCH 02/14] perf(github): fetch timeline stats only for non-merge commits --- .../src/server/GitHubPullRequestApi.test.ts | 70 +++++++++ .../src/server/GitHubPullRequestApi.ts | 20 ++- .../server/GitHubPullRequestProvider.test.ts | 17 ++- .../src/server/GitHubPullRequestProvider.ts | 140 +++++++++--------- .../src/server/gitHubPullRequestJson.test.ts | 35 ++--- .../src/server/gitHubPullRequestJson.ts | 42 +++--- .../src/server/pullRequestReads.graphql | 16 ++ .../src/server/pullRequestReads.graphql.ts | 66 ++++++++- .../src/server/pullRequestReviews.graphql | 5 - .../src/server/pullRequestReviews.graphql.ts | 15 +- 10 files changed, 288 insertions(+), 138 deletions(-) diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts index 23cadae857751..7cdedc90de172 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.test.ts @@ -723,6 +723,76 @@ it.effect( ); layer("GitHubPullRequestApi.layer", (it) => { + it.effect.each(["mixed", "merges", "unavailable"] as const)( + "reads stats only for displayed non-merge commits (%s)", + (kind) => + Effect.gen(function* () { + const oids = kind === "merges" ? ["merge"] : ["regular", "merge"]; + mockedExecute.mockReturnValueOnce( + Effect.succeed( + output( + encodeJson({ + data: { + repository: { + pullRequest: { + author: null, + comments: { nodes: [], pageInfo: lastPage }, + reviews: { nodes: [], pageInfo: lastPage }, + commits: { + nodes: oids.map((oid) => ({ + commit: { + id: "node-" + oid, + oid, + messageHeadline: oid, + committedDate: "2026-07-05T00:00:00Z", + parents: { totalCount: oid === "merge" ? 2 : 1 }, + authors: { nodes: [] }, + }, + })), + }, + }, + }, + }, + }), + ), + ), + ); + if (kind === "unavailable") { + mockedExecute.mockReturnValueOnce(Effect.succeed(output("{}", false, false, {}, 503))); + } else if (kind !== "merges") { + mockedExecute.mockReturnValueOnce( + Effect.succeed( + output( + encodeJson({ + data: { + nodes: [{ __typename: "Commit", oid: "regular", additions: 18, deletions: 7 }], + }, + }), + ), + ), + ); + } + const cli = yield* GitHubPullRequestApi.GitHubPullRequestApi; + const activity = yield* cli.getPullRequestActivity(pullRequestInput); + expect(activity.commits.map((commit) => commit.oid)).toEqual(oids); + expect(activity.commits.find((commit) => commit.oid === "merge")).not.toHaveProperty( + "additions", + ); + expect(activity.commits.find((commit) => commit.oid === "merge")).not.toHaveProperty( + "deletions", + ); + if (kind === "mixed") { + expect(activity.commits[0]).toMatchObject({ additions: 18, deletions: 7 }); + } else if (kind === "unavailable") { + expect(activity.commits[0]).not.toHaveProperty("additions"); + } + expect(variablesOf("PullRequestCommitStats")).toEqual( + kind === "merges" ? [] : [{ ids: ["node-regular"] }], + ); + expect(mockedExecute).toHaveBeenCalledTimes(kind === "merges" ? 1 : 2); + }), + ); + it.effect("loads the complete hover card with one GraphQL request", () => Effect.gen(function* () { mockedExecute.mockReturnValueOnce( diff --git a/packages/source-control-github/src/server/GitHubPullRequestApi.ts b/packages/source-control-github/src/server/GitHubPullRequestApi.ts index c25699714d147..f49bd8bc1007f 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestApi.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestApi.ts @@ -69,6 +69,7 @@ import { toActorAvatars, toLabelCandidates, toPullRequestActivityPage, + toPullRequestCommitStats, toPullRequestCheckContextsPage, toPullRequestCore, toPullRequestFilesViewedPage, @@ -2042,6 +2043,7 @@ export const make = Effect.gen(function* () { const { owner, name } = parseRepositorySelector(input.repository); let author: PullRequestActor | null = null; let commits: ReadonlyArray = []; + let nonMergeCommitIds: ReadonlyArray = []; const remarks: PullRequestComment[] = []; let commentsAfter: string | null = null; let reviewsAfter: string | null = null; @@ -2069,6 +2071,7 @@ export const make = Effect.gen(function* () { if (page === 0) { author = read.author ?? null; commits = read.commits ?? []; + nonMergeCommitIds = read.nonMergeCommitIds; } remarks.push(...read.remarks); commentsAfter = read.nextCommentsCursor; @@ -2076,12 +2079,21 @@ export const make = Effect.gen(function* () { withComments = withComments && commentsAfter !== null; withReviews = withReviews && reviewsAfter !== null; } + const emptyStats = toPullRequestCommitStats({ nodes: [] }); + const commitStats = + nonMergeCommitIds.length === 0 + ? emptyStats + : yield* client.PullRequestCommitStats({ ids: nonMergeCommitIds }).pipe( + graphql(target), + Effect.map(toPullRequestCommitStats), + Effect.orElseSucceed(() => emptyStats), + ); return { author, comments: remarks.toSorted((left, right) => left.createdAt.localeCompare(right.createdAt), ), - commits, + commits: commits.map((commit) => ({ ...commit, ...commitStats.get(commit.oid) })), } satisfies GitHubPullRequestActivity; }, ), @@ -2186,10 +2198,6 @@ export const make = Effect.gen(function* () { const entries: GitHubReviewThreadEntry[] = []; const avatarsByLogin = new Map(); const botLogins = new Set(); - const commitStats = new Map< - string, - { readonly additions: number; readonly deletions: number } - >(); let reviewers: ReadonlyArray = []; let reactions: GitHubReviewThreadPage["reactions"] = []; const reactionsById = new Map>(); @@ -2216,7 +2224,6 @@ export const make = Effect.gen(function* () { for (const [id, message] of read.dismissalsByReviewId) dismissalsByReviewId.set(id, message); dismissalCursor = read.nextDismissalCursor; - for (const [oid, stat] of read.commitStats) commitStats.set(oid, stat); } cursor = read.nextCursor; page += 1; @@ -2269,7 +2276,6 @@ export const make = Effect.gen(function* () { reviewers, avatarsByLogin, botLogins, - commitStats, commits, viewer, }; diff --git a/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts index 28b83ed3d329c..8003ae0ad45c8 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.test.ts @@ -641,7 +641,6 @@ describe("getChangeRequest commits", () => { reviewers: [], avatarsByLogin: new Map(), botLogins: new Set(), - commitStats: new Map(), viewer: { canUpdate: true, didAuthor: false }, }; @@ -653,6 +652,14 @@ describe("getChangeRequest commits", () => { author: baseDetail.author, comments: baseDetail.comments, commits: [ + { + oid: "graphql-newest", + messageHeadline: "Activity metadata", + committedDate: "2026-07-06T00:00:00Z", + additions: 18, + deletions: 7, + authors: [], + }, { oid: "view-oldest", messageHeadline: "gh pr view's oldest commit", @@ -675,6 +682,11 @@ describe("getChangeRequest commits", () => { }); expect(detail.commits.map((commit) => commit.oid)).toEqual(["graphql-newest"]); + expect(detail.commits[0]).toMatchObject({ + messageHeadline: "the newest commit gh pr view drops", + additions: 18, + deletions: 7, + }); }).pipe( Effect.provide( layerWith([ @@ -699,7 +711,7 @@ describe("getChangeRequest commits", () => { number: 7, }); - expect(detail.commits.map((commit) => commit.oid)).toEqual(["view-oldest"]); + expect(detail.commits.map((commit) => commit.oid)).toEqual(["graphql-newest", "view-oldest"]); }).pipe(Effect.provide(layerWith([]))), ); }); @@ -728,7 +740,6 @@ describe("getChangeRequestActivity dismissed reviews", () => { reviewers: [], avatarsByLogin: new Map(), botLogins: new Set(["macroscopeapp"]), - commitStats: new Map(), commits: [], viewer: { canUpdate: true, didAuthor: false }, }; diff --git a/packages/source-control-github/src/server/GitHubPullRequestProvider.ts b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts index dd710dffcfd66..b39c8a361fe55 100644 --- a/packages/source-control-github/src/server/GitHubPullRequestProvider.ts +++ b/packages/source-control-github/src/server/GitHubPullRequestProvider.ts @@ -405,10 +405,6 @@ export const make = Effect.gen(function* () { reviewers: [], avatarsByLogin: new Map(), botLogins: new Set(), - commitStats: new Map< - string, - { readonly additions: number; readonly deletions: number } - >(), commits: [], viewer: { canUpdate: true, didAuthor: false }, })), @@ -417,74 +413,82 @@ export const make = Effect.gen(function* () { { concurrency: 2 }, ).pipe( Effect.mapError(fail("getChangeRequestActivity")), - Effect.map(([pullRequest, reviewThreads]): ProviderChangeRequestActivity => ({ - author: withAvatar( - pullRequest.author, - reviewThreads.avatarsByLogin, - input.host, - reviewThreads.botLogins, - ), - reviewers: reviewThreads.reviewers, - reactions: reviewThreads.reactions, - commits: (reviewThreads.commits.length > 0 - ? reviewThreads.commits - : pullRequest.commits - ).map((commit) => ({ - ...commit, - ...reviewThreads.commitStats.get(commit.oid), - authors: commit.authors?.map( - (author) => - withAvatar( - author, + Effect.map(([pullRequest, reviewThreads]): ProviderChangeRequestActivity => { + const commitStats = new Map( + pullRequest.commits.map(({ oid, additions, deletions }) => [ + oid, + additions === undefined ? {} : { additions, deletions }, + ]), + ); + return { + author: withAvatar( + pullRequest.author, + reviewThreads.avatarsByLogin, + input.host, + reviewThreads.botLogins, + ), + reviewers: reviewThreads.reviewers, + reactions: reviewThreads.reactions, + commits: (reviewThreads.commits.length > 0 + ? reviewThreads.commits + : pullRequest.commits + ).map((commit) => ({ + ...commit, + ...commitStats.get(commit.oid), + authors: commit.authors?.map( + (author) => + withAvatar( + author, + reviewThreads.avatarsByLogin, + input.host, + reviewThreads.botLogins, + ) ?? author, + ), + })), + comments: [...pullRequest.comments, ...reviewThreads.comments] + .map((comment) => ({ + ...comment, + // GitHub keeps the dismissal reason on the timeline event, not on the review, + // so a dismissed review with nothing visible of its own reads its words from + // there. "Visible" and not "empty": bot reviews often carry only an HTML + // marker comment, which markdown renders as nothing. + body: + comment.kind === "review" && + comment.reviewState?.toUpperCase() === "DISMISSED" && + rendersEmpty(comment.body) + ? (reviewThreads.dismissalsByReviewId.get(comment.id) ?? comment.body) + : comment.body, + author: withAvatar( + comment.author, reviewThreads.avatarsByLogin, input.host, reviewThreads.botLogins, - ) ?? author, - ), - })), - comments: [...pullRequest.comments, ...reviewThreads.comments] - .map((comment) => ({ - ...comment, - // GitHub keeps the dismissal reason on the timeline event, not on the review, - // so a dismissed review with nothing visible of its own reads its words from - // there. "Visible" and not "empty": bot reviews often carry only an HTML - // marker comment, which markdown renders as nothing. - body: - comment.kind === "review" && - comment.reviewState?.toUpperCase() === "DISMISSED" && - rendersEmpty(comment.body) - ? (reviewThreads.dismissalsByReviewId.get(comment.id) ?? comment.body) - : comment.body, - author: withAvatar( - comment.author, - reviewThreads.avatarsByLogin, - input.host, - reviewThreads.botLogins, - ), - // A comment out of `gh pr view --json` carries none of its own: that read - // reports no reaction at all, so they arrive from the GraphQL page by node id. - reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], - editedAt: reviewThreads.editedAtById.get(comment.id) ?? comment.editedAt ?? null, - })) - .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), - // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two - // are always whole and only the thread walk can stop short of the host. - commentCount: pullRequest.comments.length + reviewThreads.commentCount, - commentsTruncated: reviewThreads.truncated, - reviewThreadsTruncated: reviewThreads.reviewThreadsTruncated, - reviewThreads: reviewThreads.reviewThreads.map((thread) => ({ - ...thread, - comments: thread.comments.map((comment) => ({ - ...comment, - author: withAvatar( - comment.author, - reviewThreads.avatarsByLogin, - input.host, - reviewThreads.botLogins, - ), + ), + // A comment out of `gh pr view --json` carries none of its own: that read + // reports no reaction at all, so they arrive from the GraphQL page by node id. + reactions: comment.reactions ?? reviewThreads.reactionsById.get(comment.id) ?? [], + editedAt: reviewThreads.editedAtById.get(comment.id) ?? comment.editedAt ?? null, + })) + .toSorted((left, right) => left.createdAt.localeCompare(right.createdAt)), + // `gh pr view --json comments,reviews` follows GitHub's cursors itself, so those two + // are always whole and only the thread walk can stop short of the host. + commentCount: pullRequest.comments.length + reviewThreads.commentCount, + commentsTruncated: reviewThreads.truncated, + reviewThreadsTruncated: reviewThreads.reviewThreadsTruncated, + reviewThreads: reviewThreads.reviewThreads.map((thread) => ({ + ...thread, + comments: thread.comments.map((comment) => ({ + ...comment, + author: withAvatar( + comment.author, + reviewThreads.avatarsByLogin, + input.host, + reviewThreads.botLogins, + ), + })), })), - })), - })), + }; + }), ), getReviewThreadComments: (input) => diff --git a/packages/source-control-github/src/server/gitHubPullRequestJson.test.ts b/packages/source-control-github/src/server/gitHubPullRequestJson.test.ts index 5a408c98044ba..c4d1a9f8cdec9 100644 --- a/packages/source-control-github/src/server/gitHubPullRequestJson.test.ts +++ b/packages/source-control-github/src/server/gitHubPullRequestJson.test.ts @@ -139,9 +139,6 @@ const reviewCommit = ({ commit: { authors, ...commit } }: Json) => ({ commit: { messageHeadline: "", committedDate: "", - additions: 0, - deletions: 0, - parents: { totalCount: 1 }, ...commit, authors: { nodes: nodesIn(authors).map((author) => ({ @@ -465,6 +462,8 @@ describe("pull request detail decoding", () => { nodes: ((raw["commits"] ?? []) as ReadonlyArray>).map( ({ authors, ...commit }) => ({ commit: { + id: "commit-id", + parents: { totalCount: 1 }, ...commit, authors: { nodes: ((authors ?? []) as ReadonlyArray>).map( @@ -658,25 +657,27 @@ describe("review thread decoding", () => { }, }); - it("omits misleading line counts from merge commits", () => { + it("selects stats only for non-merge activity commits", () => { const result = expectSuccess( - decodeReviewThreadsJson( + decodePullRequestActivityJson( JSON.stringify({ data: { repository: { pullRequest: { - reviewThreads: { totalCount: 0, nodes: [] }, + author: null, + comments: { nodes: [], pageInfo: LAST_PAGE }, + reviews: { nodes: [], pageInfo: LAST_PAGE }, commits: { - nodes: [ - { - commit: { - oid: "merge123", - additions: 36_858, - deletions: 12_928, - parents: { totalCount: 2 }, - }, + nodes: [1, 2].map((parents) => ({ + commit: { + id: "commit-" + parents, + oid: "sha-" + parents, + messageHeadline: "Commit", + committedDate: "2026-07-05T00:00:00Z", + parents: { totalCount: parents }, + authors: { nodes: [] }, }, - ], + })), }, }, }, @@ -684,8 +685,8 @@ describe("review thread decoding", () => { }), ), ); - - expect([...result.commitStats]).toEqual([]); + expect(result.nonMergeCommitIds).toEqual(["commit-1"]); + expect(result.commits?.map((commit) => commit.oid)).toEqual(["sha-1", "sha-2"]); }); it("decodes the newest commits off the same connection, oldest to newest", () => { diff --git a/packages/source-control-github/src/server/gitHubPullRequestJson.ts b/packages/source-control-github/src/server/gitHubPullRequestJson.ts index 318ba41468c3e..d4858793c1753 100644 --- a/packages/source-control-github/src/server/gitHubPullRequestJson.ts +++ b/packages/source-control-github/src/server/gitHubPullRequestJson.ts @@ -53,6 +53,7 @@ import type { CheckContext, LabelCandidates, PullRequestActivity, + PullRequestCommitStats, PullRequestCheckContexts, PullRequestCore, PullRequestFilesViewed, @@ -1205,6 +1206,7 @@ export function toPullRequestHeads(result: PullRequestHeads.Result): { } export interface GitHubPullRequestActivityPage { + readonly nonMergeCommitIds: ReadonlyArray; readonly author?: PullRequestActor | null; readonly commits?: ReadonlyArray; readonly remarks: ReadonlyArray; @@ -1219,6 +1221,9 @@ export function toPullRequestActivityPage( const pr = result.repository?.pullRequest; if (pr == null) return null; return { + nonMergeCommitIds: nodesOf(pr.commits).flatMap(({ commit }) => + commit.parents.totalCount > 1 ? [] : [commit.id], + ), ...(pr.author === undefined ? {} : { author: toActor(pr.author) }), ...(pr.commits === undefined ? {} @@ -1239,6 +1244,24 @@ export function toPullRequestActivityPage( }; } +export function toPullRequestCommitStats(result: PullRequestCommitStats.Result) { + return new Map( + (result.nodes ?? []).flatMap((commit) => + commit?.__typename === "Commit" + ? [ + [ + commit.oid, + { + additions: Math.max(0, commit.additions), + deletions: Math.max(0, commit.deletions), + }, + ] as const, + ] + : [], + ), + ); +} + /** A check context as the rollup reports it; opaque outside this module. */ export type GitHubCheckContext = CheckFields; @@ -1286,10 +1309,6 @@ export interface GitHubReviewThreadComments { readonly reviewers: ReadonlyArray; readonly avatarsByLogin: ReadonlyMap; readonly botLogins: ReadonlySet; - readonly commitStats: ReadonlyMap< - string, - { readonly additions: number; readonly deletions: number } - >; /** * Empty wherever the read never happened (an install too old for the field, a degraded page), * which the caller reads as "keep the `gh pr view` list" rather than as "this pull request has @@ -1314,10 +1333,6 @@ export interface GitHubReviewThreadPage { readonly reviewers: ReadonlyArray; readonly avatarsByLogin: ReadonlyMap; readonly botLogins: ReadonlySet; - readonly commitStats: ReadonlyMap< - string, - { readonly additions: number; readonly deletions: number } - >; readonly commits: ReadonlyArray; readonly viewer: { readonly canUpdate: boolean; readonly didAuthor: boolean }; readonly dismissalsByReviewId: ReadonlyMap; @@ -1442,20 +1457,10 @@ export function toReviewThreadPage(result: ReviewThreads.Result): GitHubReviewTh const actor = toActor(raw) ?? toRequestedTeam(raw); if (actor !== null && !reviewers.has(actor.login)) reviewers.set(actor.login, actor); } - const commitStats = new Map(); const commits: PullRequestCommit[] = []; for (const { commit } of nodesOf(pullRequest.commits)) { const oid = trimmed(commit.oid); if (oid === null) continue; - // GitHub measures a merge commit against its first parent, so merging the base into the head - // reports every upstream change as if it belonged to the pull request. There is no useful - // per-commit stat to show for that integration commit without another comparison request. - if (commit.parents.totalCount <= 1) { - commitStats.set(oid, { - additions: Math.max(0, commit.additions), - deletions: Math.max(0, commit.deletions), - }); - } const committedDate = trimmed(commit.committedDate); if (committedDate === null) continue; commits.push({ @@ -1484,7 +1489,6 @@ export function toReviewThreadPage(result: ReviewThreads.Result): GitHubReviewTh reviewers: [...reviewers.values()], avatarsByLogin, botLogins, - commitStats, commits, viewer: toPullRequestViewerFields(pullRequest), dismissalsByReviewId: toDismissalEntries(pullRequest.reviewDismissals), diff --git a/packages/source-control-github/src/server/pullRequestReads.graphql b/packages/source-control-github/src/server/pullRequestReads.graphql index 7281af8c898aa..6695026d083ca 100644 --- a/packages/source-control-github/src/server/pullRequestReads.graphql +++ b/packages/source-control-github/src/server/pullRequestReads.graphql @@ -242,9 +242,13 @@ query PullRequestActivity( commits(last: 100) @include(if: $head) { nodes { commit { + id oid messageHeadline committedDate + parents(first: 1) { + totalCount + } authors(first: 10) { nodes { name @@ -302,6 +306,18 @@ query PullRequestActivity( } } +# Merge commits include upstream changes in their stats, which the timeline does not display. +# Read only the non-merge commits named by the activity connection. +query PullRequestCommitStats($ids: [ID!]!) { + nodes(ids: $ids) { + ... on Commit { + oid + additions + deletions + } + } +} + # Open pull requests whose head branch has one name, from whichever repository it lives in. query PullRequestHeads($owner: String!, $name: String!, $head: String!, $after: String) { repository(owner: $owner, name: $name) { diff --git a/packages/source-control-github/src/server/pullRequestReads.graphql.ts b/packages/source-control-github/src/server/pullRequestReads.graphql.ts index fd0fef547ea9e..70b21de15b370 100644 --- a/packages/source-control-github/src/server/pullRequestReads.graphql.ts +++ b/packages/source-control-github/src/server/pullRequestReads.graphql.ts @@ -131,10 +131,10 @@ export const NodesBot = Schema.Struct({ export interface NodesBot extends Schema.Struct.Type {} /** An object with an ID. */ -export const NodesNodeOther = Schema.Struct({ +export const ActorAvatarsNodesNodeOther = Schema.Struct({ __typename: GraphQL.otherTypename()(["User", "Bot"]) }) -export interface NodesNodeOther extends Schema.Struct.Type {} +export interface ActorAvatarsNodesNodeOther extends Schema.Struct.Type {} /** `query ActorAvatars` */ export const ActorAvatars = GraphQL.query("ActorAvatars", { @@ -155,7 +155,7 @@ export const ActorAvatars = GraphQL.query("ActorAvatars", { nodes: Schema.Array(Schema.NullOr(Schema.Union([ ActorAvatarsNodesUser, NodesBot, - NodesNodeOther + ActorAvatarsNodesNodeOther ]))) }) }) @@ -646,6 +646,13 @@ export declare namespace PullRequestPreview { export type Result = typeof PullRequestPreview.result.Type } +/** The connection type for Commit. */ +export const CommitConnection = Schema.Struct({ + /** Identifies the total count of items in the connection. */ + totalCount: Shared.Int +}) +export interface CommitConnection extends Schema.Struct.Type {} + /** A user is an individual's account on GitHub that owns repositories and can make new content. */ export const UserUser = Schema.Struct({ /** The username used to login. */ @@ -673,12 +680,16 @@ export interface GitActorConnection extends Schema.Struct.Type {} + +/** An object with an ID. */ +export const PullRequestCommitStatsNodesNodeOther = Schema.Struct({ + __typename: GraphQL.otherTypename()(["Commit"]) +}) +export interface PullRequestCommitStatsNodesNodeOther extends Schema.Struct.Type {} + +/** `query PullRequestCommitStats` */ +export const PullRequestCommitStats = GraphQL.query("PullRequestCommitStats", { + document: { + variables: [ + { name: "ids", body: "[ID!]!" } + ], + selections: [ + { name: "nodes", body: ["(ids:$", "ids", "){__typename...on Commit{oid additions deletions}}"] } + ], + fragments: [] + }, + variables: { + ids: Schema.Array(Shared.ID) + }, + result: Schema.Struct({ + /** Lookup nodes by a list of IDs. */ + nodes: Schema.Array(Schema.NullOr(Schema.Union([ + NodesCommit, + PullRequestCommitStatsNodesNodeOther + ]))) + }) +}) +export declare namespace PullRequestCommitStats { + export type Variables = typeof PullRequestCommitStats.variables.Type + export type Result = typeof PullRequestCommitStats.result.Type +} + /** A repository pull request. */ export const NodesPullRequest = Schema.Struct({ /** Identifies the pull request number. */ @@ -1587,4 +1643,4 @@ export declare namespace PullRequestSourceBranch { } /** Every operation in pullRequestReads.graphql. */ -export const PullRequestReadsGraphQLGroup = GraphQLGroup.make(RoutingIdentityViewer, ActorAvatars, PullRequestCore, PullRequestCheckContexts, PullRequestPreview, PullRequestActivity, PullRequestHeads, PullRequestNodeId, PullRequestSubject, ViewerPermissions, ReviewerCandidates, LabelCandidates, PullRequestFilesViewed, PullRequestFileStats, PullRequestMergeMessage, PullRequestActionState, PullRequestBranchToDelete, PullRequestSourceBranch) +export const PullRequestReadsGraphQLGroup = GraphQLGroup.make(RoutingIdentityViewer, ActorAvatars, PullRequestCore, PullRequestCheckContexts, PullRequestPreview, PullRequestActivity, PullRequestCommitStats, PullRequestHeads, PullRequestNodeId, PullRequestSubject, ViewerPermissions, ReviewerCandidates, LabelCandidates, PullRequestFilesViewed, PullRequestFileStats, PullRequestMergeMessage, PullRequestActionState, PullRequestBranchToDelete, PullRequestSourceBranch) diff --git a/packages/source-control-github/src/server/pullRequestReviews.graphql b/packages/source-control-github/src/server/pullRequestReviews.graphql index a1c63c67a27b0..96c35783dc1b6 100644 --- a/packages/source-control-github/src/server/pullRequestReviews.graphql +++ b/packages/source-control-github/src/server/pullRequestReviews.graphql @@ -182,11 +182,6 @@ query ReviewThreads( oid messageHeadline committedDate - additions - deletions - parents(first: 1) { - totalCount - } authors(first: 3) { nodes { name diff --git a/packages/source-control-github/src/server/pullRequestReviews.graphql.ts b/packages/source-control-github/src/server/pullRequestReviews.graphql.ts index aa86ce1fab55a..0711ed9ebcfa5 100644 --- a/packages/source-control-github/src/server/pullRequestReviews.graphql.ts +++ b/packages/source-control-github/src/server/pullRequestReviews.graphql.ts @@ -319,13 +319,6 @@ export const PullRequestTimelineItemsConnection = Schema.Struct({ }) export interface PullRequestTimelineItemsConnection extends Schema.Struct.Type {} -/** The connection type for Commit. */ -export const CommitConnection = Schema.Struct({ - /** Identifies the total count of items in the connection. */ - totalCount: Shared.Int -}) -export interface CommitConnection extends Schema.Struct.Type {} - /** Represents an actor in a Git commit (ie. an author or committer). */ export const GitActor = Schema.Struct({ /** The name in the Git commit. */ @@ -352,12 +345,6 @@ export const Commit = Schema.Struct({ messageHeadline: Schema.String, /** The datetime when this commit was committed. */ committedDate: Shared.DateTime, - /** The number of additions in this commit. */ - additions: Shared.Int, - /** The number of deletions in this commit. */ - deletions: Shared.Int, - /** The parents of a commit. */ - parents: CommitConnection, /** * The list of authors for this commit based on the git author and the Co-authored-by * message trailer. The git author will always be first. @@ -426,7 +413,7 @@ export const ReviewThreads = GraphQL.query("ReviewThreads", { ], selections: [ { name: "viewer", body: ["{login}"] }, - { name: "repository", body: ["(owner:$", "owner", " name:$", "name", "){pullRequest(number:$", "number", "){reviewThreads(first:100 after:$", "after", "){totalCount pageInfo{hasNextPage endCursor}nodes{id isResolved isOutdated path line diffSide comments(first:10){totalCount pageInfo{hasNextPage endCursor}nodes{...ThreadComment}}}}viewerCanUpdate viewerDidAuthor author{__typename login avatarUrl}...Reactions comments(first:100){nodes{id lastEditedAt author{__typename login avatarUrl}...Reactions}}reviews(first:100){nodes{id lastEditedAt author{__typename login avatarUrl}...Reactions}}reviewRequests(first:$", "requestedReviewers", "){nodes{requestedReviewer{__typename...on User{login name avatarUrl}...on Bot{login avatarUrl}...on Team{combinedSlug teamName:name teamAvatarUrl:avatarUrl}}}}latestReviews(first:50){nodes{state author{__typename login avatarUrl}}}reviewDismissals:timelineItems(itemTypes:[REVIEW_DISMISSED_EVENT]first:100){pageInfo{hasNextPage endCursor}nodes{__typename...on ReviewDismissedEvent{dismissalMessage review{id}}}}commits(last:100){nodes{commit{oid messageHeadline committedDate additions deletions parents(first:1){totalCount}authors(first:3){nodes{name avatarUrl user{login}}}}}}}}"] } + { name: "repository", body: ["(owner:$", "owner", " name:$", "name", "){pullRequest(number:$", "number", "){reviewThreads(first:100 after:$", "after", "){totalCount pageInfo{hasNextPage endCursor}nodes{id isResolved isOutdated path line diffSide comments(first:10){totalCount pageInfo{hasNextPage endCursor}nodes{...ThreadComment}}}}viewerCanUpdate viewerDidAuthor author{__typename login avatarUrl}...Reactions comments(first:100){nodes{id lastEditedAt author{__typename login avatarUrl}...Reactions}}reviews(first:100){nodes{id lastEditedAt author{__typename login avatarUrl}...Reactions}}reviewRequests(first:$", "requestedReviewers", "){nodes{requestedReviewer{__typename...on User{login name avatarUrl}...on Bot{login avatarUrl}...on Team{combinedSlug teamName:name teamAvatarUrl:avatarUrl}}}}latestReviews(first:50){nodes{state author{__typename login avatarUrl}}}reviewDismissals:timelineItems(itemTypes:[REVIEW_DISMISSED_EVENT]first:100){pageInfo{hasNextPage endCursor}nodes{__typename...on ReviewDismissedEvent{dismissalMessage review{id}}}}commits(last:100){nodes{commit{oid messageHeadline committedDate authors(first:3){nodes{name avatarUrl user{login}}}}}}}}"] } ], fragments: [ { name: "ThreadComment", body: ["on PullRequestReviewComment{id author{__typename login avatarUrl}body createdAt lastEditedAt url...Reactions}"] }, From 281934175adc5cbbaa8515f4e9b13e50a0ae06bf Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 21 Oct 2026 04:39:15 +0000 Subject: [PATCH 03/14] fix(server): cache large pull request diffs within a byte budget --- .../src/pullRequest/PullRequestService.ts | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index f0c2b580064ad..1329f543b91fe 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -147,9 +147,23 @@ const LIST_CACHE_CAPACITY = 64; const LIST_STATS_CACHE_CAPACITY = 32; const DETAIL_CACHE_CAPACITY = 128; const DIFF_CACHE_CAPACITY = 128; -const MAX_CACHED_DIFF_PATCH_BYTES = 512 * 1024; +// Each holder stays within this UTF-16 patch budget as well as its entry-count bound. +const DIFF_CACHE_PATCH_BUDGET_BYTES = 32 * 1024 * 1024; const canCacheDiff = (value: PullRequestDiffResult) => - value.patch.length * 2 <= MAX_CACHED_DIFF_PATCH_BYTES; + value.patch.length * 2 <= DIFF_CACHE_PATCH_BUDGET_BYTES; +const diffCacheEvictions = (entries: Iterable) => { + let bytes = 0; + const evictions: string[] = []; + for (const [key, value] of Array.from(entries).toReversed()) { + const size = value.patch.length * 2; + if (bytes + size > DIFF_CACHE_PATCH_BUDGET_BYTES) { + evictions.push(key); + } else { + bytes += size; + } + } + return evictions; +}; const FILES_VIEWED_CACHE_CAPACITY = 128; const VIEWER_CACHE_CAPACITY = 32; @@ -2446,6 +2460,11 @@ export const make = Effect.gen(function* () { if (oldest !== undefined) held.delete(oldest); } held.set(key, { at, value }); + for (const evicted of diffCacheEvictions( + Array.from(held, ([key, snapshot]) => [key, snapshot.value] as const), + )) { + held.delete(evicted); + } armDiffSweep(); }); const serve = (key: string, read: Effect.Effect) => { @@ -3072,17 +3091,15 @@ export const make = Effect.gen(function* () { : null, ]); const read = Cache.get(diffCache, key).pipe( - Effect.tap((value) => - canCacheDiff(value) - ? Effect.void - : Cache.getSuccess(diffCache, key).pipe( - Effect.flatMap((current) => - Option.isSome(current) && current.value === value - ? Cache.invalidate(diffCache, key) - : Effect.void, - ), - Effect.uninterruptible, + Effect.tap(() => + Cache.entries(diffCache).pipe( + Effect.flatMap((entries) => + Effect.forEach(diffCacheEvictions(entries), (evicted) => + Cache.invalidate(diffCache, evicted), ), + ), + Effect.uninterruptible, + ), ), Effect.ensuring(Effect.sync(armDiffSweep)), ); From d64eb8baa13d34e58d1260a326f0e6eeeb23f379 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 21 Oct 2026 06:10:55 +0000 Subject: [PATCH 04/14] perf(web): retained pull request tabs skip unchanged renders --- .../pullRequest/PullRequestDetailPanel.tsx | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index d35c85caf30dd..765917d465074 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -54,6 +54,7 @@ import { } from "lucide-react"; import { lazy, + memo, Suspense, type MouseEvent as ReactMouseEvent, useEffect, @@ -68,6 +69,7 @@ import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { type PullRequestDetailTab, useRightPanelStore } from "~/rightPanelStore"; import { useEnvironmentSettings } from "~/hooks/useSettings"; import { useNewThreadHandler } from "~/hooks/useHandleNewThread"; +import { useStableCallback } from "~/hooks/useStableCallback"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { isCommandPaletteOpen } from "~/commandPaletteBus"; import { @@ -245,7 +247,9 @@ const TABS: ReadonlyArray<{ value: PullRequestDetailTab; label: string }> = [ // The diff viewer pulls in its worker pool, so load it only when the reader approaches Code. const loadCodeTab = () => import("./PullRequestCodeTab"); -const PullRequestCodeTab = lazy(loadCodeTab); +const PullRequestCodeTab = memo(lazy(loadCodeTab)); +const RetainedSummaryTab = memo(PullRequestSummaryTab); +const RetainedTimelineTab = memo(PullRequestTimelineTab); /** * What the last hand-off wrote into each draft, kept outside React because the panel that wrote it @@ -657,13 +661,13 @@ export function PullRequestDetailPanel({ }>(() => ({ pullRequestKey, oid: null })); const selectedCodeCommitOid = codeCommitScope.pullRequestKey === pullRequestKey ? codeCommitScope.oid : null; - const selectCodeCommit = (oid: string | null) => { + const selectCodeCommit = useStableCallback((oid: string | null) => { setCodeCommitScope({ pullRequestKey, oid }); - }; - const openCommit = (oid: string) => { + }); + const openCommit = useStableCallback((oid: string) => { selectCodeCommit(oid); selectTab("code"); - }; + }); const [codeRevealRequest, setCodeRevealRequest] = useState<{ readonly path: string; readonly nonce: number; @@ -843,11 +847,11 @@ export function PullRequestDetailPanel({ }); const activityPending = activityQuery.isPending && activity === null; const activityError = activity === null ? activityQuery.error : null; - const refreshDetail = () => { + const refreshDetail = useStableCallback(() => { detailQuery.refresh(); activityQuery.refresh(); nativeStackQuery.refresh(); - }; + }); const [refreshToken, setRefreshToken] = useState(0); const [activityRevision, setActivityRevision] = useState<{ readonly key: string; @@ -883,14 +887,14 @@ export function PullRequestDetailPanel({ const invalidate = useAtomCommand(pullRequestEnvironment.invalidate, { reportFailure: false }); const [isInvalidating, setIsInvalidating] = useState(false); const refreshing = isInvalidating || detailQuery.isPending; - const refreshFromHost = async () => { + const refreshFromHost = useStableCallback(async () => { setIsInvalidating(true); // Atom commands settle instead of throwing, so the reset below always runs. await invalidate({ environmentId, input: { reference } }); refreshDetail(); setRefreshToken((token) => token + 1); setIsInvalidating(false); - }; + }); const refreshFromHostOnRequest = useEffectEvent(() => void refreshFromHost()); const appliedForcedToken = useRef(forcedRefreshToken); useEffect(() => { @@ -1349,34 +1353,33 @@ export function PullRequestDetailPanel({ ); }; - const addSelectionToAgent = ( - selection: PullRequestAgentSelectionInput, - options?: { readonly keepGuide?: boolean }, - ) => { - if (!detail) return; - void startAsk( - `selection:${selection.comment.id}`, - buildAddSelectionToAgentHandoff({ - number: detail.number, - title: detail.title, - url: detail.url, - headBranch: detail.headBranch, - baseBranch: detail.baseBranch, - state: detail.state, - isDraft: detail.isDraft, - comment: selection.comment, - request: selection.request, - }), - options, - ); - }; + const addSelectionToAgent = useStableCallback( + (selection: PullRequestAgentSelectionInput, options?: { readonly keepGuide?: boolean }) => { + if (!detail) return; + void startAsk( + `selection:${selection.comment.id}`, + buildAddSelectionToAgentHandoff({ + number: detail.number, + title: detail.title, + url: detail.url, + headBranch: detail.headBranch, + baseBranch: detail.baseBranch, + state: detail.state, + isDraft: detail.isDraft, + comment: selection.comment, + request: selection.request, + }), + options, + ); + }, + ); const startCheckout = (mode: "worktree" | "local") => { if (!handoffSummary) return; void startHandoff(`checkout:${mode}`, null, mode); }; - const startFixFinding = (finding: PullRequestFinding) => { + const startFixFinding = useStableCallback((finding: PullRequestFinding) => { if (!detail) return; void startHandoff( pullRequestFindingKey(finding), @@ -1389,7 +1392,7 @@ export function PullRequestDetailPanel({ finding, }), ); - }; + }); const startFixFindings = () => { if (!detail) return; @@ -2814,7 +2817,7 @@ export function PullRequestDetailPanel({ {mountedTabs.has("summary") ? (
- ) : ( - Date: Sun, 25 Oct 2026 02:13:29 +0000 Subject: [PATCH 05/14] perf(web): reuse pull request parses after reopening --- .../components/pullRequest/PullRequestCodeTab.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index a914910e94a0a..e3a434d0a5350 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -4,6 +4,7 @@ import type { EnvironmentId, PullRequestDetailView, PullRequestDiffSide, + PullRequestDiffResult, PullRequestOmittedFileStat, PullRequestRef, PullRequestReviewPosition, @@ -126,6 +127,7 @@ const COMMIT_PAGE_SIZE = 10; const PULL_REQUEST_FILE_TREE_STORAGE_KEY = "t3code.pullRequestFileTreeOpen"; interface DiffSlice { + readonly source: PullRequestDiffResult; readonly cursor: string | null; readonly patch: string; readonly truncated: boolean; @@ -146,15 +148,15 @@ const REPLACE_FILE_COUNTS_CSS = ` const NO_SLICES: ReadonlyArray = []; -// Each slice is parsed once per cache key and the parse lives as long as the slice does, so a -// slice arriving costs one parse rather than one per slice already on screen. -const sliceParses = new WeakMap>(); +// Query results outlive the panel's slices, so their parses survive closing and reopening +// without retaining anything beyond the query cache's lifetime. +const sliceParses = new WeakMap>(); function parseSlice(slice: DiffSlice, cacheKey: string, ignoreWhitespace: boolean) { - let parses = sliceParses.get(slice); + let parses = sliceParses.get(slice.source); if (parses === undefined) { parses = new Map(); - sliceParses.set(slice, parses); + sliceParses.set(slice.source, parses); } if (parses.has(cacheKey)) return parses.get(cacheKey) ?? null; const parsed = getRenderablePatch(slice.patch, cacheKey, { @@ -358,6 +360,7 @@ function PullRequestCodeTab({ setSliceState((previous) => { const slices = previous.key === scopeKey ? previous.slices : NO_SLICES; const next = { + source: data, cursor, patch: data.patch, truncated: data.truncated, From 205fab396867e4a89a63a75e57e3478df2bedb54 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 02:32:41 +0000 Subject: [PATCH 06/14] revert(web): drop retained tab memoization without a production gain This reverts commit d64eb8baa13d34e58d1260a326f0e6eeeb23f379. --- .../pullRequest/PullRequestDetailPanel.tsx | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 6169e974f17d3..3d1d8686ae05b 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -36,7 +36,6 @@ import { } from "lucide-react"; import { lazy, - memo, Suspense, useEffect, useEffectEvent, @@ -49,7 +48,6 @@ import { type DraftId, useComposerDraftStore } from "~/composerDraftStore"; import { type PullRequestDetailTab, useRightPanelStore } from "~/rightPanelStore"; import { useEnvironmentSettings } from "~/hooks/useSettings"; import { useNewThreadHandler } from "~/hooks/useHandleNewThread"; -import { useStableCallback } from "~/hooks/useStableCallback"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { isCommandPaletteOpen } from "~/commandPaletteBus"; import { resolveShortcutCommand, type ShortcutMatchContext } from "~/keybindings"; @@ -203,9 +201,7 @@ const TABS: ReadonlyArray<{ value: PullRequestDetailTab; label: string }> = [ // Defer the diff viewer's worker pool until Code is approached. const loadCodeTab = () => import("./PullRequestCodeTab"); -const PullRequestCodeTab = memo(lazy(loadCodeTab)); -const RetainedSummaryTab = memo(PullRequestSummaryTab); -const RetainedTimelineTab = memo(PullRequestTimelineTab); +const PullRequestCodeTab = lazy(loadCodeTab); // Persist handoffs across unmounts; replace only unchanged contributions to preserve user edits. const lastHandoffPromptByDraft = new Map(); @@ -333,13 +329,13 @@ export function PullRequestDetailPanel({ }>(() => ({ pullRequestKey, oid: null })); const selectedCodeCommitOid = codeCommitScope.pullRequestKey === pullRequestKey ? codeCommitScope.oid : null; - const selectCodeCommit = useStableCallback((oid: string | null) => { + const selectCodeCommit = (oid: string | null) => { setCodeCommitScope({ pullRequestKey, oid }); - }); - const openCommit = useStableCallback((oid: string) => { + }; + const openCommit = (oid: string) => { selectCodeCommit(oid); selectTab("code"); - }); + }; const [codeRevealRequest, setCodeRevealRequest] = useState<{ readonly path: string; readonly nonce: number; @@ -524,11 +520,11 @@ export function PullRequestDetailPanel({ }); const activityPending = activityQuery.isPending && activity === null; const activityError = activity === null ? activityQuery.error : null; - const refreshDetail = useStableCallback(() => { + const refreshDetail = () => { detailQuery.refresh(); activityQuery.refresh(); nativeStackQuery.refresh(); - }); + }; const [refreshToken, setRefreshToken] = useState(0); const [activityRevision, setActivityRevision] = useState<{ readonly key: string; @@ -563,14 +559,14 @@ export function PullRequestDetailPanel({ const invalidate = useAtomCommand(pullRequestEnvironment.invalidate, { reportFailure: false }); const [isInvalidating, setIsInvalidating] = useState(false); const refreshing = isInvalidating || detailQuery.isPending; - const refreshFromHost = useStableCallback(async () => { + const refreshFromHost = async () => { setIsInvalidating(true); // Atom commands settle without throwing, so reset always runs. await invalidate({ environmentId, input: { reference } }); refreshDetail(); setRefreshToken((token) => token + 1); setIsInvalidating(false); - }); + }; const refreshFromHostOnRequest = useEffectEvent(() => void refreshFromHost()); const appliedForcedToken = useRef(forcedRefreshToken); useEffect(() => { @@ -990,33 +986,34 @@ export function PullRequestDetailPanel({ ); }; - const addSelectionToAgent = useStableCallback( - (selection: PullRequestAgentSelectionInput, options?: { readonly keepGuide?: boolean }) => { - if (!detail) return; - void startAsk( - `selection:${selection.comment.id}`, - buildAddSelectionToAgentHandoff({ - number: detail.number, - title: detail.title, - url: detail.url, - headBranch: detail.headBranch, - baseBranch: detail.baseBranch, - state: detail.state, - isDraft: detail.isDraft, - comment: selection.comment, - request: selection.request, - }), - options, - ); - }, - ); + const addSelectionToAgent = ( + selection: PullRequestAgentSelectionInput, + options?: { readonly keepGuide?: boolean }, + ) => { + if (!detail) return; + void startAsk( + `selection:${selection.comment.id}`, + buildAddSelectionToAgentHandoff({ + number: detail.number, + title: detail.title, + url: detail.url, + headBranch: detail.headBranch, + baseBranch: detail.baseBranch, + state: detail.state, + isDraft: detail.isDraft, + comment: selection.comment, + request: selection.request, + }), + options, + ); + }; const startCheckout = (mode: "worktree" | "local") => { if (!handoffSummary) return; void startHandoff(`checkout:${mode}`, null, mode); }; - const startFixFinding = useStableCallback((finding: PullRequestFinding) => { + const startFixFinding = (finding: PullRequestFinding) => { if (!detail) return; void startHandoff( pullRequestFindingKey(finding), @@ -1029,7 +1026,7 @@ export function PullRequestDetailPanel({ finding, }), ); - }); + }; const startFixFindings = () => { if (!detail) return; @@ -1708,7 +1705,7 @@ export function PullRequestDetailPanel({ {mountedTabs.has("summary") ? (
- ) : ( - Date: Sun, 25 Oct 2026 03:50:07 +0000 Subject: [PATCH 07/14] perf(web): observe pull request comment heights without forced layout --- .../src/components/pullRequest/PullRequestCommentBody.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestCommentBody.tsx b/apps/web/src/components/pullRequest/PullRequestCommentBody.tsx index 12715a9efe319..73554b41ad91c 100644 --- a/apps/web/src/components/pullRequest/PullRequestCommentBody.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCommentBody.tsx @@ -17,9 +17,9 @@ export function PullRequestCommentBody({ useEffect(() => { const element = content.current; if (!element) return; - const measure = () => setOverflowing(element.getBoundingClientRect().height > 240); - measure(); - return observeResize(element, measure); + return observeResize(element, ([entry]) => { + if (entry) setOverflowing(entry.contentRect.height > 240); + }); }, []); return ( From 8cc157b42ad01da3006805262a74c76f0f146e83 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 03:51:36 +0000 Subject: [PATCH 08/14] perf(web): submit pull request searches immediately --- .../components/pullRequest/PullRequestListFilters.tsx | 10 ++++++++++ .../src/components/pullRequest/PullRequestsPage.tsx | 2 ++ .../components/pullRequest/usePullRequestListView.ts | 10 +++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestListFilters.tsx b/apps/web/src/components/pullRequest/PullRequestListFilters.tsx index e1fc6d60bd65d..912e4c6d7ac4d 100644 --- a/apps/web/src/components/pullRequest/PullRequestListFilters.tsx +++ b/apps/web/src/components/pullRequest/PullRequestListFilters.tsx @@ -97,10 +97,12 @@ export function PullRequestSearchInput({ value, busy, onChange, + onSubmit, }: { value: string; busy?: boolean; onChange: (value: string) => void; + onSubmit: (value: string) => void; }) { const [draft, setDraft] = useState(null); return ( @@ -116,6 +118,14 @@ export function PullRequestSearchInput({ onChange={(event) => { setDraft(event.currentTarget.value); onChange(event.currentTarget.value); + if ((event.nativeEvent as InputEvent).inputType === "insertFromPaste") { + onSubmit(event.currentTarget.value); + } + }} + onKeyDown={(event) => { + if (event.key === "Enter" && !event.nativeEvent.isComposing) { + onSubmit(event.currentTarget.value); + } }} placeholder="Search pull requests, or label:bug" aria-label="Search pull requests" diff --git a/apps/web/src/components/pullRequest/PullRequestsPage.tsx b/apps/web/src/components/pullRequest/PullRequestsPage.tsx index 84bfd8959175e..719711f78c417 100644 --- a/apps/web/src/components/pullRequest/PullRequestsPage.tsx +++ b/apps/web/src/components/pullRequest/PullRequestsPage.tsx @@ -248,6 +248,7 @@ export function PullRequestsPage() { const [filtersOpen, setFiltersOpen] = useState(false); const { sentQuery, + submitQuery, querySettled, typedParsed, sentParsed, @@ -1076,6 +1077,7 @@ export function PullRequestsPage() { value={search.q ?? ""} busy={typedQuery.length > 0 && (!querySettled || showingCarried)} onChange={(query) => updateListScope({ q: query || undefined })} + onSubmit={submitQuery} /> ); const panelToggleControls = ( diff --git a/apps/web/src/components/pullRequest/usePullRequestListView.ts b/apps/web/src/components/pullRequest/usePullRequestListView.ts index 2629fb6a34b30..36451f6eab961 100644 --- a/apps/web/src/components/pullRequest/usePullRequestListView.ts +++ b/apps/web/src/components/pullRequest/usePullRequestListView.ts @@ -8,11 +8,10 @@ import { type PullRequestListResult, type PullRequestListState, } from "@t3tools/contracts"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useAllEnvironmentShellsBootstrapped, useProjects } from "~/state/entities"; import { useEnvironments } from "~/state/environments"; -import { useDebouncedValue } from "~/state/queries"; import { applyPullRequestOverrides, findProjectForRepository, @@ -185,7 +184,11 @@ export function usePullRequestScope(search: PullRequestsSearch) { } export function usePullRequestQueryFilters(search: PullRequestsSearch, typedQuery: string) { - const sentQuery = useDebouncedValue(typedQuery, SEARCH_DEBOUNCE_MS); + const [sentQuery, setSentQuery] = useState(typedQuery); + useEffect(() => { + const timer = window.setTimeout(() => setSentQuery(typedQuery), SEARCH_DEBOUNCE_MS); + return () => window.clearTimeout(timer); + }, [typedQuery]); const typedParsed = parsePullRequestQuery(typedQuery); const sentParsed = parsePullRequestQuery(sentQuery); @@ -203,6 +206,7 @@ export function usePullRequestQueryFilters(search: PullRequestsSearch, typedQuer const hasLocalFilters = Object.keys(localFilters).length > 0; return { sentQuery, + submitQuery: (query: string) => setSentQuery(query.trim()), querySettled: typedQuery === sentQuery, typedParsed, sentParsed, From 96fd606e5ae71fea129e797da627097e3f12bf7c Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 03:53:24 +0000 Subject: [PATCH 09/14] perf(pull-requests): panel opens share one detail read and start side reads early --- .../pullRequest/PullRequestService.test.ts | 45 +++++++++++++++++++ .../src/pullRequest/PullRequestService.ts | 12 +++++ apps/web/src/components/RightPanelTabs.tsx | 1 + .../pullRequest/PullRequestDetailPanel.tsx | 17 +++++++ .../src/hooks/useOpenPanelPullRequestUrl.ts | 2 +- 5 files changed, 76 insertions(+), 1 deletion(-) diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index b569e7e531d30..d4f1c98578f24 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -2663,6 +2663,9 @@ it.effect("isolates routed caches for two credentials belonging to the same acco host: "github.com", expectedAccountId: "101", }; + if (operation === "checks") { + yield* service.withRoutingCredential(reference, service.detail(reference)); + } yield* service.withRoutingCredential(reference, readOperation(reference)); credential = "restricted"; for (const allowStale of [false, true]) { @@ -2838,6 +2841,48 @@ it.effect("deduplicates routed PR reports without dropping draft changes", () => }), ); +it.effect( + "reuses fresh detail checks without extending their refresh interval on detail cache hits", + () => + Effect.gen(function* () { + let reads = 0; + const reference = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }; + const service = yield* makeService({ + projects: [ + project({ id: "p1", title: "web", workspaceRoot: "/a", repository: "acme/web" }), + ], + providers: [ + fakePullRequestProvider(SourceControlProviderKind.make("github"), { + getChangeRequest: () => Effect.succeed(hostedChangeRequest("Description")), + getChangeRequestChecks: () => + Effect.sync(() => { + reads += 1; + return { state: "closed" as const, checks: [] }; + }), + }), + ], + }); + yield* TestClock.setTime(1_000); + const detail = yield* service.detail(reference); + expect(yield* service.checks(reference)).toEqual({ + state: detail.state, + checks: detail.checks, + observedAt: detail.observedAt, + }); + expect(reads).toBe(0); + yield* TestClock.adjust("16 seconds"); + yield* service.detail(reference); + expect(yield* service.checks(reference)).toMatchObject({ + state: "closed", + observedAt: 17_000, + }); + expect(reads).toBe(1); + yield* service.detail(reference); + expect((yield* service.checks(reference))?.state).toBe("closed"); + expect(reads).toBe(1); + }), +); + it.effect("preserves check observation time on cache hits and advances it on refresh", () => Effect.gen(function* () { let reads = 0; diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 7f44c3d906a29..469ba67dd508b 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -3013,6 +3013,18 @@ export const make = Effect.gen(function* () { return detailUncached(refOfCacheKey(key)).pipe( Effect.tap( Effect.fn("PullRequestService.recordDetailStats")(function* (value: PullRequestDetail) { + // GitHub detail includes the same checks; seed only fresh reads at their shorter TTL. + if ( + value.provider === "github" && + value.observedAt !== undefined && + !(yield* Cache.has(checksCache, key)) + ) { + yield* Cache.set(checksCache, key, { + state: value.state, + checks: value.checks, + observedAt: value.observedAt, + }); + } recordStats( statsKey, { diff --git a/apps/web/src/components/RightPanelTabs.tsx b/apps/web/src/components/RightPanelTabs.tsx index fdbe904ad489a..de548692f88c2 100644 --- a/apps/web/src/components/RightPanelTabs.tsx +++ b/apps/web/src/components/RightPanelTabs.tsx @@ -726,6 +726,7 @@ function PullRequestSurfaceIcon({ : {}), repository: surface.repository, number: surface.number, + allowStale: false, }, }), ).data; diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 3d1d8686ae05b..9c3faaf31677b 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -1,4 +1,5 @@ import { projectChangeRequestUrl } from "@t3tools/client-runtime/change-request-links"; +import { normalizeThreadPullRequestKey } from "@t3tools/client-runtime/thread-pull-requests"; import { changeRequestReferenceAutolinker } from "@t3tools/client-runtime/pull-request-markdown"; import { useAtomValue } from "@effect/atom-react"; import { usePullRequestStack } from "~/state/usePullRequestStack"; @@ -315,6 +316,22 @@ export function PullRequestDetailPanel({ listEntry.number === reference.number ? listEntry : null; + const linkedReference = sourceControlClients.parseChangeRequestUrl( + matchingListEntry?.url ?? unavailableHostUrl ?? "", + )?.link; + useEnvironmentQuery( + context === "page" && + supportsThreadPullRequests && + linkedReference && + (reference.host === undefined || + reference.host.toLowerCase() === linkedReference.host.toLowerCase()) + ? pullRequestEnvironment.linkedThreads({ + environmentId, + input: { ...reference, ...normalizeThreadPullRequestKey(linkedReference) }, + }) + : null, + ); + useEnvironmentQuery(pullRequestEnvironment.changeBreakdown({ environmentId, input: reference })); const [threadPickerOpen, setThreadPickerOpen] = useState(false); const [openReviewRequest, setOpenReviewRequest] = useState(0); const [tab, setTab] = useState(initialTab); diff --git a/apps/web/src/hooks/useOpenPanelPullRequestUrl.ts b/apps/web/src/hooks/useOpenPanelPullRequestUrl.ts index 791e4ad3a6d4e..7be2020a804d9 100644 --- a/apps/web/src/hooks/useOpenPanelPullRequestUrl.ts +++ b/apps/web/src/hooks/useOpenPanelPullRequestUrl.ts @@ -75,7 +75,7 @@ export function useOpenPanelPullRequestUrl(threadRef: ScopedThreadRef | null) { reference && environmentId ? pullRequestEnvironment.detail({ environmentId, - input: reference, + input: { ...reference, allowStale: false }, }) : null, ).data; From dcac82c96cbec53370915295377c5b5801221440 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 04:39:30 +0000 Subject: [PATCH 10/14] fix(server): checks seeded from detail skip observations older than their TTL --- apps/server/src/pullRequest/PullRequestService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 469ba67dd508b..bae28f39d42c6 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -3014,9 +3014,11 @@ export const make = Effect.gen(function* () { Effect.tap( Effect.fn("PullRequestService.recordDetailStats")(function* (value: PullRequestDetail) { // GitHub detail includes the same checks; seed only fresh reads at their shorter TTL. + const now = yield* Clock.currentTimeMillis; if ( value.provider === "github" && value.observedAt !== undefined && + now - value.observedAt < Duration.toMillis(CHECKS_CACHE_TTL) && !(yield* Cache.has(checksCache, key)) ) { yield* Cache.set(checksCache, key, { @@ -3034,7 +3036,7 @@ export const make = Effect.gen(function* () { additions: value.additions, deletions: value.deletions, }, - yield* Clock.currentTimeMillis, + now, ); }), ), From 322257fb5f3a1904c0edf1e462c19720a16fffcc Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 05:21:16 +0000 Subject: [PATCH 11/14] fix(web): keep pull request row totals in sync with the panel --- .../pullRequest/PullRequestsPage.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/web/src/components/pullRequest/PullRequestsPage.tsx b/apps/web/src/components/pullRequest/PullRequestsPage.tsx index 719711f78c417..57ae57924c998 100644 --- a/apps/web/src/components/pullRequest/PullRequestsPage.tsx +++ b/apps/web/src/components/pullRequest/PullRequestsPage.tsx @@ -26,6 +26,7 @@ import { collectPullRequestListFacets, narrowPullRequestsToFilters, mergePullRequestDiffStats, + pullRequestDiffStatKey, pullRequestEntryKey, rankPullRequestMatches, readPullRequestListSnapshot, @@ -104,6 +105,7 @@ import { usePullRequestList, usePullRequestListStats, usePullRequestTurnRefreshes, + useSharedPullRequestSummary, type EnvironmentQueryTarget, } from "~/state/pullRequests"; import { useAtomCommand } from "~/state/use-atom-command"; @@ -195,6 +197,16 @@ export function PullRequestsPage() { (renderedPullRequestSurface?.environmentId as EnvironmentId | undefined) ?? selectedProject?.environmentId ?? null; + const panelSummary = useSharedPullRequestSummary( + panelEnvironmentId, + renderedPullRequestSurface === null + ? null + : { + ...renderedPullRequestSurface, + projectId: renderedPullRequestSurface.projectId as ProjectId, + }, + null, + ); const updateSearch = (patch: { [Key in keyof PullRequestsSearch]?: PullRequestsSearch[Key] | undefined; }) => @@ -711,6 +723,22 @@ export function PullRequestsPage() { const visibleStatsKeys = useRef({ key: filterKey, values: new Set() }); const [statsByRow, setStatsByRow] = useState(() => new Map()); + if ( + panelEnvironmentId !== null && + panelSummary?.additions !== undefined && + panelSummary.deletions !== undefined + ) { + const stat = { + ...panelSummary, + environmentId: panelEnvironmentId, + additions: panelSummary.additions, + deletions: panelSummary.deletions, + }; + const previous = statsByRow.get(pullRequestDiffStatKey(stat)); + if (previous?.additions !== stat.additions || previous.deletions !== stat.deletions) { + setStatsByRow(mergePullRequestDiffStats(statsByRow, [stat])); + } + } const [statsTargetState, setStatsTargetState] = useState<{ readonly key: string; readonly batches: ReadonlyArray; From ce47834a2750a4f4fb72b4392c2df361a99f5f71 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 05:40:38 +0000 Subject: [PATCH 12/14] fix(server): expire cached pull request diffs from read start --- .../pullRequest/PullRequestService.test.ts | 29 +++++++++++++++++-- .../src/pullRequest/PullRequestService.ts | 27 +++++++++++------ 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.test.ts b/apps/server/src/pullRequest/PullRequestService.test.ts index 2f49f55a5f098..86cfda3ee1100 100644 --- a/apps/server/src/pullRequest/PullRequestService.test.ts +++ b/apps/server/src/pullRequest/PullRequestService.test.ts @@ -4179,7 +4179,7 @@ it.effect.each(["commit", "credential"] as const)( }), ); -it.effect.each(["expiry", "refresh"] as const)( +it.effect.each(["expiry", "commit expiry", "cache hit", "commit cache hit", "refresh"] as const)( "keeps a loaded diff's original membership scope through an in-flight %s", (boundary) => Effect.gen(function* () { @@ -4203,17 +4203,40 @@ it.effect.each(["expiry", "refresh"] as const)( nextCursor: null, }; }), - getDiffFileContents: () => Effect.die("an expired file must not be read"), + getDiffFileContents: () => Effect.succeed({ oldContents: "old", newContents: "new" }), }), ], }); - const ref = { projectId: "p1" as ProjectId, repository: "acme/web", number: 1 }; + const ref = { + projectId: "p1" as ProjectId, + repository: "acme/web", + number: 1, + ...(boundary.includes("commit") ? { commit: "a".repeat(40) } : {}), + }; const loading = yield* Effect.forkChild(service.diff(ref)); yield* Deferred.await(started); if (boundary === "expiry") yield* TestClock.adjust("61 seconds"); + else if (boundary === "commit expiry") yield* TestClock.adjust("11 minutes"); + else if (boundary.includes("cache hit")) yield* TestClock.adjust("30 seconds"); else yield* service.invalidate({ reference: ref }); yield* Deferred.succeed(release, undefined); yield* Fiber.join(loading); + if (boundary.includes("cache hit")) { + yield* service.diff(ref); + yield* service.diff(ref); + yield* TestClock.adjust(boundary.includes("commit") ? "9 minutes" : "29 seconds"); + assert.deepStrictEqual( + yield* service.diffFileContents({ + ...ref, + changeType: "change", + oldPath: "private.ts", + newPath: "private.ts", + }), + { oldContents: "old", newContents: "new" }, + ); + assert.strictEqual(diffReads, 1); + yield* TestClock.adjust(boundary.includes("commit") ? "1 minute" : "2 seconds"); + } const error = yield* service .diffFileContents({ ...ref, diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 6216751eb1e0f..2486d5d7c5ca3 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -691,6 +691,7 @@ export const make = Effect.gen(function* () { const rateLimits = yield* SourceControlRateLimit.SourceControlRateLimit; const filesViewedStore = yield* PullRequestFilesViewed.PullRequestFilesViewedRepository; const readCache = yield* PullRequestReadCache.PullRequestReadCache; + const clock = yield* Clock.Clock; /** * Where each project's remote is served from, by the remote's recognised base URL: a remote no @@ -3237,10 +3238,10 @@ export const make = Effect.gen(function* () { ...(commit === null ? {} : { commit }), } as PullRequestDiffInput).pipe( observeRead, - Effect.map(({ value, observedAt }) => { + Effect.tap(({ value, observedAt }) => { // Keep the loader's epoch and read time if a refresh occurs during the provider read. diffFiles.record(JSON.stringify([reference, cursor, commit]), value, observedAt); - return value; + return Effect.void; }), ); }, @@ -3249,7 +3250,10 @@ export const make = Effect.gen(function* () { timeToLive: (exit, key) => { if (!Exit.isSuccess(exit)) return Duration.zero; const commit = (JSON.parse(key) as ReadonlyArray)[2]; - return commit === null ? DIFF_CACHE_TTL : COMMIT_DIFF_CACHE_TTL; + const ttl = Duration.toMillis(commit === null ? DIFF_CACHE_TTL : COMMIT_DIFF_CACHE_TTL); + return Duration.millis( + Math.max(0, exit.value.observedAt + ttl - clock.currentTimeMillisUnsafe()), + ); }, }, ); @@ -3275,7 +3279,7 @@ export const make = Effect.gen(function* () { if (held > 0) armDiffSweep(); }), ); - const diff: PullRequestService["Service"]["diff"] = (input) => { + const readDiff = (input: PullRequestDiffInput) => { const key = JSON.stringify([ refCacheKey(input), input.cursor ?? null, @@ -3288,8 +3292,9 @@ export const make = Effect.gen(function* () { Effect.tap(() => Cache.entries(diffCache).pipe( Effect.flatMap((entries) => - Effect.forEach(diffCacheEvictions(entries), (evicted) => - Cache.invalidate(diffCache, evicted), + Effect.forEach( + diffCacheEvictions(Array.from(entries, ([key, read]) => [key, read.value] as const)), + (evicted) => Cache.invalidate(diffCache, evicted), ), ), Effect.uninterruptible, @@ -3297,7 +3302,11 @@ export const make = Effect.gen(function* () { ), Effect.ensuring(Effect.sync(armDiffSweep)), ); - return staleDiff.serve(key, read); + return { key, read }; + }; + const diff: PullRequestService["Service"]["diff"] = (input) => { + const { key, read } = readDiff(input); + return staleDiff.serve(key, read.pipe(Effect.map((read) => read.value))); }; const filesViewedCache = yield* Cache.makeWith( @@ -3529,8 +3538,8 @@ export const make = Effect.gen(function* () { const now = yield* Clock.currentTimeMillis; const cached = held.get(key); if (cached !== undefined && now - cached.at <= ttlMs) return cached; - const slice = yield* diff(input); - return record(key, slice, now); + const { value, observedAt } = yield* readDiff(input).read; + return record(key, value, observedAt); }); return { read: credentialCached(read), record, fileKey }; })(); From f3f2bf080462399f1bc21de6f3839761f58f5e3e Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 05:49:58 +0000 Subject: [PATCH 13/14] fix(server): pull request checks expire from when they were observed --- .../src/pullRequest/PullRequestService.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index 2486d5d7c5ca3..b3b181df6dc81 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -2980,7 +2980,20 @@ export const make = Effect.gen(function* () { }, { capacity: DETAIL_CACHE_CAPACITY, - timeToLive: (exit) => (Exit.isSuccess(exit) ? CHECKS_CACHE_TTL : Duration.zero), + // Expire from the read's start so checks seeded from a slow detail read keep their age. + timeToLive: (exit) => + !Exit.isSuccess(exit) + ? Duration.zero + : exit.value === null + ? CHECKS_CACHE_TTL + : Duration.millis( + Math.max( + 0, + exit.value.observedAt + + Duration.toMillis(CHECKS_CACHE_TTL) - + clock.currentTimeMillisUnsafe(), + ), + ), }, ); @@ -3014,12 +3027,10 @@ export const make = Effect.gen(function* () { return detailUncached(refOfCacheKey(key)).pipe( Effect.tap( Effect.fn("PullRequestService.recordDetailStats")(function* (value: PullRequestDetail) { - // GitHub detail includes the same checks; seed only fresh reads at their shorter TTL. - const now = yield* Clock.currentTimeMillis; + // GitHub detail includes the same checks; the checks TTL counts from their observation. if ( value.provider === "github" && value.observedAt !== undefined && - now - value.observedAt < Duration.toMillis(CHECKS_CACHE_TTL) && !(yield* Cache.has(checksCache, key)) ) { yield* Cache.set(checksCache, key, { @@ -3037,7 +3048,7 @@ export const make = Effect.gen(function* () { additions: value.additions, deletions: value.deletions, }, - now, + yield* Clock.currentTimeMillis, ); }), ), From d928f0ff82e6faf1a744dcf0ba001da772cd5b48 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 25 Oct 2026 17:17:15 +0000 Subject: [PATCH 14/14] fix(server): pull request diff cache evicts even when a read is interrupted --- .../src/pullRequest/PullRequestService.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/server/src/pullRequest/PullRequestService.ts b/apps/server/src/pullRequest/PullRequestService.ts index fb8bc4a4c85d2..e967b18ef8c3b 100644 --- a/apps/server/src/pullRequest/PullRequestService.ts +++ b/apps/server/src/pullRequest/PullRequestService.ts @@ -3303,18 +3303,17 @@ export const make = Effect.gen(function* () { ? (lastGoodSummary.peek(refCacheKey(input))?.updatedAt ?? null) : null, ]); - const read = Cache.get(diffCache, key).pipe( - Effect.tap(() => - Cache.entries(diffCache).pipe( - Effect.flatMap((entries) => - Effect.forEach( - diffCacheEvictions(Array.from(entries, ([key, read]) => [key, read.value] as const)), - (evicted) => Cache.invalidate(diffCache, evicted), - ), - ), - Effect.uninterruptible, + const evictDiffs = Cache.entries(diffCache).pipe( + Effect.flatMap((entries) => + Effect.forEach( + diffCacheEvictions(Array.from(entries, ([key, read]) => [key, read.value] as const)), + (evicted) => Cache.invalidate(diffCache, evicted), ), ), + Effect.uninterruptible, + ); + const read = Cache.get(diffCache, key).pipe( + Effect.ensuring(evictDiffs), Effect.ensuring(Effect.sync(armDiffSweep)), ); return { key, read };