chat.go

  1// Package chat provides the chat UI components for displaying and managing
  2// conversation messages between users and assistants.
  3package chat
  4
  5import (
  6	tea "charm.land/bubbletea/v2"
  7	"github.com/charmbracelet/crush/internal/message"
  8	"github.com/charmbracelet/crush/internal/ui/common"
  9	"github.com/charmbracelet/crush/internal/ui/list"
 10	uv "github.com/charmbracelet/ultraviolet"
 11)
 12
 13// maxTextWidth is the maximum width text messages can be
 14const maxTextWidth = 120
 15
 16// Identifiable is an interface for items that can provide a unique identifier.
 17type Identifiable interface {
 18	ID() string
 19}
 20
 21// MessageItem represents a [message.Message] item that can be displayed in the
 22// UI and be part of a [list.List] identifiable by a unique ID.
 23type MessageItem interface {
 24	list.Item
 25	Identifiable
 26}
 27
 28// Animatable is implemented by items that support animation initialization.
 29type Animatable interface {
 30	InitAnimation() tea.Cmd
 31}
 32
 33// ToolItem is implemented by tool items that support mutable updates.
 34type ToolItem interface {
 35	SetResult(result message.ToolResult)
 36	SetCancelled()
 37	UpdateCall(call message.ToolCall)
 38	SetNestedCalls(calls []ToolCallContext)
 39	Context() *ToolCallContext
 40}
 41
 42// Chat represents the chat UI model that handles chat interactions and
 43// messages.
 44type Chat struct {
 45	com  *common.Common
 46	list *list.List
 47}
 48
 49// NewChat creates a new instance of [Chat] that handles chat interactions and
 50// messages.
 51func NewChat(com *common.Common) *Chat {
 52	l := list.NewList()
 53	l.SetGap(1)
 54	return &Chat{
 55		com:  com,
 56		list: l,
 57	}
 58}
 59
 60// Height returns the height of the chat view port.
 61func (m *Chat) Height() int {
 62	return m.list.Height()
 63}
 64
 65// Draw renders the chat UI component to the screen and the given area.
 66func (m *Chat) Draw(scr uv.Screen, area uv.Rectangle) {
 67	uv.NewStyledString(m.list.Render()).Draw(scr, area)
 68}
 69
 70// SetSize sets the size of the chat view port.
 71func (m *Chat) SetSize(width, height int) {
 72	m.list.SetSize(width, height)
 73}
 74
 75// Len returns the number of items in the chat list.
 76func (m *Chat) Len() int {
 77	return m.list.Len()
 78}
 79
 80// SetMessages sets the chat messages to the provided list of message items.
 81func (m *Chat) SetMessages(msgs ...MessageItem) {
 82	items := make([]list.Item, len(msgs))
 83	for i, msg := range msgs {
 84		items[i] = msg
 85	}
 86	m.list.SetItems(items...)
 87	m.list.ScrollToBottom()
 88}
 89
 90// AppendMessages appends a new message item to the chat list.
 91func (m *Chat) AppendMessages(msgs ...MessageItem) {
 92	items := make([]list.Item, len(msgs))
 93	for i, msg := range msgs {
 94		items[i] = msg
 95	}
 96	m.list.AppendItems(items...)
 97}
 98
 99// AppendItems appends new items to the chat list.
100func (m *Chat) AppendItems(items ...list.Item) {
101	m.list.AppendItems(items...)
102	m.list.ScrollToIndex(m.list.Len() - 1)
103}
104
105// GetMessage returns the message with the given ID. Returns nil if not found.
106func (m *Chat) GetMessage(id string) MessageItem {
107	for i := 0; i < m.list.Len(); i++ {
108		item := m.list.GetItemAt(i)
109		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
110			if msg, ok := item.(MessageItem); ok {
111				return msg
112			}
113		}
114	}
115	return nil
116}
117
118// Focus sets the focus state of the chat component.
119func (m *Chat) Focus() {
120	m.list.Focus()
121}
122
123// Blur removes the focus state from the chat component.
124func (m *Chat) Blur() {
125	m.list.Blur()
126}
127
128// ScrollToTop scrolls the chat view to the top.
129func (m *Chat) ScrollToTop() {
130	m.list.ScrollToTop()
131}
132
133// ScrollToBottom scrolls the chat view to the bottom.
134func (m *Chat) ScrollToBottom() {
135	m.list.ScrollToBottom()
136}
137
138// ScrollBy scrolls the chat view by the given number of line deltas.
139func (m *Chat) ScrollBy(lines int) {
140	m.list.ScrollBy(lines)
141}
142
143// ScrollToSelected scrolls the chat view to the selected item.
144func (m *Chat) ScrollToSelected() {
145	m.list.ScrollToSelected()
146}
147
148// SelectedItemInView returns whether the selected item is currently in view.
149func (m *Chat) SelectedItemInView() bool {
150	return m.list.SelectedItemInView()
151}
152
153// SetSelected sets the selected message index in the chat list.
154func (m *Chat) SetSelected(index int) {
155	m.list.SetSelected(index)
156}
157
158// SelectPrev selects the previous message in the chat list.
159func (m *Chat) SelectPrev() {
160	m.list.SelectPrev()
161}
162
163// SelectNext selects the next message in the chat list.
164func (m *Chat) SelectNext() {
165	m.list.SelectNext()
166}
167
168// SelectFirst selects the first message in the chat list.
169func (m *Chat) SelectFirst() {
170	m.list.SelectFirst()
171}
172
173// SelectLast selects the last message in the chat list.
174func (m *Chat) SelectLast() {
175	m.list.SelectLast()
176}
177
178// SelectFirstInView selects the first message currently in view.
179func (m *Chat) SelectFirstInView() {
180	m.list.SelectFirstInView()
181}
182
183// SelectLastInView selects the last message currently in view.
184func (m *Chat) SelectLastInView() {
185	m.list.SelectLastInView()
186}
187
188// HandleMouseDown handles mouse down events for the chat component.
189func (m *Chat) HandleMouseDown(x, y int) {
190	m.list.HandleMouseDown(x, y)
191}
192
193// HandleMouseUp handles mouse up events for the chat component.
194func (m *Chat) HandleMouseUp(x, y int) {
195	m.list.HandleMouseUp(x, y)
196}
197
198// HandleMouseDrag handles mouse drag events for the chat component.
199func (m *Chat) HandleMouseDrag(x, y int) {
200	m.list.HandleMouseDrag(x, y)
201}
202
203// HandleKeyPress handles key press events for the currently selected item.
204func (m *Chat) HandleKeyPress(msg tea.KeyPressMsg) bool {
205	return m.list.HandleKeyPress(msg)
206}
207
208// UpdateItems propagates a message to all items that support updates (e.g.,
209// for animations). Returns commands from updated items.
210func (m *Chat) UpdateItems(msg tea.Msg) tea.Cmd {
211	return m.list.UpdateItems(msg)
212}
213
214// GetToolItem returns the tool item with the given ID, or nil if not found.
215func (m *Chat) GetToolItem(id string) ToolItem {
216	for i := 0; i < m.list.Len(); i++ {
217		item := m.list.GetItemAt(i)
218		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
219			if toolItem, ok := item.(ToolItem); ok {
220				return toolItem
221			}
222		}
223	}
224	return nil
225}
226
227// InvalidateItem invalidates the render cache for the item with the given ID.
228// Use after mutating an item via ToolItem methods.
229func (m *Chat) InvalidateItem(id string) {
230	for i := 0; i < m.list.Len(); i++ {
231		item := m.list.GetItemAt(i)
232		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
233			m.list.InvalidateItemAt(i)
234			return
235		}
236	}
237}
238
239// DeleteMessage removes a message by ID. Returns true if found and deleted.
240func (m *Chat) DeleteMessage(id string) bool {
241	for i := m.list.Len() - 1; i >= 0; i-- {
242		item := m.list.GetItemAt(i)
243		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
244			return m.list.DeleteItemAt(i)
245		}
246	}
247	return false
248}