From 6ba301e6e1214eb8f8f5fcf2d51af0781d8ef7be Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Fri, 23 Jan 2026 15:47:23 +0100 Subject: [PATCH] fix: new/update message behavior (#1958) --- internal/ui/list/list.go | 26 ++++++++++++++++++++++++++ internal/ui/model/chat.go | 4 ++-- internal/ui/model/ui.go | 20 ++++++++++++++------ 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/internal/ui/list/list.go b/internal/ui/list/list.go index 78cb437d361f8ec05d81acfa98f2e87a23755d58..6b9fbf45b0dfcdc310a014b42eb04950aa891a71 100644 --- a/internal/ui/list/list.go +++ b/internal/ui/list/list.go @@ -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 diff --git a/internal/ui/model/chat.go b/internal/ui/model/chat.go index 9ab77c60f6b1a48228c5d08ae4d9827584b62e6d..f3388085d1808d984ed4b90bdeaeb58d71cb2463 100644 --- a/internal/ui/model/chat.go +++ b/internal/ui/model/chat.go @@ -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 "" diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 78846fd86c2f271f1293290e126ea7e5d1c73b87..a104168c58422d01363850976ab68f1f99d90137 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -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 {