@@ -27,6 +27,10 @@ import {
2727 reviewNoteSource ,
2828 type VisibleAgentNote ,
2929} from "../../lib/agentAnnotations" ;
30+ import {
31+ computeRapidScrollOverscanRows ,
32+ RAPID_SCROLL_OVERSCAN_IDLE_MS ,
33+ } from "../../lib/adaptiveScrollOverscan" ;
3034import { computeHunkRevealScrollTop } from "../../lib/hunkScroll" ;
3135import {
3236 measureDiffSectionGeometry ,
@@ -123,12 +127,14 @@ function buildAdjacentPrefetchFileIds(files: DiffFile[], selectedFileId?: string
123127function buildHighlightPrefetchFileIds ( {
124128 adjacentPrefetchFileIds,
125129 fileSectionLayouts,
130+ rapidScrollOverscanRows,
126131 scrollTop,
127132 viewportHeight,
128133 selectedFileId,
129134} : {
130135 adjacentPrefetchFileIds : Set < string > ;
131136 fileSectionLayouts : FileSectionLayout [ ] ;
137+ rapidScrollOverscanRows : number ;
132138 scrollTop : number ;
133139 viewportHeight : number ;
134140 selectedFileId ?: string ;
@@ -140,7 +146,7 @@ function buildHighlightPrefetchFileIds({
140146 }
141147
142148 const clampedViewportHeight = Math . max ( 1 , viewportHeight ) ;
143- const prefetchRows = Math . max ( 24 , clampedViewportHeight * 3 ) ;
149+ const prefetchRows = Math . max ( 24 , clampedViewportHeight * 3 , rapidScrollOverscanRows ) ;
144150 const minPrefetchY = Math . max ( 0 , scrollTop - prefetchRows ) ;
145151 const maxPrefetchY = scrollTop + viewportHeight + prefetchRows ;
146152
@@ -406,6 +412,7 @@ export function DiffPane({
406412 // other files can still use placeholders and viewport windowing.
407413 const windowingEnabled = ! wrapLines ;
408414 const [ scrollViewport , setScrollViewport ] = useState ( { top : 0 , height : 0 } ) ;
415+ const [ rapidScrollOverscanRows , setRapidScrollOverscanRows ] = useState ( 0 ) ;
409416 const [ hoveredFileId , setHoveredFileId ] = useState < string | null > ( null ) ;
410417 const [ copySelectionDrag , setCopySelectionDrag ] = useState < CopySelectionDrag | null > ( null ) ;
411418 // Mirror the drag state in a ref so updateCopySelection can suppress native selection
@@ -429,11 +436,28 @@ export function DiffPane({
429436 const suppressViewportSelectionSyncTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > (
430437 null ,
431438 ) ;
439+ const rapidScrollOverscanTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
432440 // Initialized to null so the first render never fires a selection change; a real scroll
433441 // is required before passive viewport-follow selection can trigger.
434442 const lastViewportSelectionTopRef = useRef < number | null > ( null ) ;
435443 const lastViewportRowAnchorRef = useRef < ViewportRowAnchor | null > ( null ) ;
436444
445+ /** Temporarily widen the mounted diff window while scroll input is arriving in bursts. */
446+ const activateRapidScrollOverscan = useCallback ( ( overscanRows : number ) => {
447+ if ( overscanRows <= 0 ) {
448+ return ;
449+ }
450+
451+ setRapidScrollOverscanRows ( ( current ) => Math . max ( current , overscanRows ) ) ;
452+ if ( rapidScrollOverscanTimeoutRef . current ) {
453+ clearTimeout ( rapidScrollOverscanTimeoutRef . current ) ;
454+ }
455+ rapidScrollOverscanTimeoutRef . current = setTimeout ( ( ) => {
456+ rapidScrollOverscanTimeoutRef . current = null ;
457+ setRapidScrollOverscanRows ( 0 ) ;
458+ } , RAPID_SCROLL_OVERSCAN_IDLE_MS ) ;
459+ } , [ ] ) ;
460+
437461 /**
438462 * Ignore viewport-follow selection updates while the pane is scrolling to an explicit selection.
439463 * That lets direct hunk/file navigation own the viewport until the jump settles.
@@ -454,6 +478,9 @@ export function DiffPane({
454478 if ( suppressViewportSelectionSyncTimeoutRef . current ) {
455479 clearTimeout ( suppressViewportSelectionSyncTimeoutRef . current ) ;
456480 }
481+ if ( rapidScrollOverscanTimeoutRef . current ) {
482+ clearTimeout ( rapidScrollOverscanTimeoutRef . current ) ;
483+ }
457484 } ;
458485 } , [ ] ) ;
459486
@@ -475,8 +502,15 @@ export function DiffPane({
475502 // Detect scroll activity, show scrollbar, and clear hover-only controls. The pointer may
476503 // now sit over a different row, but only an actual mouse move should reveal row actions.
477504 if ( nextTop !== prevScrollTopRef . current ) {
505+ const previousTop = prevScrollTopRef . current ;
478506 scrollbarRef . current ?. show ( ) ;
479507 clearAddNoteHoverForScroll ( ) ;
508+ activateRapidScrollOverscan (
509+ computeRapidScrollOverscanRows ( {
510+ deltaRows : nextTop - previousTop ,
511+ viewportHeight : nextHeight ,
512+ } ) ,
513+ ) ;
480514 prevScrollTopRef . current = nextTop ;
481515 }
482516
@@ -524,7 +558,7 @@ export function DiffPane({
524558 scrollBox . viewport . off ( "layout-changed" , handleViewportChange ) ;
525559 scrollBox . viewport . off ( "resized" , handleViewportChange ) ;
526560 } ;
527- } , [ clearAddNoteHoverForScroll , files . length , scrollRef ] ) ;
561+ } , [ activateRapidScrollOverscan , clearAddNoteHoverForScroll , files . length , scrollRef ] ) ;
528562
529563 const sectionHeaderHeights = useMemo ( ( ) => buildInStreamFileHeaderHeights ( files ) , [ files ] ) ;
530564
@@ -568,11 +602,11 @@ export function DiffPane({
568602 ) ;
569603
570604 const visibleViewportFileIds = useMemo ( ( ) => {
571- const overscanTerminalRows = 8 ;
605+ const overscanTerminalRows = Math . max ( 8 , rapidScrollOverscanRows ) ;
572606 const minVisibleY = Math . max ( 0 , scrollViewport . top - overscanTerminalRows ) ;
573607 const maxVisibleY = scrollViewport . top + scrollViewport . height + overscanTerminalRows ;
574608 return collectIntersectingFileSectionIds ( baseFileSectionLayouts , minVisibleY , maxVisibleY ) ;
575- } , [ baseFileSectionLayouts , scrollViewport . height , scrollViewport . top ] ) ;
609+ } , [ baseFileSectionLayouts , rapidScrollOverscanRows , scrollViewport . height , scrollViewport . top ] ) ;
576610
577611 const visibleAgentNotesByFile = useMemo ( ( ) => {
578612 const next = new Map < string , VisibleAgentNote [ ] > ( ) ;
@@ -970,13 +1004,15 @@ export function DiffPane({
9701004 buildHighlightPrefetchFileIds ( {
9711005 adjacentPrefetchFileIds,
9721006 fileSectionLayouts,
1007+ rapidScrollOverscanRows,
9731008 scrollTop : scrollViewport . top ,
9741009 viewportHeight : scrollViewport . height ,
9751010 selectedFileId,
9761011 } ) ,
9771012 [
9781013 adjacentPrefetchFileIds ,
9791014 fileSectionLayouts ,
1015+ rapidScrollOverscanRows ,
9801016 scrollViewport . height ,
9811017 scrollViewport . top ,
9821018 selectedFileId ,
@@ -1076,7 +1112,7 @@ export function DiffPane({
10761112 return next ;
10771113 }
10781114
1079- const overscanTerminalRows = Math . max ( 24 , scrollViewport . height * 2 ) ;
1115+ const overscanTerminalRows = Math . max ( 24 , scrollViewport . height * 2 , rapidScrollOverscanRows ) ;
10801116
10811117 files . forEach ( ( file , index ) => {
10821118 const sectionLayout = fileSectionLayouts [ index ] ;
@@ -1115,6 +1151,7 @@ export function DiffPane({
11151151 } , [
11161152 fileSectionLayouts ,
11171153 files ,
1154+ rapidScrollOverscanRows ,
11181155 scrollViewport . height ,
11191156 scrollViewport . top ,
11201157 sectionGeometry ,
0 commit comments