@@ -163,7 +163,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163163 return m , nil
164164 case tea .MouseButtonWheelDown :
165165 if m .mouseInPreview {
166- m .previewScroll += 3
166+ m .previewScroll = min ( m . previewScroll + 3 , m . maxPreviewScroll ())
167167 } else {
168168 if m .cursor < len (m .filtered )- 1 {
169169 m .cursor ++
@@ -231,7 +231,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
231231 return m , nil
232232
233233 case "pgdown" , "ctrl+j" :
234- m .previewScroll += 10
234+ m .previewScroll = min ( m . previewScroll + 10 , m . maxPreviewScroll ())
235235 return m , nil
236236
237237 case "ctrl+u" :
@@ -393,20 +393,11 @@ func (m model) formatListItem(item listItem, selected bool) string {
393393 ts , project , topic , msgs , hits )
394394}
395395
396- func (m model ) renderPreview (item listItem , height int ) string {
397- query := m .textInput .Value ()
398- conv := item .conv
399-
400- // Fixed header (always visible)
401- var header []string
402- header = append (header , "\033 [1;33mProject:\033 [0m " + highlight (conv .Cwd , query ))
403- if conv .Title != "" {
404- header = append (header , "\033 [1;33mName:\033 [0m " + highlight (conv .Title , query ))
405- }
406- header = append (header , "\033 [1;33mSession:\033 [0m " + highlight (conv .SessionID , query ))
407- header = append (header , "" )
408-
409- // Build message lines (scrollable)
396+ // buildPreviewLines builds the scrollable message lines of a conversation
397+ // preview (everything below the fixed header). Shared by renderPreview and
398+ // maxPreviewScroll so the render and the scroll-clamp can never disagree on how
399+ // far the preview can scroll.
400+ func buildPreviewLines (conv Conversation , query string ) []string {
410401 var msgLines []string
411402
412403 // Find messages containing the query
@@ -495,16 +486,45 @@ func (m model) renderPreview(item listItem, height int) string {
495486 msgLines = append (msgLines , fmt .Sprintf ("\033 [90m ... %d more messages\033 [0m" , remaining ))
496487 }
497488
498- // Apply scroll to messages only (header stays fixed)
489+ return msgLines
490+ }
491+
492+ // maxPreviewScroll is the furthest the preview of the current selection can
493+ // scroll - one line short of the rendered message-line count.
494+ func (m model ) maxPreviewScroll () int {
495+ if len (m .filtered ) == 0 {
496+ return 0
497+ }
498+ lines := buildPreviewLines (m .filtered [m .cursor ].conv , m .textInput .Value ())
499+ return max (0 , len (lines )- 1 )
500+ }
501+
502+ func (m model ) renderPreview (item listItem , height int ) string {
503+ query := m .textInput .Value ()
504+ conv := item .conv
505+
506+ // Fixed header (always visible)
507+ var header []string
508+ header = append (header , "\033 [1;33mProject:\033 [0m " + highlight (conv .Cwd , query ))
509+ if conv .Title != "" {
510+ header = append (header , "\033 [1;33mName:\033 [0m " + highlight (conv .Title , query ))
511+ }
512+ header = append (header , "\033 [1;33mSession:\033 [0m " + highlight (conv .SessionID , query ))
513+ header = append (header , "" )
514+
515+ msgLines := buildPreviewLines (conv , query )
516+
517+ // Apply scroll to messages only (header stays fixed). Clamp locally for this
518+ // render; the persisted m.previewScroll is bounded in Update via
519+ // maxPreviewScroll (this method has a value receiver, so a write here would
520+ // be discarded).
499521 msgHeight := height - len (header )
500522 if msgHeight < 1 {
501523 msgHeight = 1
502524 }
503- if m .previewScroll >= len (msgLines ) {
504- m .previewScroll = max (0 , len (msgLines )- 1 )
505- }
506- end := min (m .previewScroll + msgHeight , len (msgLines ))
507- visibleMsgLines := msgLines [m .previewScroll :end ]
525+ scroll := min (m .previewScroll , max (0 , len (msgLines )- 1 ))
526+ end := min (scroll + msgHeight , len (msgLines ))
527+ visibleMsgLines := msgLines [scroll :end ]
508528
509529 // Combine header + scrolled messages
510530 allLines := append (header , visibleMsgLines ... )
0 commit comments