From 5588b4af18b6eb86b6f2cbfcdaa43ee229f6d289 Mon Sep 17 00:00:00 2001 From: Guillermo Casanova Date: Wed, 30 Sep 2026 22:45:43 -0300 Subject: [PATCH] fix(web): PR code tab no longer reloads on background refreshes --- .../pullRequest/PullRequestCodeTab.tsx | 48 ++++++++++++++++++- .../pullRequest/PullRequestDetailPanel.tsx | 9 ++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx index 251f3cd49f95..7c22de506009 100644 --- a/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx +++ b/apps/web/src/components/pullRequest/PullRequestCodeTab.tsx @@ -24,9 +24,17 @@ import { TextWrapIcon, TriangleAlertIcon, } from "lucide-react"; -import { useAtomRefresh } from "@effect/atom-react"; +import { RegistryContext, useAtomRefresh } from "@effect/atom-react"; import * as Schema from "effect/Schema"; -import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { useClientSettings, useUpdateClientSettings } from "~/hooks/useSettings"; @@ -203,6 +211,7 @@ function PullRequestCodeTab({ onAddToAgentSelection, onRefresh, refreshToken = 0, + backgroundRefreshToken = 0, }: { environmentId: EnvironmentId; reference: PullRequestRef; @@ -219,6 +228,7 @@ function PullRequestCodeTab({ onRefresh: () => void; /** Bumped by the panel's refresh button: drop the accumulated pages and re-read the diff. */ refreshToken?: number; + backgroundRefreshToken?: number; }) { const { resolvedTheme } = useTheme(); const settings = useClientSettings(); @@ -437,6 +447,40 @@ function PullRequestCodeTab({ refreshFirstDiffPage(); refreshFilesViewed(); }, [refreshToken, scopeKey, refreshFirstDiffPage, refreshFilesViewed]); + // A background refresh keeps the first page on screen and re-reads it in place. Later pages + // are dropped and their cached answers invalidated, so scrolling reads them fresh rather than + // trusting an unchanged first page to vouch for the rest of the diff. + const registry = useContext(RegistryContext); + const appliedBackgroundRefreshToken = useRef(backgroundRefreshToken); + useEffect(() => { + if (appliedBackgroundRefreshToken.current === backgroundRefreshToken) return; + appliedBackgroundRefreshToken.current = backgroundRefreshToken; + for (const slice of loadedSlices.slice(1)) { + if (slice.cursor === null) continue; + registry.refresh( + pullRequestEnvironment.diff({ + environmentId, + input: { ...reference, cursor: slice.cursor, ...(commit === null ? {} : { commit }) }, + }), + ); + } + setSliceState((previous) => ({ + key: previous.key, + cursor: null, + slices: previous.slices.slice(0, 1), + })); + refreshFirstDiffPage(); + refreshFilesViewed(); + }, [ + backgroundRefreshToken, + commit, + environmentId, + loadedSlices, + reference, + refreshFirstDiffPage, + refreshFilesViewed, + registry, + ]); const nextCursor = loadedSlices.at(-1)?.nextCursor ?? null; // What a slice withheld: the host declining to inline part of it, or a patch the viewer could // not structure and so dropped. Neither says anything about there being more to fetch. diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 1b42fdd4481a..45bd42f0b0e5 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -818,7 +818,9 @@ export function PullRequestDetailPanel({ nativeStackQuery.refresh(); }, [activityQuery.refresh, detailQuery.refresh, nativeStackQuery.refresh]); const [refreshToken, setRefreshToken] = useState(0); - const codeRefreshToken = refreshToken + (turnRefresh ?? 0); + // A new revision or a finished turn may have moved the diff, but nobody asked to start over. + const [revisionToken, setRevisionToken] = useState(0); + const backgroundRefreshToken = revisionToken + (turnRefresh ?? 0); const activityRevision = useRef<{ readonly key: string; readonly updatedAt: string } | null>( null, ); @@ -830,7 +832,7 @@ export function PullRequestDetailPanel({ // mutation's activity refresh can leave SWR displaying its previous value. if (activityQuery.isPending) return; activityQuery.refresh(); - setRefreshToken((token) => token + 1); + setRevisionToken((token) => token + 1); } activityRevision.current = next; }, [activityQuery.isPending, activityQuery.refresh, coreDetail, tabScopeKey]); @@ -2774,7 +2776,8 @@ export function PullRequestDetailPanel({ fixFindingLabel={handoffLabels.fixFinding} onFixFinding={startFixFinding} onRefresh={refreshDetail} - refreshToken={codeRefreshToken} + refreshToken={refreshToken} + backgroundRefreshToken={backgroundRefreshToken} />