diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index ef88c6311c57553a7794a14f7384a58dec7663b8..d81e047051ecc6160d06483e5c5b3cf4186149de 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -2,6 +2,7 @@ package chat import ( "context" + "strings" "time" "github.com/atotto/clipboard" @@ -18,7 +19,6 @@ import ( "github.com/charmbracelet/crush/internal/tui/exp/list" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" - "github.com/charmbracelet/lipgloss/v2" ) type SendMsg struct { @@ -72,6 +72,7 @@ type messageListCmp struct { lastClickX int lastClickY int clickCount int + promptQueue int } // New creates a new message list component with custom keybindings @@ -101,14 +102,24 @@ func (m *messageListCmp) Init() tea.Cmd { // Update handles incoming messages and updates the component state. func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + if m.session.ID != "" && m.app.CoderAgent != nil { + queueSize := m.app.CoderAgent.QueuedPrompts(m.session.ID) + if queueSize != m.promptQueue { + m.promptQueue = queueSize + cmds = append(cmds, m.SetSize(m.width, m.height)) + } + } switch msg := msg.(type) { case tea.KeyPressMsg: if m.listCmp.IsFocused() && m.listCmp.HasSelection() { switch { case key.Matches(msg, messages.CopyKey): - return m, m.CopySelectedText(true) + cmds = append(cmds, m.CopySelectedText(true)) + return m, tea.Batch(cmds...) case key.Matches(msg, messages.ClearSelectionKey): - return m, m.SelectionClear() + cmds = append(cmds, m.SelectionClear()) + return m, tea.Batch(cmds...) } } case tea.MouseClickMsg: @@ -118,39 +129,45 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil // Ignore clicks outside the component } if msg.Button == tea.MouseLeft { - return m, m.handleMouseClick(x, y) + cmds = append(cmds, m.handleMouseClick(x, y)) + return m, tea.Batch(cmds...) } - return m, nil + return m, tea.Batch(cmds...) case tea.MouseMotionMsg: x := msg.X - 1 // Adjust for padding y := msg.Y - 1 // Adjust for padding if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 { if y < 0 { - return m, m.listCmp.MoveUp(1) + cmds = append(cmds, m.listCmp.MoveUp(1)) + return m, tea.Batch(cmds...) } if y >= m.height-1 { - return m, m.listCmp.MoveDown(1) + cmds = append(cmds, m.listCmp.MoveDown(1)) + return m, tea.Batch(cmds...) } return m, nil // Ignore clicks outside the component } if msg.Button == tea.MouseLeft { m.listCmp.EndSelection(x, y) } - return m, nil + return m, tea.Batch(cmds...) case tea.MouseReleaseMsg: x := msg.X - 1 // Adjust for padding y := msg.Y - 1 // Adjust for padding if msg.Button == tea.MouseLeft { clickCount := m.clickCount if x < 0 || y < 0 || x >= m.width-2 || y >= m.height-1 { - return m, tea.Tick(doubleClickThreshold, func(time.Time) tea.Msg { + tick := tea.Tick(doubleClickThreshold, func(time.Time) tea.Msg { return SelectionCopyMsg{ clickCount: clickCount, endSelection: false, } }) + + cmds = append(cmds, tick) + return m, tea.Batch(cmds...) } - return m, tea.Tick(doubleClickThreshold, func(time.Time) tea.Msg { + tick := tea.Tick(doubleClickThreshold, func(time.Time) tea.Msg { return SelectionCopyMsg{ clickCount: clickCount, endSelection: true, @@ -158,6 +175,8 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { y: y, } }) + cmds = append(cmds, tick) + return m, tea.Batch(cmds...) } return m, nil case SelectionCopyMsg: @@ -167,61 +186,60 @@ func (m *messageListCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.listCmp.EndSelection(msg.x, msg.y) } m.listCmp.SelectionStop() - return m, m.CopySelectedText(true) + cmds = append(cmds, m.CopySelectedText(true)) + return m, tea.Batch(cmds...) } case pubsub.Event[permission.PermissionNotification]: - return m, m.handlePermissionRequest(msg.Payload) + cmds = append(cmds, m.handlePermissionRequest(msg.Payload)) + return m, tea.Batch(cmds...) case SessionSelectedMsg: if msg.ID != m.session.ID { - cmd := m.SetSession(msg) - return m, cmd + cmds = append(cmds, m.SetSession(msg)) } - return m, nil + return m, tea.Batch(cmds...) case SessionClearedMsg: m.session = session.Session{} - return m, m.listCmp.SetItems([]list.Item{}) + cmds = append(cmds, m.listCmp.SetItems([]list.Item{})) + return m, tea.Batch(cmds...) case pubsub.Event[message.Message]: - cmd := m.handleMessageEvent(msg) - return m, cmd + cmds = append(cmds, m.handleMessageEvent(msg)) + return m, tea.Batch(cmds...) case tea.MouseWheelMsg: u, cmd := m.listCmp.Update(msg) m.listCmp = u.(list.List[list.Item]) - return m, cmd + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } u, cmd := m.listCmp.Update(msg) m.listCmp = u.(list.List[list.Item]) - return m, cmd + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } // View renders the message list or an initial screen if empty. func (m *messageListCmp) View() string { t := styles.CurrentTheme() - listView := t.S().Base. - Padding(1, 1, 0, 1). - Width(m.width). - Height(m.height). - Render( - m.listCmp.View(), - ) - - if m.app.CoderAgent != nil && m.app.CoderAgent.QueuedPrompts(m.session.ID) > 0 { - queue := m.app.CoderAgent.QueuedPrompts(m.session.ID) - queuePill := queuePill(queue, t) - layers := []*lipgloss.Layer{ - lipgloss.NewLayer(listView), - lipgloss.NewLayer( - queuePill, - ).X(4).Y(m.height - 3), - } - canvas := lipgloss.NewCanvas( - layers..., - ) - return canvas.Render() + height := m.height + if m.promptQueue > 0 { + height -= 3 + } + view := []string{ + t.S().Base. + Padding(1, 1, 0, 1). + Width(m.width). + Height(height). + Render( + m.listCmp.View(), + ), + } + if m.app.CoderAgent != nil && m.promptQueue > 0 { + queuePill := queuePill(m.promptQueue, t) + view = append(view, t.S().Base.PaddingLeft(4).Render(queuePill)) } - return listView + return strings.Join(view, "\n") } func (m *messageListCmp) handlePermissionRequest(permission permission.PermissionNotification) tea.Cmd { @@ -638,6 +656,10 @@ func (m *messageListCmp) GetSize() (int, int) { func (m *messageListCmp) SetSize(width int, height int) tea.Cmd { m.width = width m.height = height + if m.promptQueue > 0 { + queueHeight := 3 + return m.listCmp.SetSize(width-2, height-(1+queueHeight)) + } return m.listCmp.SetSize(width-2, height-1) // for padding }