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}