Skip to content

Commit 5a7e916

Browse files
committed
fade out competed tool calls in collpased reasoning blocks
also adds some "bottom slack" to the scrolling of messages so the UI doesn't jump up and down if tool calls in reasoning blocks appear and then get removed at the very bottom of the viewport Signed-off-by: Christopher Petito <[email protected]>
1 parent eb8f299 commit 5a7e916

File tree

4 files changed

+391
-22
lines changed

4 files changed

+391
-22
lines changed

pkg/tui/components/messages/messages.go

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
515538
func (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

522546
func (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

530555
func (m *model) scrollPageUp() {
531556
m.userHasScrolled = true
557+
m.bottomSlack = 0
532558
m.setScrollOffset(max(0, m.scrollOffset-m.height))
533559
}
534560

535561
func (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

543570
func (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

553581
func (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
782827
func (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
11891239
func (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 {
12541304
func (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

Comments
 (0)