Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@tanstack/react-router-devtools": "~1.166.11",
"@tanstack/react-router-ssr-query": "~1.166.10",
"@tanstack/react-start": "~1.167.23",
"@tanstack/react-virtual": "^3.13.24",
"@tanstack/router-plugin": "~1.167.12",
"agentation": "^3.0.2",
"better-auth": "^1.6.0",
Expand Down
123 changes: 68 additions & 55 deletions apps/dashboard/src/components/pulls/review/review-diff-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useEffect,
useImperativeHandle,
useMemo,
useReducer,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -257,91 +258,103 @@ export const ReviewDiffPane = memo(
[files, visibleCount],
);

// Single shared observer for active file tracking
// Near-viewport tracking lives in a ref so the observer is NOT torn
// down on every scroll tick. State updates to a Set dep previously
// recreated the observer, opening a race window where elements that
// scrolled into view during teardown were never re-observed — the
// "diffs sometimes don't render" bug.
const nearViewportRef = useRef<Set<string>>(new Set());
const [, bumpNearViewport] = useReducer((n: number) => n + 1, 0);
const nearViewportObserverRef = useRef<IntersectionObserver | null>(null);
const activeFileObserverRef = useRef<IntersectionObserver | null>(null);
const onActiveFileChangeRef = useRef(onActiveFileChange);

useEffect(() => {
onActiveFileChangeRef.current = onActiveFileChange;
}, [onActiveFileChange]);

// Create both observers once, tied to the scroll panel's lifetime.
useEffect(() => {
const panel = diffPanelRef.current;
if (!panel || visibleFiles.length === 0) return;
if (!panel) return;

const observer = new IntersectionObserver(
const nearObserver = new IntersectionObserver(
(entries) => {
let changed = false;
for (const entry of entries) {
if (!entry.isIntersecting) continue;

const filename = entry.target.getAttribute("data-filename");
if (filename) {
onActiveFileChange(filename);
if (filename && !nearViewportRef.current.has(filename)) {
nearViewportRef.current.add(filename);
nearObserver.unobserve(entry.target);
changed = true;
}
}
if (changed) bumpNearViewport();
},
{
root: panel,
rootMargin: "-10% 0px -80% 0px",
rootMargin: "1500px 0px",
threshold: 0,
},
);

for (const file of visibleFiles) {
const element = document.getElementById(encodeFileId(file.filename));
if (element) observer.observe(element);
}

return () => observer.disconnect();
}, [onActiveFileChange, visibleFiles]);

// Single shared observer for viewport proximity — controls when diff content mounts
const [nearViewportFiles, setNearViewportFiles] = useState<Set<string>>(
() => new Set(),
);

// Seed the first visible files as near-viewport immediately.
// During client-side navigation the scroll container may not have its
// final dimensions when the IntersectionObserver first checks, causing
// all diffs to remain as empty placeholders until a hard refresh.
useEffect(() => {
if (visibleFiles.length === 0 || nearViewportFiles.size > 0) return;
setNearViewportFiles(
new Set(visibleFiles.slice(0, 4).map((f) => f.filename)),
);
}, [visibleFiles, nearViewportFiles.size]);

useEffect(() => {
const panel = diffPanelRef.current;
if (!panel || visibleFiles.length === 0) return;

const observer = new IntersectionObserver(
const activeObserver = new IntersectionObserver(
(entries) => {
const newlyVisible: string[] = [];
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const filename = entry.target.getAttribute("data-filename");
if (filename) {
newlyVisible.push(filename);
observer.unobserve(entry.target);
}
}
if (newlyVisible.length > 0) {
setNearViewportFiles((prev) => {
const next = new Set(prev);
for (const f of newlyVisible) next.add(f);
return next;
});
if (filename) onActiveFileChangeRef.current(filename);
}
},
{
root: panel,
rootMargin: "1500px 0px",
rootMargin: "-10% 0px -80% 0px",
threshold: 0,
},
);

nearViewportObserverRef.current = nearObserver;
activeFileObserverRef.current = activeObserver;

return () => {
nearObserver.disconnect();
activeObserver.disconnect();
nearViewportObserverRef.current = null;
activeFileObserverRef.current = null;
};
}, []);

// Seed the first visible files as near-viewport immediately.
// During client-side navigation the scroll container may not have its
// final dimensions when the IntersectionObserver first checks, causing
// all diffs to remain as empty placeholders until a hard refresh.
useEffect(() => {
if (visibleFiles.length === 0 || nearViewportRef.current.size > 0) {
return;
}
for (const file of visibleFiles.slice(0, 4)) {
nearViewportRef.current.add(file.filename);
}
bumpNearViewport();
}, [visibleFiles]);

// Observe any newly-mounted file elements. observe() is idempotent, so
// re-calling it on already-observed nodes is safe.
useEffect(() => {
const nearObserver = nearViewportObserverRef.current;
const activeObserver = activeFileObserverRef.current;
if (!nearObserver || !activeObserver) return;

for (const file of visibleFiles) {
if (nearViewportFiles.has(file.filename)) continue;
const element = document.getElementById(encodeFileId(file.filename));
if (element) observer.observe(element);
if (!element) continue;
activeObserver.observe(element);
if (!nearViewportRef.current.has(file.filename)) {
nearObserver.observe(element);
}
}

return () => observer.disconnect();
}, [visibleFiles, nearViewportFiles]);
}, [visibleFiles]);

if (totalFileCount === 0 && !hasNextPage) {
return (
Expand All @@ -360,7 +373,7 @@ export const ReviewDiffPane = memo(
id={encodeFileId(file.filename)}
file={file}
diffStyle={diffStyle}
isNearViewport={nearViewportFiles.has(file.filename)}
isNearViewport={nearViewportRef.current.has(file.filename)}
annotations={
annotationsByFile.get(file.filename) ?? EMPTY_ANNOTATIONS
}
Expand Down
Loading
Loading