1package chat
2
3import (
4 "image"
5
6 "github.com/charmbracelet/crush/internal/message"
7 "github.com/charmbracelet/crush/internal/ui/list"
8 "github.com/charmbracelet/crush/internal/ui/styles"
9)
10
11// this is the total width that is taken up by the border + padding
12// we also cap the width so text is readable to the maxTextWidth(120)
13const messageLeftPaddingTotal = 2
14
15// maxTextWidth is the maximum width text messages can be
16const maxTextWidth = 120
17
18// Identifiable is an interface for items that can provide a unique identifier.
19type Identifiable interface {
20 ID() string
21}
22
23// MessageItem represents a [message.Message] item that can be displayed in the
24// UI and be part of a [list.List] identifiable by a unique ID.
25type MessageItem interface {
26 list.Item
27 list.Highlightable
28 list.Focusable
29 Identifiable
30}
31
32// SendMsg represents a message to send a chat message.
33type SendMsg struct {
34 Text string
35 Attachments []message.Attachment
36}
37
38type highlightableMessageItem struct {
39 startLine int
40 startCol int
41 endLine int
42 endCol int
43 highlighter list.Highlighter
44}
45
46// isHighlighted returns true if the item has a highlight range set.
47func (h *highlightableMessageItem) isHighlighted() bool {
48 return h.startLine != -1 || h.endLine != -1
49}
50
51// renderHighlighted highlights the content if necessary.
52func (h *highlightableMessageItem) renderHighlighted(content string, width, height int) string {
53 if !h.isHighlighted() {
54 return content
55 }
56 area := image.Rect(0, 0, width, height)
57 return list.Highlight(content, area, h.startLine, h.startCol, h.endLine, h.endCol, h.highlighter)
58}
59
60// Highlight implements MessageItem.
61func (h *highlightableMessageItem) Highlight(startLine int, startCol int, endLine int, endCol int) {
62 // Adjust columns for the style's left inset (border + padding) since we
63 // highlight the content only.
64 offset := messageLeftPaddingTotal
65 h.startLine = startLine
66 h.startCol = max(0, startCol-offset)
67 h.endLine = endLine
68 if endCol >= 0 {
69 h.endCol = max(0, endCol-offset)
70 } else {
71 h.endCol = endCol
72 }
73}
74
75func defaultHighlighter(sty *styles.Styles) *highlightableMessageItem {
76 return &highlightableMessageItem{
77 startLine: -1,
78 startCol: -1,
79 endLine: -1,
80 endCol: -1,
81 highlighter: list.ToHighlighter(sty.TextSelection),
82 }
83}
84
85// cachedMessageItem caches rendered message content to avoid re-rendering.
86//
87// This should be used by any message that can store a cahced version of its render. e.x user,assistant... and so on
88//
89// THOUGHT(kujtim): we should consider if its efficient to store the render for different widths
90// the issue with that could be memory usage
91type cachedMessageItem struct {
92 // rendered is the cached rendered string
93 rendered string
94 // width and height are the dimensions of the cached render
95 width int
96 height int
97}
98
99// getCachedRender returns the cached render if it exists for the given width.
100func (c *cachedMessageItem) getCachedRender(width int) (string, int, bool) {
101 if c.width == width && c.rendered != "" {
102 return c.rendered, c.height, true
103 }
104 return "", 0, false
105}
106
107// setCachedRender sets the cached render.
108func (c *cachedMessageItem) setCachedRender(rendered string, width, height int) {
109 c.rendered = rendered
110 c.width = width
111 c.height = height
112}
113
114// cappedMessageWidth returns the maximum width for message content for readability.
115func cappedMessageWidth(availableWidth int) int {
116 return min(availableWidth-messageLeftPaddingTotal, maxTextWidth)
117}
118
119// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
120// all parts of the message as [MessageItem]s.
121//
122// For assistant messages with tool calls, pass a toolResults map to link results.
123// Use BuildToolResultMap to create this map from all messages in a session.
124func GetMessageItems(sty *styles.Styles, msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
125 switch msg.Role {
126 case message.User:
127 return []MessageItem{NewUserMessageItem(sty, msg)}
128 }
129 return []MessageItem{}
130}
131
132// BuildToolResultMap creates a map of tool call IDs to their results from a list of messages.
133// Tool result messages (role == message.Tool) contain the results that should be linked
134// to tool calls in assistant messages.
135func BuildToolResultMap(messages []*message.Message) map[string]message.ToolResult {
136 resultMap := make(map[string]message.ToolResult)
137 for _, msg := range messages {
138 if msg.Role == message.Tool {
139 for _, result := range msg.ToolResults() {
140 if result.ToolCallID != "" {
141 resultMap[result.ToolCallID] = result
142 }
143 }
144 }
145 }
146 return resultMap
147}