@@ -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 }
@@ -216,6 +220,7 @@ func (m *model) handleMouseClick(msg tea.MouseClickMsg) (layout.Model, tea.Cmd)
216220 if block .IsToggleLine (localLine ) {
217221 block .Toggle ()
218222 m .userHasScrolled = true // Prevent auto-scroll jump
223+ m .bottomSlack = 0
219224 m .invalidateItem (msgIdx )
220225 return m , nil
221226 }
@@ -306,12 +311,14 @@ func (m *model) handleMouseWheel(msg tea.MouseWheelMsg) (layout.Model, tea.Cmd)
306311 case "wheelup" :
307312 if m .scrollOffset > 0 {
308313 m .userHasScrolled = true
314+ m .bottomSlack = 0
309315 for range mouseScrollAmount {
310316 m .setScrollOffset (m .scrollOffset - defaultScrollAmount )
311317 }
312318 }
313319 case "wheeldown" :
314320 m .userHasScrolled = true
321+ m .bottomSlack = 0
315322 for range mouseScrollAmount {
316323 m .setScrollOffset (m .scrollOffset + defaultScrollAmount )
317324 }
@@ -370,23 +377,39 @@ func (m *model) View() string {
370377 }
371378
372379 prevTotalHeight := m .totalHeight
380+ prevScrollableHeight := m .totalHeight + m .bottomSlack
373381 m .ensureAllItemsRendered ()
374382
375383 if m .totalHeight == 0 {
376384 return ""
377385 }
378386
379- // Calculate viewport bounds
380- maxScrollOffset := max (0 , m .totalHeight - m .height )
387+ if m .userHasScrolled {
388+ m .bottomSlack = 0
389+ } else {
390+ delta := m .totalHeight - prevTotalHeight
391+ if delta < 0 {
392+ m .bottomSlack += - delta
393+ } else if delta > 0 && m .bottomSlack > 0 {
394+ consume := min (delta , m .bottomSlack )
395+ m .bottomSlack -= consume
396+ }
397+ }
398+
399+ scrollableHeight := m .totalHeight + m .bottomSlack
400+ maxScrollOffset := max (0 , scrollableHeight - m .height )
381401
382- // Auto-scroll if content grew and user hasn't manually scrolled
383- if ! m .userHasScrolled && m . totalHeight > prevTotalHeight {
402+ // Auto-scroll when content grows beyond any slack.
403+ if ! m .userHasScrolled && scrollableHeight > prevScrollableHeight {
384404 m .scrollOffset = maxScrollOffset
385405 } else {
386406 m .scrollOffset = max (0 , min (m .scrollOffset , maxScrollOffset ))
387407 }
388408
389409 lines := strings .Split (m .rendered , "\n " )
410+ if m .bottomSlack > 0 {
411+ lines = append (lines , make ([]string , m .bottomSlack )... )
412+ }
390413 if len (lines ) == 0 {
391414 return ""
392415 }
@@ -515,12 +538,14 @@ const defaultScrollAmount = 1
515538func (m * model ) scrollUp () {
516539 if m .scrollOffset > 0 {
517540 m .userHasScrolled = true
541+ m .bottomSlack = 0
518542 m .setScrollOffset (max (0 , m .scrollOffset - defaultScrollAmount ))
519543 }
520544}
521545
522546func (m * model ) scrollDown () {
523547 m .userHasScrolled = true
548+ m .bottomSlack = 0
524549 m .setScrollOffset (m .scrollOffset + defaultScrollAmount )
525550 if m .isAtBottom () {
526551 m .userHasScrolled = false
@@ -529,11 +554,13 @@ func (m *model) scrollDown() {
529554
530555func (m * model ) scrollPageUp () {
531556 m .userHasScrolled = true
557+ m .bottomSlack = 0
532558 m .setScrollOffset (max (0 , m .scrollOffset - m .height ))
533559}
534560
535561func (m * model ) scrollPageDown () {
536562 m .userHasScrolled = true
563+ m .bottomSlack = 0
537564 m .setScrollOffset (m .scrollOffset + m .height )
538565 if m .isAtBottom () {
539566 m .userHasScrolled = false
@@ -542,6 +569,7 @@ func (m *model) scrollPageDown() {
542569
543570func (m * model ) scrollToTop () {
544571 m .userHasScrolled = true
572+ m .bottomSlack = 0
545573 m .setScrollOffset (0 )
546574}
547575
@@ -551,7 +579,7 @@ func (m *model) scrollToBottom() {
551579}
552580
553581func (m * model ) setScrollOffset (offset int ) {
554- maxOffset := max (0 , m .totalHeight - m .height )
582+ maxOffset := max (0 , m .totalScrollableHeight () - m .height )
555583 m .scrollOffset = max (0 , min (offset , maxOffset ))
556584 m .scrollbar .SetScrollOffset (m .scrollOffset )
557585}
@@ -560,7 +588,7 @@ func (m *model) isAtBottom() bool {
560588 if len (m .messages ) == 0 {
561589 return true
562590 }
563- maxScrollOffset := max (0 , m .totalHeight - m .height )
591+ maxScrollOffset := max (0 , m .totalScrollableHeight () - m .height )
564592 return m .scrollOffset >= maxScrollOffset
565593}
566594
@@ -653,9 +681,11 @@ func (m *model) scrollToSelectedMessage() {
653681 // Scroll to make the selected message visible
654682 if startLine < m .scrollOffset {
655683 m .userHasScrolled = true
684+ m .bottomSlack = 0
656685 m .setScrollOffset (startLine )
657686 } else if endLine > m .scrollOffset + m .height {
658687 m .userHasScrolled = true
688+ m .bottomSlack = 0
659689 m .setScrollOffset (endLine - m .height )
660690 }
661691}
@@ -778,6 +808,21 @@ func (m *model) invalidateAllItems() {
778808 m .renderDirty = true
779809}
780810
811+ // forwardToReasoningBlock finds the reasoning block with the given ID and forwards the message to it.
812+ func (m * model ) forwardToReasoningBlock (blockID string , msg tea.Msg ) (layout.Model , tea.Cmd ) {
813+ for i , tuiMsg := range m .messages {
814+ if tuiMsg .Type == types .MessageTypeAssistantReasoningBlock {
815+ if block , ok := m .views [i ].(* reasoningblock.Model ); ok && block .ID () == blockID {
816+ updatedView , cmd := m .views [i ].Update (msg )
817+ m .views [i ] = updatedView
818+ m .invalidateItem (i )
819+ return m , cmd
820+ }
821+ }
822+ }
823+ return m , nil
824+ }
825+
781826// Message management methods
782827func (m * model ) AddUserMessage (content string ) tea.Cmd {
783828 return m .addMessage (types .User (content ))
@@ -909,6 +954,7 @@ func (m *model) LoadFromSession(sess *session.Session) tea.Cmd {
909954 m .rendered = ""
910955 m .scrollOffset = 0
911956 m .totalHeight = 0
957+ m .bottomSlack = 0
912958 m .selectedMessageIndex = - 1
913959
914960 var cmds []tea.Cmd
@@ -1185,6 +1231,10 @@ func (m *model) contentWidth() int {
11851231 return m .width - 2
11861232}
11871233
1234+ func (m * model ) totalScrollableHeight () int {
1235+ return m .totalHeight + m .bottomSlack
1236+ }
1237+
11881238// Helper methods
11891239func (m * model ) createToolCallView (msg * types.Message ) layout.Model {
11901240 view := tool .New (msg , m .sessionState )
@@ -1254,6 +1304,8 @@ func (m *model) isMouseOnScrollbar(x, y int) bool {
12541304func (m * model ) handleScrollbarUpdate (msg tea.Msg ) (layout.Model , tea.Cmd ) {
12551305 sb , cmd := m .scrollbar .Update (msg )
12561306 m .scrollbar = sb
1307+ m .userHasScrolled = true
1308+ m .bottomSlack = 0
12571309 m .scrollOffset = m .scrollbar .GetScrollOffset ()
12581310 return m , cmd
12591311}
0 commit comments