messages.go

  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}