@@ -81,6 +81,7 @@ type model struct {
8181
8282 // Height tracking system fields
8383 scrollOffset int // Current scroll position in lines
84+ bottomSlack int // Extra blank lines added after content shrinks
8485 rendered string // Complete rendered content string
8586 renderedItems map [int ]renderedItem // Cache of rendered items with positions
8687 totalHeight int // Total height of all content in lines
@@ -185,6 +186,9 @@ func (m *model) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
185186 m .invalidateAllItems ()
186187 return m , nil
187188
189+ case reasoningblock.BlockMsg :
190+ return m .forwardToReasoningBlock (msg .GetBlockID (), msg )
191+
188192 case tea.KeyPressMsg :
189193 return m .handleKeyPress (msg )
190194 }
@@ -220,6 +224,7 @@ func (m *model) handleMouseClick(msg tea.MouseClickMsg) (layout.Model, tea.Cmd)
220224 if block .IsToggleLine (localLine ) {
221225 block .Toggle ()
222226 m .userHasScrolled = true // Prevent auto-scroll jump
227+ m .bottomSlack = 0
223228 m .invalidateItem (msgIdx )
224229 return m , nil
225230 }
@@ -310,12 +315,14 @@ func (m *model) handleMouseWheel(msg tea.MouseWheelMsg) (layout.Model, tea.Cmd)
310315 case "wheelup" :
311316 if m .scrollOffset > 0 {
312317 m .userHasScrolled = true
318+ m .bottomSlack = 0
313319 for range mouseScrollAmount {
314320 m .setScrollOffset (m .scrollOffset - defaultScrollAmount )
315321 }
316322 }
317323 case "wheeldown" :
318324 m .userHasScrolled = true
325+ m .bottomSlack = 0
319326 for range mouseScrollAmount {
320327 m .setScrollOffset (m .scrollOffset + defaultScrollAmount )
321328 }
@@ -374,23 +381,39 @@ func (m *model) View() string {
374381 }
375382
376383 prevTotalHeight := m .totalHeight
384+ prevScrollableHeight := m .totalHeight + m .bottomSlack
377385 m .ensureAllItemsRendered ()
378386
379387 if m .totalHeight == 0 {
380388 return ""
381389 }
382390
383- // Calculate viewport bounds
384- maxScrollOffset := max (0 , m .totalHeight - m .height )
391+ if m .userHasScrolled {
392+ m .bottomSlack = 0
393+ } else {
394+ delta := m .totalHeight - prevTotalHeight
395+ if delta < 0 {
396+ m .bottomSlack += - delta
397+ } else if delta > 0 && m .bottomSlack > 0 {
398+ consume := min (delta , m .bottomSlack )
399+ m .bottomSlack -= consume
400+ }
401+ }
402+
403+ scrollableHeight := m .totalHeight + m .bottomSlack
404+ maxScrollOffset := max (0 , scrollableHeight - m .height )
385405
386- // Auto-scroll if content grew and user hasn't manually scrolled
387- if ! m .userHasScrolled && m . totalHeight > prevTotalHeight {
406+ // Auto-scroll when content grows beyond any slack.
407+ if ! m .userHasScrolled && scrollableHeight > prevScrollableHeight {
388408 m .scrollOffset = maxScrollOffset
389409 } else {
390410 m .scrollOffset = max (0 , min (m .scrollOffset , maxScrollOffset ))
391411 }
392412
393413 lines := strings .Split (m .rendered , "\n " )
414+ if m .bottomSlack > 0 {
415+ lines = append (lines , make ([]string , m .bottomSlack )... )
416+ }
394417 if len (lines ) == 0 {
395418 return ""
396419 }
@@ -519,12 +542,14 @@ const defaultScrollAmount = 1
519542func (m * model ) scrollUp () {
520543 if m .scrollOffset > 0 {
521544 m .userHasScrolled = true
545+ m .bottomSlack = 0
522546 m .setScrollOffset (max (0 , m .scrollOffset - defaultScrollAmount ))
523547 }
524548}
525549
526550func (m * model ) scrollDown () {
527551 m .userHasScrolled = true
552+ m .bottomSlack = 0
528553 m .setScrollOffset (m .scrollOffset + defaultScrollAmount )
529554 if m .isAtBottom () {
530555 m .userHasScrolled = false
@@ -533,11 +558,13 @@ func (m *model) scrollDown() {
533558
534559func (m * model ) scrollPageUp () {
535560 m .userHasScrolled = true
561+ m .bottomSlack = 0
536562 m .setScrollOffset (max (0 , m .scrollOffset - m .height ))
537563}
538564
539565func (m * model ) scrollPageDown () {
540566 m .userHasScrolled = true
567+ m .bottomSlack = 0
541568 m .setScrollOffset (m .scrollOffset + m .height )
542569 if m .isAtBottom () {
543570 m .userHasScrolled = false
@@ -546,6 +573,7 @@ func (m *model) scrollPageDown() {
546573
547574func (m * model ) scrollToTop () {
548575 m .userHasScrolled = true
576+ m .bottomSlack = 0
549577 m .setScrollOffset (0 )
550578}
551579
@@ -555,7 +583,7 @@ func (m *model) scrollToBottom() {
555583}
556584
557585func (m * model ) setScrollOffset (offset int ) {
558- maxOffset := max (0 , m .totalHeight - m .height )
586+ maxOffset := max (0 , m .totalScrollableHeight () - m .height )
559587 m .scrollOffset = max (0 , min (offset , maxOffset ))
560588 m .scrollbar .SetScrollOffset (m .scrollOffset )
561589}
@@ -564,7 +592,7 @@ func (m *model) isAtBottom() bool {
564592 if len (m .messages ) == 0 {
565593 return true
566594 }
567- maxScrollOffset := max (0 , m .totalHeight - m .height )
595+ maxScrollOffset := max (0 , m .totalScrollableHeight () - m .height )
568596 return m .scrollOffset >= maxScrollOffset
569597}
570598
@@ -657,9 +685,11 @@ func (m *model) scrollToSelectedMessage() {
657685 // Scroll to make the selected message visible
658686 if startLine < m .scrollOffset {
659687 m .userHasScrolled = true
688+ m .bottomSlack = 0
660689 m .setScrollOffset (startLine )
661690 } else if endLine > m .scrollOffset + m .height {
662691 m .userHasScrolled = true
692+ m .bottomSlack = 0
663693 m .setScrollOffset (endLine - m .height )
664694 }
665695}
@@ -782,6 +812,21 @@ func (m *model) invalidateAllItems() {
782812 m .renderDirty = true
783813}
784814
815+ // forwardToReasoningBlock finds the reasoning block with the given ID and forwards the message to it.
816+ func (m * model ) forwardToReasoningBlock (blockID string , msg tea.Msg ) (layout.Model , tea.Cmd ) {
817+ for i , tuiMsg := range m .messages {
818+ if tuiMsg .Type == types .MessageTypeAssistantReasoningBlock {
819+ if block , ok := m .views [i ].(* reasoningblock.Model ); ok && block .ID () == blockID {
820+ updatedView , cmd := m .views [i ].Update (msg )
821+ m .views [i ] = updatedView
822+ m .invalidateItem (i )
823+ return m , cmd
824+ }
825+ }
826+ }
827+ return m , nil
828+ }
829+
785830// Message management methods
786831func (m * model ) AddUserMessage (content string ) tea.Cmd {
787832 return m .addMessage (types .User (content ))
@@ -913,6 +958,7 @@ func (m *model) LoadFromSession(sess *session.Session) tea.Cmd {
913958 m .rendered = ""
914959 m .scrollOffset = 0
915960 m .totalHeight = 0
961+ m .bottomSlack = 0
916962 m .selectedMessageIndex = - 1
917963
918964 var cmds []tea.Cmd
@@ -1189,6 +1235,10 @@ func (m *model) contentWidth() int {
11891235 return m .width - 2
11901236}
11911237
1238+ func (m * model ) totalScrollableHeight () int {
1239+ return m .totalHeight + m .bottomSlack
1240+ }
1241+
11921242// Helper methods
11931243func (m * model ) createToolCallView (msg * types.Message ) layout.Model {
11941244 view := tool .New (msg , m .sessionState )
@@ -1258,6 +1308,8 @@ func (m *model) isMouseOnScrollbar(x, y int) bool {
12581308func (m * model ) handleScrollbarUpdate (msg tea.Msg ) (layout.Model , tea.Cmd ) {
12591309 sb , cmd := m .scrollbar .Update (msg )
12601310 m .scrollbar = sb
1311+ m .userHasScrolled = true
1312+ m .bottomSlack = 0
12611313 m .scrollOffset = m .scrollbar .GetScrollOffset ()
12621314 return m , cmd
12631315}
0 commit comments