@@ -1,548 +0,0 @@
9− "charm.land/lipgloss/v2"
10− "github.com/charmbracelet/x/ansi"
12− "github.com/charmbracelet/crush/internal/config"
13− "github.com/charmbracelet/crush/internal/message"
14− "github.com/charmbracelet/crush/internal/ui/common"
15− "github.com/charmbracelet/crush/internal/ui/list"
16− "github.com/charmbracelet/crush/internal/ui/styles"
17− "github.com/charmbracelet/crush/internal/ui/toolrender"
20−// Identifiable is an interface for items that can provide a unique identifier.
21−type Identifiable interface {
25−// MessageItem represents a [message.Message] item that can be displayed in the
26−// UI and be part of a [list.List] identifiable by a unique ID.
27−type MessageItem interface {
33−// MessageContentItem represents rendered message content (text, markdown, errors, etc).
34−type MessageContentItem struct {
37− role message.MessageRole
43−// NewMessageContentItem creates a new message content item.
44−func NewMessageContentItem(id, content string, role message.MessageRole, isMarkdown bool, sty *styles.Styles) *MessageContentItem {
45− m := &MessageContentItem{
48− isMarkdown: isMarkdown,
56−// ID implements Identifiable.
57−func (m *MessageContentItem) ID() string {
61−// FocusStyle returns the focus style.
62−func (m *MessageContentItem) FocusStyle() lipgloss.Style {
63− if m.role == message.User {
64− return m.sty.Chat.Message.UserFocused
66− return m.sty.Chat.Message.AssistantFocused
69−// BlurStyle returns the blur style.
70−func (m *MessageContentItem) BlurStyle() lipgloss.Style {
71− if m.role == message.User {
72− return m.sty.Chat.Message.UserBlurred
74− return m.sty.Chat.Message.AssistantBlurred
77−// HighlightStyle returns the highlight style.
78−func (m *MessageContentItem) HighlightStyle() lipgloss.Style {
79− return m.sty.TextSelection
82−// Render renders the content at the given width, using cache if available.
84−// It implements [list.Item].
85−func (m *MessageContentItem) Render(width int) string {
86− contentWidth := width
87− // Cap width to maxWidth for markdown
88− cappedWidth := contentWidth
90− cappedWidth = min(contentWidth, m.maxWidth)
95− renderer := common.MarkdownRenderer(m.sty, cappedWidth)
96− result, err := renderer.Render(m.content)
100− rendered = strings.TrimSuffix(result, "\n")
103− rendered = m.content
109−// ToolCallItem represents a rendered tool call with its header and content.
110−type ToolCallItem struct {
112− toolCall message.ToolCall
113− toolResult message.ToolResult
120−// cachedToolRender stores both the rendered string and its height.
121−type cachedToolRender struct {
126−// NewToolCallItem creates a new tool call item.
127−func NewToolCallItem(id string, toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool, isNested bool, sty *styles.Styles) *ToolCallItem {
131− toolResult: toolResult,
132− cancelled: cancelled,
140−// generateCacheKey creates a key that changes when tool call content changes.
141−func generateCacheKey(toolCall message.ToolCall, toolResult message.ToolResult, cancelled bool) string {
142− // Simple key based on result state - when result arrives or changes, key changes
143− return fmt.Sprintf("%s:%s:%v", toolCall.ID, toolResult.ToolCallID, cancelled)
146−// ID implements Identifiable.
147−func (t *ToolCallItem) ID() string {
151−// FocusStyle returns the focus style.
152−func (t *ToolCallItem) FocusStyle() lipgloss.Style {
153− return t.sty.Chat.Message.ToolCallFocused
156−// BlurStyle returns the blur style.
157−func (t *ToolCallItem) BlurStyle() lipgloss.Style {
158− return t.sty.Chat.Message.ToolCallBlurred
161−// HighlightStyle returns the highlight style.
162−func (t *ToolCallItem) HighlightStyle() lipgloss.Style {
163− return t.sty.TextSelection
166−// Render implements list.Item.
167−func (t *ToolCallItem) Render(width int) string {
168− // Render the tool call
169− ctx := &toolrender.RenderContext{
171− Result: t.toolResult,
172− Cancelled: t.cancelled,
173− IsNested: t.isNested,
178− rendered := toolrender.Render(ctx)
182−// AttachmentItem represents a file attachment in a user message.
183−type AttachmentItem struct {
190−// NewAttachmentItem creates a new attachment item.
191−func NewAttachmentItem(id, filename, path string, sty *styles.Styles) *AttachmentItem {
192− a := &AttachmentItem{
201−// ID implements Identifiable.
202−func (a *AttachmentItem) ID() string {
206−// FocusStyle returns the focus style.
207−func (a *AttachmentItem) FocusStyle() lipgloss.Style {
208− return a.sty.Chat.Message.AssistantFocused
211−// BlurStyle returns the blur style.
212−func (a *AttachmentItem) BlurStyle() lipgloss.Style {
213− return a.sty.Chat.Message.AssistantBlurred
216−// HighlightStyle returns the highlight style.
217−func (a *AttachmentItem) HighlightStyle() lipgloss.Style {
218− return a.sty.TextSelection
221−// Render implements list.Item.
222−func (a *AttachmentItem) Render(width int) string {
223− const maxFilenameWidth = 10
224− content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
226− styles.DocumentIcon,
227− ansi.Truncate(a.filename, maxFilenameWidth, "..."),
232− // return a.RenderWithHighlight(content, width, a.CurrentStyle())
235−// ThinkingItem represents thinking/reasoning content in assistant messages.
236−type ThinkingItem struct {
239− duration time.Duration
245−// NewThinkingItem creates a new thinking item.
246−func NewThinkingItem(id, thinking string, duration time.Duration, finished bool, sty *styles.Styles) *ThinkingItem {
258−// ID implements Identifiable.
259−func (t *ThinkingItem) ID() string {
263−// FocusStyle returns the focus style.
264−func (t *ThinkingItem) FocusStyle() lipgloss.Style {
265− return t.sty.Chat.Message.AssistantFocused
268−// BlurStyle returns the blur style.
269−func (t *ThinkingItem) BlurStyle() lipgloss.Style {
270− return t.sty.Chat.Message.AssistantBlurred
273−// HighlightStyle returns the highlight style.
274−func (t *ThinkingItem) HighlightStyle() lipgloss.Style {
275− return t.sty.TextSelection
278−// Render implements list.Item.
279−func (t *ThinkingItem) Render(width int) string {
280− cappedWidth := min(width, t.maxWidth)
282− renderer := common.PlainMarkdownRenderer(cappedWidth - 1)
283− rendered, err := renderer.Render(t.thinking)
285− // Fallback to line-by-line rendering
286− lines := strings.Split(t.thinking, "\n")
287− var content strings.Builder
288− lineStyle := t.sty.PanelMuted
289− for i, line := range lines {
293− content.WriteString(lineStyle.Width(cappedWidth).Render(line))
294− if i < len(lines)-1 {
295− content.WriteString("\n")
298− rendered = content.String()
301− fullContent := strings.TrimSpace(rendered)
303− // Add footer if finished
304− if t.finished && t.duration > 0 {
305− footer := t.sty.Chat.Message.ThinkingFooter.Render(fmt.Sprintf("Thought for %s", t.duration.String()))
306− fullContent = lipgloss.JoinVertical(lipgloss.Left, fullContent, "", footer)
309− result := t.sty.PanelMuted.Width(cappedWidth).Padding(0, 1).Render(fullContent)
314−// SectionHeaderItem represents a section header (e.g., assistant info).
315−type SectionHeaderItem struct {
318− duration time.Duration
319− isSectionHeader bool
324−// NewSectionHeaderItem creates a new section header item.
325−func NewSectionHeaderItem(id, modelName string, duration time.Duration, sty *styles.Styles) *SectionHeaderItem {
326− s := &SectionHeaderItem{
328− modelName: modelName,
330− isSectionHeader: true,
336−// ID implements Identifiable.
337−func (s *SectionHeaderItem) ID() string {
341−// IsSectionHeader returns true if this is a section header.
342−func (s *SectionHeaderItem) IsSectionHeader() bool {
343− return s.isSectionHeader
346−// FocusStyle returns the focus style.
347−func (s *SectionHeaderItem) FocusStyle() lipgloss.Style {
348− return s.sty.Chat.Message.AssistantFocused
351−// BlurStyle returns the blur style.
352−func (s *SectionHeaderItem) BlurStyle() lipgloss.Style {
353− return s.sty.Chat.Message.AssistantBlurred
356−// Render implements list.Item.
357−func (s *SectionHeaderItem) Render(width int) string {
358− content := fmt.Sprintf("%s %s %s",
359− s.sty.Subtle.Render(styles.ModelIcon),
360− s.sty.Muted.Render(s.modelName),
361− s.sty.Subtle.Render(s.duration.String()),
364− return s.sty.Chat.Message.SectionHeader.Render(content)
367−// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
368−// all parts of the message as [MessageItem]s.
370−// For assistant messages with tool calls, pass a toolResults map to link results.
371−// Use BuildToolResultMap to create this map from all messages in a session.
372−func GetMessageItems(sty *styles.Styles, msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
373− var items []MessageItem
375− // Skip tool result messages - they're displayed inline with tool calls
376− if msg.Role == message.Tool {
380− // Process user messages
381− if msg.Role == message.User {
382− // Add main text content
383− content := msg.Content().String()
385− item := NewMessageContentItem(
386− fmt.Sprintf("%s-content", msg.ID),
389− true, // User messages are markdown
392− items = append(items, item)
396− for i, attachment := range msg.BinaryContent() {
397− filename := filepath.Base(attachment.Path)
398− item := NewAttachmentItem(
399− fmt.Sprintf("%s-attachment-%d", msg.ID, i),
404− items = append(items, item)
410− // Process assistant messages
411− if msg.Role == message.Assistant {
412− // Check if we need to add a section header
413− finishData := msg.FinishPart()
414− if finishData != nil && msg.Model != "" {
415− model := config.Get().GetModel(msg.Provider, msg.Model)
416− modelName := "Unknown Model"
418− modelName = model.Name
421− // Calculate duration (this would need the last user message time)
422− duration := time.Duration(0)
423− if finishData.Time > 0 {
424− duration = time.Duration(finishData.Time-msg.CreatedAt) * time.Second
427− header := NewSectionHeaderItem(
428− fmt.Sprintf("%s-header", msg.ID),
433− items = append(items, header)
436− // Add thinking content if present
437− reasoning := msg.ReasoningContent()
438− if strings.TrimSpace(reasoning.Thinking) != "" {
439− duration := time.Duration(0)
440− if reasoning.StartedAt > 0 && reasoning.FinishedAt > 0 {
441− duration = time.Duration(reasoning.FinishedAt-reasoning.StartedAt) * time.Second
444− item := NewThinkingItem(
445− fmt.Sprintf("%s-thinking", msg.ID),
448− reasoning.FinishedAt > 0,
451− items = append(items, item)
454− // Add main text content
455− content := msg.Content().String()
456− finished := msg.IsFinished()
458− // Handle special finish states
459− if finished && content == "" && finishData != nil {
460− switch finishData.Reason {
461− case message.FinishReasonEndTurn:
462− // No content to show
463− case message.FinishReasonCanceled:
464− item := NewMessageContentItem(
465− fmt.Sprintf("%s-content", msg.ID),
471− items = append(items, item)
472− case message.FinishReasonError:
474− errTag := sty.Chat.Message.ErrorTag.Render("ERROR")
475− truncated := ansi.Truncate(finishData.Message, 100, "...")
476− title := fmt.Sprintf("%s %s", errTag, sty.Chat.Message.ErrorTitle.Render(truncated))
477− details := sty.Chat.Message.ErrorDetails.Render(finishData.Details)
478− errorContent := fmt.Sprintf("%s\n\n%s", title, details)
480− item := NewMessageContentItem(
481− fmt.Sprintf("%s-error", msg.ID),
487− items = append(items, item)
489− } else if content != "" {
490− item := NewMessageContentItem(
491− fmt.Sprintf("%s-content", msg.ID),
494− true, // Assistant messages are markdown
497− items = append(items, item)
501− toolCalls := msg.ToolCalls()
503− // Use passed-in tool results map (if nil, use empty map)
504− resultMap := toolResults
505− if resultMap == nil {
506− resultMap = make(map[string]message.ToolResult)
509− for _, tc := range toolCalls {
510− result, hasResult := resultMap[tc.ID]
512− result = message.ToolResult{}
515− item := NewToolCallItem(
516− fmt.Sprintf("%s-tool-%s", msg.ID, tc.ID),
519− false, // cancelled state would need to be tracked separately
520− false, // nested state would be detected from tool results
524− items = append(items, item)
533−// BuildToolResultMap creates a map of tool call IDs to their results from a list of messages.
534−// Tool result messages (role == message.Tool) contain the results that should be linked
535−// to tool calls in assistant messages.
536−func BuildToolResultMap(messages []*message.Message) map[string]message.ToolResult {
537− resultMap := make(map[string]message.ToolResult)
538− for _, msg := range messages {
539− if msg.Role == message.Tool {
540− for _, result := range msg.ToolResults() {
541− if result.ToolCallID != "" {
542− resultMap[result.ToolCallID] = result