@@ -55,6 +55,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5555 case tea.WindowSizeMsg :
5656 m .Width = msg .Width
5757 m .Height = msg .Height
58+ m .clampScrollOffset ()
5859 return m , nil
5960 case FilesUpdatedMsg :
6061 return m .handleFilesUpdated (msg )
@@ -107,13 +108,16 @@ func (m Model) applyFilesUpdate(msg FilesUpdatedMsg) Model {
107108
108109 if m .FollowOn && len (m .Files ) > 0 {
109110 target := len (m .Files ) - 1
110- for i , file := range m .Files {
111- for _ , changedPath := range msg .ChangedPaths {
111+ for i := len (msg .ChangedPaths ) - 1 ; i >= 0 ; i -- {
112+ changedPath := msg .ChangedPaths [i ]
113+ for j , file := range m .Files {
112114 if file .Path == changedPath {
113- target = i
115+ target = j
116+ goto followTargetFound
114117 }
115118 }
116119 }
120+ followTargetFound:
117121 m .CurrentIdx = target
118122 m .ScrollOffset = 0
119123 m .NewFiles = make (map [string ]bool )
@@ -152,6 +156,7 @@ func (m Model) applyFilesUpdate(msg FilesUpdatedMsg) Model {
152156 m .NewCount = 0
153157 }
154158
159+ m .clampScrollOffset ()
155160 return m
156161}
157162
@@ -161,11 +166,13 @@ func (m Model) handleKey(key string) (tea.Model, tea.Cmd) {
161166 return m , tea .Quit
162167 case "j" , "down" :
163168 m .ScrollOffset ++
169+ m .clampScrollOffset ()
164170 return m , nil
165171 case "k" , "up" :
166172 if m .ScrollOffset > 0 {
167173 m .ScrollOffset --
168174 }
175+ m .clampScrollOffset ()
169176 return m , nil
170177 case "n" :
171178 if len (m .Files ) > 1 {
@@ -271,13 +278,41 @@ func (m *Model) selectLatestPendingFile() {
271278 }
272279}
273280
281+ // clampScrollOffset keeps scroll state within the current diff viewport bounds.
282+ func (m * Model ) clampScrollOffset () {
283+ if m .ScrollOffset < 0 {
284+ m .ScrollOffset = 0
285+ return
286+ }
287+
288+ maxOffset := m .maxScrollOffset ()
289+ if m .ScrollOffset > maxOffset {
290+ m .ScrollOffset = maxOffset
291+ }
292+ }
293+
294+ // maxScrollOffset returns the furthest scroll position that can show diff content.
295+ func (m Model ) maxScrollOffset () int {
296+ if len (m .Files ) == 0 || m .CurrentIdx < 0 || m .CurrentIdx >= len (m .Files ) {
297+ return 0
298+ }
299+
300+ diffHeight := max (0 , m .Height - 2 )
301+ if diffHeight == 0 {
302+ return 0
303+ }
304+
305+ totalLines := countVisualDiffLines (& m .Files [m .CurrentIdx ], m .Width )
306+ return max (0 , totalLines - diffHeight )
307+ }
308+
274309// View renders header, content, and footer into the terminal viewport.
275310func (m Model ) View () string {
276311 if m .Width == 0 || m .Height == 0 {
277312 return ""
278313 }
279314
280- diffHeight := m .Height - 2
315+ diffHeight := max ( 0 , m .Height - 2 )
281316 header := RenderHeader (m .DirName , m .Files , m .CurrentIdx , m .NewCount )
282317 footer := RenderFooter (m .FollowOn , m .Notification )
283318
0 commit comments