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}