on main help sign in
fix: new/update message behavior (#1958)
6ba301eKujtim Hoxha
internal/ui/list/list.go+26
@@ -75,6 +75,32 @@ func (l *List) Gap() int {
75 return l.gap
75 return l.gap
76}
76}
77
77
78+// AtBottom returns whether the list is scrolled to the bottom.
79+func (l *List) AtBottom() bool {
80+ if len(l.items) == 0 {
81+ return true
82+ }
83+
84+ // Calculate total height of all items from the bottom.
85+ var totalHeight int
86+ for i := len(l.items) - 1; i >= 0; i-- {
87+ item := l.getItem(i)
88+ totalHeight += item.height
89+ if l.gap > 0 && i < len(l.items)-1 {
90+ totalHeight += l.gap
91+ }
92+ if totalHeight >= l.height {
93+ // This is the expected bottom position.
94+ expectedIdx := i
95+ expectedLine := totalHeight - l.height
96+ return l.offsetIdx == expectedIdx && l.offsetLine >= expectedLine
97+ }
98+ }
99+
100+ // All items fit in viewport - we're at bottom if at top.
101+ return l.offsetIdx == 0 && l.offsetLine == 0
102+}
103+
78// SetReverse shows the list in reverse order.
104// SetReverse shows the list in reverse order.
79func (l *List) SetReverse(reverse bool) {
105func (l *List) SetReverse(reverse bool) {
80 l.reverse = reverse
106 l.reverse = reverse
internal/ui/model/chat.go+2−2
@@ -481,9 +481,9 @@ func (m *Chat) HasHighlight() bool {
481 return startItemIdx >= 0 && endItemIdx >= 0 && (startLine != endLine || startCol != endCol)
481 return startItemIdx >= 0 && endItemIdx >= 0 && (startLine != endLine || startCol != endCol)
482}
482}
483
483
484−// HighlighContent returns the currently highlighted content based on the mouse
484+// HighlightContent returns the currently highlighted content based on the mouse
485// selection. It returns an empty string if no content is highlighted.
485// selection. It returns an empty string if no content is highlighted.
486−func (m *Chat) HighlighContent() string {
486+func (m *Chat) HighlightContent() string {
487 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
487 startItemIdx, startLine, startCol, endItemIdx, endLine, endCol := m.getHighlightRange()
488 if startItemIdx < 0 || endItemIdx < 0 || startLine == endLine && startCol == endCol {
488 if startItemIdx < 0 || endItemIdx < 0 || startLine == endLine && startCol == endCol {
489 return ""
489 return ""
internal/ui/model/ui.go+14−6
@@ -873,6 +873,7 @@ func (m *UI) appendSessionMessage(msg message.Message) tea.Cmd {
873func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
873func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
874 var cmds []tea.Cmd
874 var cmds []tea.Cmd
875 existingItem := m.chat.MessageItem(msg.ID)
875 existingItem := m.chat.MessageItem(msg.ID)
876+ atBottom := m.chat.list.AtBottom()
876
877
877 if existingItem != nil {
878 if existingItem != nil {
878 if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
879 if assistantItem, ok := existingItem.(*chat.AssistantMessageItem); ok {
@@ -893,9 +894,6 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
893 if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
894 if infoItem := m.chat.MessageItem(chat.AssistantInfoID(msg.ID)); infoItem == nil {
894 newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, time.Unix(m.lastUserMessageTime, 0))
895 newInfoItem := chat.NewAssistantInfoItem(m.com.Styles, &msg, time.Unix(m.lastUserMessageTime, 0))
895 m.chat.AppendMessages(newInfoItem)
896 m.chat.AppendMessages(newInfoItem)
896− if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
897− cmds = append(cmds, cmd)
898− }
899 }
897 }
900 }
898 }
901
899
@@ -922,9 +920,12 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
922 }
920 }
923 }
921 }
924 }
922 }
923+
925 m.chat.AppendMessages(items...)
924 m.chat.AppendMessages(items...)
925+ if atBottom {
926− if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
926+ if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
927− cmds = append(cmds, cmd)
927+ cmds = append(cmds, cmd)
928+ }
928 }
929 }
929
930
930 return tea.Batch(cmds...)
931 return tea.Batch(cmds...)
@@ -934,6 +935,7 @@ func (m *UI) updateSessionMessage(msg message.Message) tea.Cmd {
934func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.Cmd {
935func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.Cmd {
935 var cmds []tea.Cmd
936 var cmds []tea.Cmd
936
937
938+ atBottom := m.chat.list.AtBottom()
937 // Only process messages with tool calls or results.
939 // Only process messages with tool calls or results.
938 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
940 if len(event.Payload.ToolCalls()) == 0 && len(event.Payload.ToolResults()) == 0 {
939 return nil
941 return nil
@@ -1013,6 +1015,12 @@ func (m *UI) handleChildSessionMessage(event pubsub.Event[message.Message]) tea.
1013 // Update the chat so it updates the index map for animations to work as expected
1015 // Update the chat so it updates the index map for animations to work as expected
1014 m.chat.UpdateNestedToolIDs(toolCallID)
1016 m.chat.UpdateNestedToolIDs(toolCallID)
1015
1017
1018+ if atBottom {
1019+ if cmd := m.chat.ScrollToBottomAndAnimate(); cmd != nil {
1020+ cmds = append(cmds, cmd)
1021+ }
1022+ }
1023+
1016 return tea.Batch(cmds...)
1024 return tea.Batch(cmds...)
1017}
1025}
1018
1026
@@ -2905,7 +2913,7 @@ func (m *UI) runMCPPrompt(clientID, promptID string, arguments map[string]string
2905}
2913}
2906
2914
2907func (m *UI) copyChatHighlight() tea.Cmd {
2915func (m *UI) copyChatHighlight() tea.Cmd {
2908− text := m.chat.HighlighContent()
2916+ text := m.chat.HighlightContent()
2909 return tea.Sequence(
2917 return tea.Sequence(
2910 tea.SetClipboard(text),
2918 tea.SetClipboard(text),
2911 func() tea.Msg {
2919 func() tea.Msg {