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// PrependItems prepends new items to the chat list.
 81func (m *Chat) PrependItems(items ...list.Item) {
 82	m.list.PrependItems(items...)
 83	m.list.ScrollToIndex(0)
 84}
 85
 86// SetMessages sets the chat messages to the provided list of message items.
 87func (m *Chat) SetMessages(msgs ...MessageItem) {
 88	items := make([]list.Item, len(msgs))
 89	for i, msg := range msgs {
 90		items[i] = msg
 91	}
 92	m.list.SetItems(items...)
 93	m.list.ScrollToBottom()
 94}
 95
 96// AppendMessages appends a new message item to the chat list.
 97func (m *Chat) AppendMessages(msgs ...MessageItem) {
 98	items := make([]list.Item, len(msgs))
 99	for i, msg := range msgs {
100		items[i] = msg
101	}
102	m.list.AppendItems(items...)
103}
104
105// AppendItems appends new items to the chat list.
106func (m *Chat) AppendItems(items ...list.Item) {
107	m.list.AppendItems(items...)
108	m.list.ScrollToIndex(m.list.Len() - 1)
109}
110
111// UpdateMessage updates an existing message by ID. Returns true if the message
112// was found and updated.
113func (m *Chat) UpdateMessage(id string, msg MessageItem) bool {
114	for i := 0; i < m.list.Len(); i++ {
115		item := m.list.GetItemAt(i)
116		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
117			return m.list.UpdateItemAt(i, msg)
118		}
119	}
120	return false
121}
122
123// GetMessage returns the message with the given ID. Returns nil if not found.
124func (m *Chat) GetMessage(id string) MessageItem {
125	for i := 0; i < m.list.Len(); i++ {
126		item := m.list.GetItemAt(i)
127		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
128			if msg, ok := item.(MessageItem); ok {
129				return msg
130			}
131		}
132	}
133	return nil
134}
135
136// Focus sets the focus state of the chat component.
137func (m *Chat) Focus() {
138	m.list.Focus()
139}
140
141// Blur removes the focus state from the chat component.
142func (m *Chat) Blur() {
143	m.list.Blur()
144}
145
146// ScrollToTop scrolls the chat view to the top.
147func (m *Chat) ScrollToTop() {
148	m.list.ScrollToTop()
149}
150
151// ScrollToBottom scrolls the chat view to the bottom.
152func (m *Chat) ScrollToBottom() {
153	m.list.ScrollToBottom()
154}
155
156// ScrollBy scrolls the chat view by the given number of line deltas.
157func (m *Chat) ScrollBy(lines int) {
158	m.list.ScrollBy(lines)
159}
160
161// ScrollToSelected scrolls the chat view to the selected item.
162func (m *Chat) ScrollToSelected() {
163	m.list.ScrollToSelected()
164}
165
166// SelectedItemInView returns whether the selected item is currently in view.
167func (m *Chat) SelectedItemInView() bool {
168	return m.list.SelectedItemInView()
169}
170
171// SetSelected sets the selected message index in the chat list.
172func (m *Chat) SetSelected(index int) {
173	m.list.SetSelected(index)
174}
175
176// SelectPrev selects the previous message in the chat list.
177func (m *Chat) SelectPrev() {
178	m.list.SelectPrev()
179}
180
181// SelectNext selects the next message in the chat list.
182func (m *Chat) SelectNext() {
183	m.list.SelectNext()
184}
185
186// SelectFirst selects the first message in the chat list.
187func (m *Chat) SelectFirst() {
188	m.list.SelectFirst()
189}
190
191// SelectLast selects the last message in the chat list.
192func (m *Chat) SelectLast() {
193	m.list.SelectLast()
194}
195
196// SelectFirstInView selects the first message currently in view.
197func (m *Chat) SelectFirstInView() {
198	m.list.SelectFirstInView()
199}
200
201// SelectLastInView selects the last message currently in view.
202func (m *Chat) SelectLastInView() {
203	m.list.SelectLastInView()
204}
205
206// HandleMouseDown handles mouse down events for the chat component.
207func (m *Chat) HandleMouseDown(x, y int) {
208	m.list.HandleMouseDown(x, y)
209}
210
211// HandleMouseUp handles mouse up events for the chat component.
212func (m *Chat) HandleMouseUp(x, y int) {
213	m.list.HandleMouseUp(x, y)
214}
215
216// HandleMouseDrag handles mouse drag events for the chat component.
217func (m *Chat) HandleMouseDrag(x, y int) {
218	m.list.HandleMouseDrag(x, y)
219}
220
221// HandleKeyPress handles key press events for the currently selected item.
222func (m *Chat) HandleKeyPress(msg tea.KeyPressMsg) bool {
223	return m.list.HandleKeyPress(msg)
224}
225
226// UpdateItems propagates a message to all items that support updates (e.g.,
227// for animations). Returns commands from updated items.
228func (m *Chat) UpdateItems(msg tea.Msg) tea.Cmd {
229	return m.list.UpdateItems(msg)
230}
231
232// GetToolItem returns the tool item with the given ID, or nil if not found.
233func (m *Chat) GetToolItem(id string) ToolItem {
234	for i := 0; i < m.list.Len(); i++ {
235		item := m.list.GetItemAt(i)
236		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
237			if toolItem, ok := item.(ToolItem); ok {
238				return toolItem
239			}
240		}
241	}
242	return nil
243}
244
245// InvalidateItem invalidates the render cache for the item with the given ID.
246// Use after mutating an item via ToolItem methods.
247func (m *Chat) InvalidateItem(id string) {
248	for i := 0; i < m.list.Len(); i++ {
249		item := m.list.GetItemAt(i)
250		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
251			m.list.InvalidateItemAt(i)
252			return
253		}
254	}
255}
256
257// DeleteMessage removes a message by ID. Returns true if found and deleted.
258func (m *Chat) DeleteMessage(id string) bool {
259	for i := m.list.Len() - 1; i >= 0; i-- {
260		item := m.list.GetItemAt(i)
261		if identifiable, ok := item.(Identifiable); ok && identifiable.ID() == id {
262			return m.list.DeleteItemAt(i)
263		}
264	}
265	return false
266}