diff --git a/src/features/git/components/GitDiffPanel.tsx b/src/features/git/components/GitDiffPanel.tsx
index 015b69036..cc55a8348 100644
--- a/src/features/git/components/GitDiffPanel.tsx
+++ b/src/features/git/components/GitDiffPanel.tsx
@@ -168,6 +168,7 @@ export function GitDiffPanel({
gitRootScanLoading = false,
gitRootScanError = null,
gitRootScanHasScanned = false,
+ selectedPath = null,
onGitRootScanDepthChange,
onScanGitRoots,
onSelectGitRoot,
@@ -461,7 +462,7 @@ export function GitDiffPanel({
return (
onSelectFile?.(file.path)}
diff --git a/src/features/git/components/GitDiffViewer.tsx b/src/features/git/components/GitDiffViewer.tsx
index 85a9ed51b..c991bd96b 100644
--- a/src/features/git/components/GitDiffViewer.tsx
+++ b/src/features/git/components/GitDiffViewer.tsx
@@ -80,20 +80,21 @@ const DiffCard = memo(function DiffCard({
}, [entry.diff, entry.path]);
return (
-
+
- {entry.status}
+
+ {entry.status}
+
{entry.path}
{entry.diff.trim().length > 0 && fileDiff ? (
-
+
@@ -109,9 +110,11 @@ export function GitDiffViewer({
selectedPath,
isLoading,
error,
+ onActivePathChange,
}: GitDiffViewerProps) {
const containerRef = useRef
(null);
const lastScrolledPathRef = useRef(null);
+ const activePathRef = useRef(null);
const poolOptions = useMemo(() => ({ workerFactory }), []);
const highlighterOptions = useMemo(
() => ({ theme: { dark: "pierre-dark", light: "pierre-light" } }),
@@ -131,6 +134,18 @@ export function GitDiffViewer({
overscan: 6,
});
const virtualItems = rowVirtualizer.getVirtualItems();
+ const stickyEntry = useMemo(() => {
+ if (!diffs.length) {
+ return null;
+ }
+ if (selectedPath) {
+ const index = indexByPath.get(selectedPath);
+ if (index !== undefined) {
+ return diffs[index];
+ }
+ }
+ return diffs[0];
+ }, [diffs, selectedPath, indexByPath]);
useEffect(() => {
if (!selectedPath) {
@@ -147,12 +162,78 @@ export function GitDiffViewer({
lastScrolledPathRef.current = selectedPath;
}, [selectedPath, indexByPath, rowVirtualizer]);
+ useEffect(() => {
+ activePathRef.current = selectedPath;
+ }, [selectedPath]);
+
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container || !onActivePathChange) {
+ return;
+ }
+ let frameId: number | null = null;
+
+ const updateActivePath = () => {
+ frameId = null;
+ const items = rowVirtualizer.getVirtualItems();
+ if (!items.length) {
+ return;
+ }
+ const scrollTop = container.scrollTop;
+ const targetOffset = scrollTop + 8;
+ let activeItem = items[0];
+ for (const item of items) {
+ if (item.start <= targetOffset) {
+ activeItem = item;
+ } else {
+ break;
+ }
+ }
+ const nextPath = diffs[activeItem.index]?.path;
+ if (!nextPath || nextPath === activePathRef.current) {
+ return;
+ }
+ activePathRef.current = nextPath;
+ lastScrolledPathRef.current = nextPath;
+ onActivePathChange(nextPath);
+ };
+
+ const handleScroll = () => {
+ if (frameId !== null) {
+ return;
+ }
+ frameId = requestAnimationFrame(updateActivePath);
+ };
+
+ handleScroll();
+ container.addEventListener("scroll", handleScroll, { passive: true });
+ return () => {
+ if (frameId !== null) {
+ cancelAnimationFrame(frameId);
+ }
+ container.removeEventListener("scroll", handleScroll);
+ };
+ }, [diffs, onActivePathChange, rowVirtualizer]);
+
return (
+ {!error && stickyEntry && (
+
+
+
+ {stickyEntry.status}
+
+ {stickyEntry.path}
+
+
+ )}
{error &&
{error}
}
{!error && isLoading && diffs.length > 0 && (
@@ -178,7 +259,7 @@ export function GitDiffViewer({
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
- transform: `translateY(${virtualRow.start}px)`,
+ transform: `translate3d(0, ${virtualRow.start}px, 0)`,
}}
>