fix(ui): keep diff refreshes targeted

This commit is contained in:
Bohdan Triapitsyn
2026-08-02 21:46:22 +03:00
parent 75832876fa
commit 17c2d5ec36
14 changed files with 268 additions and 38 deletions
+64 -11
View File
@@ -34,6 +34,7 @@ import { Icon } from "@/components/icon/Icon";
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
import { toAbsoluteFilePath } from '@/lib/path-utils';
import { sessionEvents } from '@/lib/sessionEvents';
import { findDiffScrollAnchor, getRestoredDiffScrollTop, type DiffScrollAnchor } from './diffScrollAnchor';
import { useI18n } from '@/lib/i18n';
import type { I18nKey } from '@/lib/i18n/store';
import { fileDiffFromPatch } from '@/lib/diff/patchFileDiff';
@@ -973,6 +974,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
const setActiveDirectory = useGitStore((state) => state.setActiveDirectory);
const ensureStatus = useGitStore((state) => state.ensureStatus);
const fetchStatus = useGitStore((state) => state.fetchStatus);
const clearDiffCache = useGitStore((state) => state.clearDiffCache);
const setDiff = useGitStore((state) => state.setDiff);
const [displayFile, setDisplayFile] = React.useState<string | null>(null);
const [displayFileStaged, setDisplayFileStaged] = React.useState(false);
@@ -981,6 +983,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
const [mountedStackedFiles, setMountedStackedFiles] = React.useState<Set<string>>(() => new Set());
const [loadFullFiles, setLoadFullFiles] = React.useState(false);
const [scrollRequestNonce, setScrollRequestNonce] = React.useState(0);
const [fileDiffRefreshNonce, setFileDiffRefreshNonce] = React.useState<Map<string, number>>(() => new Map());
const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false);
const [reviewFlowSubmitting, setReviewFlowSubmitting] = React.useState(false);
const [activeDiffScope, setActiveDiffScope] = React.useState(diffScope);
@@ -1018,6 +1021,20 @@ export const DiffView: React.FC<DiffViewProps> = ({
const shouldPinAfterAlignRef = React.useRef(false);
const visibleSyncFrameRef = React.useRef<number | null>(null);
const stackedStateScopeRef = React.useRef<string | null>(null);
const lastScrollAnchorRef = React.useRef<DiffScrollAnchor | null>(null);
const pendingScrollAnchorRestoreRef = React.useRef<DiffScrollAnchor | null>(null);
const captureScrollAnchor = React.useCallback((): DiffScrollAnchor | null => {
const scrollRoot = diffScrollRef.current;
if (!scrollRoot) return null;
const rootTop = scrollRoot.getBoundingClientRect().top;
const sections: Array<{ path: string; top: number }> = [];
for (const [path, node] of fileSectionRefs.current) {
if (node) sections.push({ path, top: node.getBoundingClientRect().top });
}
return findDiffScrollAnchor(rootTop, sections);
}, []);
const cancelPendingScrollAlignment = React.useCallback(() => {
pendingScrollTargetRef.current = null;
@@ -1171,13 +1188,17 @@ export const DiffView: React.FC<DiffViewProps> = ({
const top = rootRect.top - STACKED_DIFF_MOUNT_MARGIN;
const bottom = rootRect.bottom + STACKED_DIFF_MOUNT_MARGIN;
const next: Record<string, boolean> = {};
const sectionPositions: Array<{ path: string; top: number }> = [];
for (const [path, node] of fileSectionRefs.current) {
if (!node || !expandedFiles.has(path)) continue;
if (!node) continue;
const rect = node.getBoundingClientRect();
sectionPositions.push({ path, top: rect.top });
if (!expandedFiles.has(path)) continue;
if (rect.bottom < top || rect.top > bottom) continue;
next[path] = true;
}
lastScrollAnchorRef.current = findDiffScrollAnchor(rootRect.top, sectionPositions);
setMountedStackedFiles((previous) => {
let changed = false;
@@ -1259,9 +1280,40 @@ export const DiffView: React.FC<DiffViewProps> = ({
if (normalizePath(hint.directory) !== normalizePath(effectiveDirectory)) {
return;
}
void fetchStatus(effectiveDirectory, git);
if (hint.paths?.length) {
pendingScrollAnchorRestoreRef.current = captureScrollAnchor() ?? lastScrollAnchorRef.current;
clearDiffCache(effectiveDirectory, hint.paths);
setFileDiffRefreshNonce((previous) => {
const next = new Map(previous);
for (const path of hint.paths ?? []) {
next.set(path, (next.get(path) ?? 0) + 1);
}
return next;
});
}
void fetchStatus(effectiveDirectory, git, { silent: true });
});
}, [effectiveDirectory, fetchStatus, git]);
}, [captureScrollAnchor, clearDiffCache, effectiveDirectory, fetchStatus, git]);
React.useLayoutEffect(() => {
const anchor = pendingScrollAnchorRestoreRef.current;
if (!anchor) return;
pendingScrollAnchorRestoreRef.current = null;
const scrollRoot = diffScrollRef.current;
const node = fileSectionRefs.current.get(anchor.path);
if (!scrollRoot || !node) return;
const rootTop = scrollRoot.getBoundingClientRect().top;
const currentTopOffset = node.getBoundingClientRect().top - rootTop;
scrollRoot.scrollTop = getRestoredDiffScrollTop(
scrollRoot.scrollTop,
anchor.topOffset,
currentTopOffset,
scrollRoot.scrollHeight - scrollRoot.clientHeight,
);
lastScrollAnchorRef.current = anchor;
}, [fileDiffRefreshNonce]);
// Handle pending diff file from external navigation
React.useEffect(() => {
@@ -1399,11 +1451,8 @@ export const DiffView: React.FC<DiffViewProps> = ({
return false;
}
const rootRect = scrollRoot.getBoundingClientRect();
const nodeRect = node.getBoundingClientRect();
const delta = nodeRect.top - rootRect.top;
const maxTop = Math.max(0, scrollRoot.scrollHeight - scrollRoot.clientHeight);
scrollRoot.scrollTop = Math.min(maxTop, Math.max(0, scrollRoot.scrollTop + delta));
const scrollOffset = node.getBoundingClientRect().top - scrollRoot.getBoundingClientRect().top;
scrollRoot.scrollTo({ top: scrollRoot.scrollTop + scrollOffset, behavior: 'auto' });
return true;
}, []);
@@ -1415,14 +1464,16 @@ export const DiffView: React.FC<DiffViewProps> = ({
const maxAttempts = 20;
let cancelled = false;
const cancelPending = () => {
const cancelPending = (clearPinnedTarget = true) => {
if (cancelled) {
return;
}
cancelled = true;
pendingScrollTargetRef.current = null;
shouldPinAfterAlignRef.current = false;
setPinnedStackedTarget(null);
if (clearPinnedTarget) {
setPinnedStackedTarget(null);
}
if (pendingScrollFrameRef.current !== null) {
window.cancelAnimationFrame(pendingScrollFrameRef.current);
pendingScrollFrameRef.current = null;
@@ -1455,6 +1506,8 @@ export const DiffView: React.FC<DiffViewProps> = ({
if (pinSelectedFileHeaderToTopOnNavigate && shouldPinAfterAlignRef.current) {
setPinnedStackedTarget(currentTarget);
cancelPending(false);
return;
}
cancelPending();
};
@@ -1599,7 +1652,7 @@ export const DiffView: React.FC<DiffViewProps> = ({
<div className="flex flex-col [overflow-anchor:none]" data-diff-virtual-content>
{changedFiles.map((file) => (
<MultiFileDiffEntry
key={file.path}
key={`${file.path}:${fileDiffRefreshNonce.get(file.path) ?? 0}`}
directory={effectiveDirectory}
file={file}
layout={getLayoutForFile(file)}