@@ -2,8 +2,10 @@ import type { ScrollBoxRenderable } from "@opentui/core";
22import { useCallback , useEffect , useMemo , useState , type RefObject } from "react" ;
33import type { AgentAnnotation , DiffFile , LayoutMode } from "../../../core/types" ;
44import type { VisibleAgentNote } from "../../lib/agentAnnotations" ;
5+ import { estimateDiffBodyRows } from "../../lib/sectionHeights" ;
56import type { AppTheme } from "../../themes" ;
67import { DiffSection } from "./DiffSection" ;
8+ import { DiffSectionPlaceholder } from "./DiffSectionPlaceholder" ;
79
810const EMPTY_VISIBLE_AGENT_NOTES : VisibleAgentNote [ ] = [ ] ;
911
@@ -117,6 +119,74 @@ export function DiffPane({
117119 return next ;
118120 } , [ activeAnnotations , dismissedAgentNoteIds , selectedFileId , selectedHunkIndex , showAgentNotes ] ) ;
119121
122+ // Keep exact row rendering for wrapped lines and visible notes; otherwise reserve
123+ // offscreen section height and only materialize rows near the viewport.
124+ const windowingEnabled = ! wrapLines && visibleAgentNotesByFile . size === 0 ;
125+ const [ scrollViewport , setScrollViewport ] = useState ( { top : 0 , height : 0 } ) ;
126+
127+ useEffect ( ( ) => {
128+ if ( ! windowingEnabled ) {
129+ setScrollViewport ( { top : 0 , height : 0 } ) ;
130+ return ;
131+ }
132+
133+ const updateViewport = ( ) => {
134+ const nextTop = scrollRef . current ?. scrollTop ?? 0 ;
135+ const nextHeight = scrollRef . current ?. viewport . height ?? 0 ;
136+
137+ setScrollViewport ( ( current ) =>
138+ current . top === nextTop && current . height === nextHeight ? current : { top : nextTop , height : nextHeight } ,
139+ ) ;
140+ } ;
141+
142+ updateViewport ( ) ;
143+ const interval = setInterval ( updateViewport , 50 ) ;
144+ return ( ) => clearInterval ( interval ) ;
145+ } , [ scrollRef , windowingEnabled ] ) ;
146+
147+ const estimatedBodyHeights = useMemo (
148+ ( ) => files . map ( ( file ) => estimateDiffBodyRows ( file , layout , showHunkHeaders ) ) ,
149+ [ files , layout , showHunkHeaders ] ,
150+ ) ;
151+
152+ const visibleWindowedFileIds = useMemo ( ( ) => {
153+ if ( ! windowingEnabled ) {
154+ return null ;
155+ }
156+
157+ const overscanRows = 40 ;
158+ const minVisibleY = Math . max ( 0 , scrollViewport . top - overscanRows ) ;
159+ const maxVisibleY = scrollViewport . top + scrollViewport . height + overscanRows ;
160+ let offsetY = 0 ;
161+ const next = new Set < string > ( ) ;
162+
163+ files . forEach ( ( file , index ) => {
164+ const sectionHeight = ( index > 0 ? 1 : 0 ) + 1 + ( estimatedBodyHeights [ index ] ?? 0 ) ;
165+ const sectionStart = offsetY ;
166+ const sectionEnd = sectionStart + sectionHeight ;
167+
168+ if (
169+ file . id === selectedFileId ||
170+ adjacentPrefetchFileIds . has ( file . id ) ||
171+ ( sectionEnd >= minVisibleY && sectionStart <= maxVisibleY )
172+ ) {
173+ next . add ( file . id ) ;
174+ }
175+
176+ offsetY = sectionEnd ;
177+ } ) ;
178+
179+ return next ;
180+ } , [
181+ adjacentPrefetchFileIds ,
182+ estimatedBodyHeights ,
183+ files ,
184+ scrollViewport . height ,
185+ scrollViewport . top ,
186+ selectedFileId ,
187+ windowingEnabled ,
188+ ] ) ;
189+
120190 return (
121191 < box
122192 style = { {
@@ -144,30 +214,46 @@ export function DiffPane({
144214 horizontalScrollbarOptions = { { visible : false } }
145215 >
146216 < box style = { { width : "100%" , flexDirection : "column" } } >
147- { files . map ( ( file , index ) => (
148- < DiffSection
149- key = { file . id }
150- file = { file }
151- headerLabelWidth = { headerLabelWidth }
152- headerStatsWidth = { headerStatsWidth }
153- layout = { layout }
154- selected = { file . id === selectedFileId }
155- selectedHunkIndex = { file . id === selectedFileId ? selectedHunkIndex : - 1 }
156- shouldLoadHighlight = { file . id === selectedFileId || adjacentPrefetchFileIds . has ( file . id ) }
157- onHighlightReady = { file . id === selectedFileId ? handleSelectedHighlightReady : undefined }
158- separatorWidth = { separatorWidth }
159- showSeparator = { index > 0 }
160- showLineNumbers = { showLineNumbers }
161- showHunkHeaders = { showHunkHeaders }
162- wrapLines = { wrapLines }
163- theme = { theme }
164- viewWidth = { diffContentWidth }
165- visibleAgentNotes = { visibleAgentNotesByFile . get ( file . id ) ?? EMPTY_VISIBLE_AGENT_NOTES }
166- onDismissAgentNote = { onDismissAgentNote }
167- onOpenAgentNotesAtHunk = { ( hunkIndex ) => onOpenAgentNotesAtHunk ( file . id , hunkIndex ) }
168- onSelect = { ( ) => onSelectFile ( file . id ) }
169- />
170- ) ) }
217+ { files . map ( ( file , index ) => {
218+ const shouldRenderSection = visibleWindowedFileIds ?. has ( file . id ) ?? true ;
219+
220+ return shouldRenderSection ? (
221+ < DiffSection
222+ key = { file . id }
223+ file = { file }
224+ headerLabelWidth = { headerLabelWidth }
225+ headerStatsWidth = { headerStatsWidth }
226+ layout = { layout }
227+ selected = { file . id === selectedFileId }
228+ selectedHunkIndex = { file . id === selectedFileId ? selectedHunkIndex : - 1 }
229+ shouldLoadHighlight = { file . id === selectedFileId || adjacentPrefetchFileIds . has ( file . id ) }
230+ onHighlightReady = { file . id === selectedFileId ? handleSelectedHighlightReady : undefined }
231+ separatorWidth = { separatorWidth }
232+ showSeparator = { index > 0 }
233+ showLineNumbers = { showLineNumbers }
234+ showHunkHeaders = { showHunkHeaders }
235+ wrapLines = { wrapLines }
236+ theme = { theme }
237+ viewWidth = { diffContentWidth }
238+ visibleAgentNotes = { visibleAgentNotesByFile . get ( file . id ) ?? EMPTY_VISIBLE_AGENT_NOTES }
239+ onDismissAgentNote = { onDismissAgentNote }
240+ onOpenAgentNotesAtHunk = { ( hunkIndex ) => onOpenAgentNotesAtHunk ( file . id , hunkIndex ) }
241+ onSelect = { ( ) => onSelectFile ( file . id ) }
242+ />
243+ ) : (
244+ < DiffSectionPlaceholder
245+ key = { file . id }
246+ bodyHeight = { estimatedBodyHeights [ index ] ?? 0 }
247+ file = { file }
248+ headerLabelWidth = { headerLabelWidth }
249+ headerStatsWidth = { headerStatsWidth }
250+ separatorWidth = { separatorWidth }
251+ showSeparator = { index > 0 }
252+ theme = { theme }
253+ onSelect = { ( ) => onSelectFile ( file . id ) }
254+ />
255+ ) ;
256+ } ) }
171257 </ box >
172258 </ scrollbox >
173259 ) : (
0 commit comments