@@ -75,6 +75,32 @@ func (l *List) Gap() int {
return l.gap
}
+// AtBottom returns whether the list is scrolled to the bottom.
+func (l *List) AtBottom() bool {
+ if len(l.items) == 0 {
+ return true
+ }
+
+ // Calculate total height of all items from the bottom.
+ var totalHeight int
+ for i := len(l.items) - 1; i >= 0; i-- {
+ item := l.getItem(i)
+ totalHeight += item.height
+ if l.gap > 0 && i < len(l.items)-1 {
+ totalHeight += l.gap
+ }
+ if totalHeight >= l.height {
+ // This is the expected bottom position.
+ expectedIdx := i
+ expectedLine := totalHeight - l.height
+ return l.offsetIdx == expectedIdx && l.offsetLine >= expectedLine
+ }
+ }
+
+ // All items fit in viewport - we're at bottom if at top.
+ return l.offsetIdx == 0 && l.offsetLine == 0
+}
+
// SetReverse shows the list in reverse order.
func (l *List) SetReverse(reverse bool) {
l.reverse = reverse
@@ -481,9 +481,9 @@ func (m *Chat) HasHighlight() bool {
return startItemIdx >= 0 && endItemIdx >= 0 && (startLine != endLine || startCol != endCol)
}
-// HighlighContent returns the currently highlighted content based on the mouse
+// HighlightContent returns the currently highlighted content based on the mouse
// selection. It returns an empty string if no content is highlighted.
-func (m *Chat) HighlighContent() string {
+func (m *Chat) HighlightContent() string {
startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
if startItemIdx < 0 || endItemIdx < 0 || startLine == endLine && startCol == endCol {
return ""
@@ -873,6 +873,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
var cmds []tea.Cmd
existingItem := m.chat.MessageItem(msg.ID)
+ atBottom := m.chat.list.AtBottom()
if existingItem != nil {
if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
@@ -893,9 +894,6 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, time.Unix(m.lastUserMessageTime, 0))
m.chat.AppendMessages(newInfoItem)
- if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
- cmds = append(cmds, cmd)
- }
}
}
@@ -922,9 +920,12 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
}
}
}
+
m.chat.AppendMessages(items...)
- if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
- cmds = append(cmds, cmd)
+ if atBottom {
+ if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
}
return tea.Batch(cmds...)
@@ -934,6 +935,7 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.Cmd {
var cmds []tea.Cmd
+ atBottom := m.chat.list.AtBottom()
// Only process messages with tool calls or results.
if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
return nil
@@ -1013,6 +1015,12 @@ func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.
// Update the chat so it updates the index map for animations to work as expected
m.chat.UpdateNestedToolIDs(toolCallID)
+ if atBottom {
+ if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+
return tea.Batch(cmds...)
}
@@ -2905,7 +2913,7 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
}
func (m *UI) copyChatHighlight() tea.Cmd {
- text := m.chat.HighlighContent()
+ text := m.chat.HighlightContent()
return tea.Sequence(
tea.SetClipboard(text),
func() tea.Msg {