@@ -2,6 +2,8 @@ package model
5 "path/filepath"
7 "path/filepath"
@@ -14,7 +16,6 @@ import (
14 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/ui/common"
17 "github.com/charmbracelet/crush/internal/ui/common"
16 "github.com/charmbracelet/crush/internal/ui/lazylist"
18 "github.com/charmbracelet/crush/internal/ui/lazylist"
17− "github.com/charmbracelet/crush/internal/ui/list"
18 "github.com/charmbracelet/crush/internal/ui/styles"
19 "github.com/charmbracelet/crush/internal/ui/styles"
19 "github.com/charmbracelet/crush/internal/ui/toolrender"
20 "github.com/charmbracelet/crush/internal/ui/toolrender"
@@ -25,38 +26,33 @@ type Identifiable interface {
27// MessageItem represents a [message.Message] item that can be displayed in the
28// MessageItem represents a [message.Message] item that can be displayed in the
28−// UI and be part of a [list.List] identifiable by a unique ID.
29+// UI and be part of a [lazylist.List] identifiable by a unique ID.
29type MessageItem interface {
30type MessageItem interface {
30− list.Item
31+ lazylist.Item
33 lazylist.Item
32 lazylist.Item
34 Identifiable
33 Identifiable
37// MessageContentItem represents rendered message content (text, markdown, errors, etc).
36// MessageContentItem represents rendered message content (text, markdown, errors, etc).
38type MessageContentItem struct {
37type MessageContentItem struct {
40− list.BaseHighlightable
42 content string
39 content string
40+ role message.MessageRole
43 isMarkdown bool
41 isMarkdown bool
44 maxWidth int
42 maxWidth int
45− cache map[int]string // Cache for rendered content at different widths
46 sty *styles.Styles
43 sty *styles.Styles
49// NewMessageContentItem creates a new message content item.
46// NewMessageContentItem creates a new message content item.
50−func NewMessageContentItem(id, content string, isMarkdown bool, sty *styles.Styles) *MessageContentItem {
47+func NewMessageContentItem(id, content string, role message.MessageRole, isMarkdown bool, sty *styles.Styles) *MessageContentItem {
51 m := &MessageContentItem{
48 m := &MessageContentItem{
53 content: content,
50 content: content,
54 isMarkdown: isMarkdown,
51 isMarkdown: isMarkdown,
55 maxWidth: 120,
53 maxWidth: 120,
56− cache: make(map[int]string),
@@ -65,76 +61,38 @@ func (m *MessageContentItem) ID() string {
65 return m.id
61 return m.id
68−// Height implements list.Item.
64+// FocusStyle returns the focus style.
69−func (m *MessageContentItem) Height(width int) int {
65+func (m *MessageContentItem) FocusStyle() lipgloss.Style {
70− // Calculate content width accounting for frame size
66+ if m.role == message.User {
71− contentWidth := width
67+ return m.sty.Chat.Message.UserFocused
72− if style := m.CurrentStyle(); style != nil {
73− contentWidth -= style.GetHorizontalFrameSize()
76− rendered := m.render(contentWidth)
78− // Apply focus/blur styling if configured to get accurate height
79− if style := m.CurrentStyle(); style != nil {
80− rendered = style.Render(rendered)
82−
69+ return m.sty.Chat.Message.AssistantFocused
83− return strings.Count(rendered, "\n") + 1
86−// Draw implements list.Item.
72+// BlurStyle returns the blur style.
87−func (m *MessageContentItem) Draw(scr uv.Screen, area uv.Rectangle) {
73+func (m *MessageContentItem) BlurStyle() lipgloss.Style {
88− width := area.Dx()
74+ if m.role == message.User {
89− height := area.Dy()
75+ return m.sty.Chat.Message.UserBlurred
91− // Calculate content width accounting for frame size
92− contentWidth := width
93− style := m.CurrentStyle()
95− contentWidth -= style.GetHorizontalFrameSize()
98− rendered := m.render(contentWidth)
100− // Apply focus/blur styling if configured
102− rendered = style.Render(rendered)
104−
77+ return m.sty.Chat.Message.AssistantBlurred
105− // Create temp buffer to draw content with highlighting
106− tempBuf := uv.NewScreenBuffer(width, height)
108− // Draw the rendered content to temp buffer
109− styled := uv.NewStyledString(rendered)
110− styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
112− // Apply highlighting if active
113− m.ApplyHighlight(&tempBuf, width, height, style)
115− // Copy temp buffer to actual screen at the target area
116− tempBuf.Draw(scr, area)
119−// Render implements lazylist.Item.
80+// HighlightStyle returns the highlight style.
120−func (m *MessageContentItem) Render(width int) string {
81+func (m *MessageContentItem) HighlightStyle() lipgloss.Style {
121− return m.render(width)
82+ return m.sty.TextSelection
124−// render renders the content at the given width, using cache if available.
85+// Render renders the content at the given width, using cache if available.
87+// It implements [lazylist.Item].
125−func (m *MessageContentItem) render(width int) string {
88+func (m *MessageContentItem) Render(width int) string {
89+ contentWidth := width
126 // Cap width to maxWidth for markdown
90 // Cap width to maxWidth for markdown
127− cappedWidth := width
91+ cappedWidth := contentWidth
128 if m.isMarkdown {
92 if m.isMarkdown {
129− cappedWidth = min(width, m.maxWidth)
93+ cappedWidth = min(contentWidth, m.maxWidth)
132− // Check cache first
133− if cached, ok := m.cache[cappedWidth]; ok {
137− // Not cached - render now
138 var rendered string
96 var rendered string
139 if m.isMarkdown {
97 if m.isMarkdown {
140 renderer := common.MarkdownRenderer(m.sty, cappedWidth)
98 renderer := common.MarkdownRenderer(m.sty, cappedWidth)
@@ -148,30 +106,19 @@ func (m *MessageContentItem) render(width int) string {
148 rendered = m.content
106 rendered = m.content
152− m.cache[cappedWidth] = rendered
153 return rendered
109 return rendered
156−// SetHighlight implements list.Highlightable and extends BaseHighlightable.
157−func (m *MessageContentItem) SetHighlight(startLine, startCol, endLine, endCol int) {
158− m.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
159− // Clear cache when highlight changes
160− m.cache = make(map[int]string)
163// ToolCallItem represents a rendered tool call with its header and content.
112// ToolCallItem represents a rendered tool call with its header and content.
164type ToolCallItem struct {
113type ToolCallItem struct {
165− list.BaseFocusable
114+ BaseFocusable
166− list.BaseHighlightable
115+ BaseHighlightable
167 id string
116 id string
168 toolCall message.ToolCall
117 toolCall message.ToolCall
169 toolResult message.ToolResult
118 toolResult message.ToolResult
170 cancelled bool
119 cancelled bool
171 isNested bool
120 isNested bool
172 maxWidth int
121 maxWidth int
173− cache map[int]cachedToolRender // Cache for rendered content at different widths
174− cacheKey string // Key to invalidate cache when content changes
175 sty *styles.Styles
122 sty *styles.Styles
@@ -190,8 +137,6 @@ func NewToolCallItem(id string, toolCall message.ToolCall, toolResult message.To
190 cancelled: cancelled,
137 cancelled: cancelled,
191 isNested: isNested,
138 isNested: isNested,
192 maxWidth: 120,
139 maxWidth: 120,
193− cache: make(map[int]cachedToolRender),
194− cacheKey: generateCacheKey(toolCall, toolResult, cancelled),
195 sty: sty,
140 sty: sty,
197 t.InitHighlight()
142 t.InitHighlight()
@@ -209,116 +154,59 @@ func (t *ToolCallItem) ID() string {
209 return t.id
154 return t.id
212−// Height implements list.Item.
157+// FocusStyle returns the focus style.
213−func (t *ToolCallItem) Height(width int) int {
158+func (t *ToolCallItem) FocusStyle() lipgloss.Style {
214− // Calculate content width accounting for frame size
159+ if t.focusStyle != nil {
215− contentWidth := width
160+ return *t.focusStyle
217− if style := t.CurrentStyle(); style != nil {
218− frameSize = style.GetHorizontalFrameSize()
219− contentWidth -= frameSize
222− cached := t.renderCached(contentWidth)
224− // Add frame size to height if needed
225− height := cached.height
227− // Frame can add to height (borders, padding)
228− if style := t.CurrentStyle(); style != nil {
229− // Quick render to get accurate height with frame
230− rendered := style.Render(cached.content)
231− height = strings.Count(rendered, "\n") + 1
234−
162+ return lipgloss.Style{}
238−// Render implements lazylist.Item.
165+// BlurStyle returns the blur style.
239−func (t *ToolCallItem) Render(width int) string {
166+func (t *ToolCallItem) BlurStyle() lipgloss.Style {
240− cached := t.renderCached(width)
241− return cached.content
244−// Draw implements list.Item.
245−func (t *ToolCallItem) Draw(scr uv.Screen, area uv.Rectangle) {
249− // Calculate content width accounting for frame size
250− contentWidth := width
251− style := t.CurrentStyle()
252− if style != nil {
167+ if t.blurStyle != nil {
253− contentWidth -= style.GetHorizontalFrameSize()
168+ return *t.blurStyle
255−
170+ return lipgloss.Style{}
256− cached := t.renderCached(contentWidth)
257− rendered := cached.content
260− rendered = style.Render(rendered)
263− tempBuf := uv.NewScreenBuffer(width, height)
264− styled := uv.NewStyledString(rendered)
265− styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
267− t.ApplyHighlight(&tempBuf, width, height, style)
268− tempBuf.Draw(scr, area)
271−// renderCached renders the tool call at the given width with caching.
173+// HighlightStyle returns the highlight style.
272−func (t *ToolCallItem) renderCached(width int) cachedToolRender {
174+func (t *ToolCallItem) HighlightStyle() lipgloss.Style {
273− cappedWidth := min(width, t.maxWidth)
175+ return t.sty.TextSelection
275− // Check if we have a valid cache entry
276− if cached, ok := t.cache[cappedWidth]; ok {
178+// Render implements lazylist.Item.
179+func (t *ToolCallItem) Render(width int) string {
280 // Render the tool call
180 // Render the tool call
281 ctx := &toolrender.RenderContext{
181 ctx := &toolrender.RenderContext{
282 Call: t.toolCall,
182 Call: t.toolCall,
283 Result: t.toolResult,
183 Result: t.toolResult,
284 Cancelled: t.cancelled,
184 Cancelled: t.cancelled,
285 IsNested: t.isNested,
185 IsNested: t.isNested,
286− Width: cappedWidth,
186+ Width: width,
287 Styles: t.sty,
187 Styles: t.sty,
290 rendered := toolrender.Render(ctx)
190 rendered := toolrender.Render(ctx)
291− height := strings.Count(rendered, "\n") + 1
191+ return rendered
293− cached := cachedToolRender{
193+ // return t.RenderWithHighlight(rendered, width, style)
297− t.cache[cappedWidth] = cached
301// SetHighlight implements list.Highlightable.
196// SetHighlight implements list.Highlightable.
302func (t *ToolCallItem) SetHighlight(startLine, startCol, endLine, endCol int) {
197func (t *ToolCallItem) SetHighlight(startLine, startCol, endLine, endCol int) {
303 t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
198 t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
304− // Clear cache when highlight changes
305− t.cache = make(map[int]cachedToolRender)
308// UpdateResult updates the tool result and invalidates the cache if needed.
201// UpdateResult updates the tool result and invalidates the cache if needed.
309func (t *ToolCallItem) UpdateResult(result message.ToolResult) {
202func (t *ToolCallItem) UpdateResult(result message.ToolResult) {
310− newKey := generateCacheKey(t.toolCall, result, t.cancelled)
311− if newKey != t.cacheKey {
312− t.toolResult = result
203+ t.toolResult = result
314− t.cache = make(map[int]cachedToolRender)
318// AttachmentItem represents a file attachment in a user message.
206// AttachmentItem represents a file attachment in a user message.
319type AttachmentItem struct {
207type AttachmentItem struct {
320− list.BaseFocusable
208+ BaseFocusable
321− list.BaseHighlightable
209+ BaseHighlightable
322 id string
210 id string
323 filename string
211 filename string
324 path string
212 path string
@@ -342,33 +230,29 @@ func (a *AttachmentItem) ID() string {
342 return a.id
230 return a.id
345−// Height implements list.Item.
233+// FocusStyle returns the focus style.
346−func (a *AttachmentItem) Height(width int) int {
234+func (a *AttachmentItem) FocusStyle() lipgloss.Style {
235+ if a.focusStyle != nil {
347− return 1
236+ return *a.focusStyle
238+ return lipgloss.Style{}
350−// Render implements lazylist.Item.
241+// BlurStyle returns the blur style.
351−func (a *AttachmentItem) Render(width int) string {
242+func (a *AttachmentItem) BlurStyle() lipgloss.Style {
352− const maxFilenameWidth = 10
243+ if a.blurStyle != nil {
353− return a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
244+ return *a.blurStyle
355− styles.DocumentIcon,
246+ return lipgloss.Style{}
356− ansi.Truncate(a.filename, maxFilenameWidth, "..."),
360−// Draw implements list.Item.
249+// HighlightStyle returns the highlight style.
361−func (a *AttachmentItem) Draw(scr uv.Screen, area uv.Rectangle) {
250+func (a *AttachmentItem) HighlightStyle() lipgloss.Style {
362− width := area.Dx()
251+ return a.sty.TextSelection
365− // Calculate content width accounting for frame size
366− contentWidth := width
367− style := a.CurrentStyle()
369− contentWidth -= style.GetHorizontalFrameSize()
254+// Render implements lazylist.Item.
255+func (a *AttachmentItem) Render(width int) string {
372 const maxFilenameWidth = 10
256 const maxFilenameWidth = 10
373 content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
257 content := a.sty.Chat.Message.Attachment.Render(fmt.Sprintf(
374 " %s %s ",
258 " %s %s ",
@@ -376,28 +260,18 @@ func (a *AttachmentItem) Draw(scr uv.Screen, area uv.Rectangle) {
376 ansi.Truncate(a.filename, maxFilenameWidth, "..."),
260 ansi.Truncate(a.filename, maxFilenameWidth, "..."),
379− if style != nil {
263+ return content
380− content = style.Render(content)
383− tempBuf := uv.NewScreenBuffer(width, height)
265+ // return a.RenderWithHighlight(content, width, a.CurrentStyle())
384− styled := uv.NewStyledString(content)
385− styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
387− a.ApplyHighlight(&tempBuf, width, height, style)
388− tempBuf.Draw(scr, area)
391// ThinkingItem represents thinking/reasoning content in assistant messages.
268// ThinkingItem represents thinking/reasoning content in assistant messages.
392type ThinkingItem struct {
269type ThinkingItem struct {
394− list.BaseHighlightable
395 id string
270 id string
396 thinking string
271 thinking string
397 duration time.Duration
272 duration time.Duration
398 finished bool
273 finished bool
399 maxWidth int
274 maxWidth int
400− cache map[int]string
401 sty *styles.Styles
275 sty *styles.Styles
@@ -409,10 +283,8 @@ func NewThinkingItem(id, thinking string, duration time.Duration, finished bool,
409 duration: duration,
283 duration: duration,
410 finished: finished,
284 finished: finished,
411 maxWidth: 120,
285 maxWidth: 120,
412− cache: make(map[int]string),
413 sty: sty,
286 sty: sty,
@@ -421,57 +293,25 @@ func (t *ThinkingItem) ID() string {
421 return t.id
293 return t.id
424−// Height implements list.Item.
296+// FocusStyle returns the focus style.
425−func (t *ThinkingItem) Height(width int) int {
297+func (t *ThinkingItem) FocusStyle() lipgloss.Style {
426− // Calculate content width accounting for frame size
298+ return t.sty.Chat.Message.AssistantFocused
427− contentWidth := width
428− if style := t.CurrentStyle(); style != nil {
429− contentWidth -= style.GetHorizontalFrameSize()
432− rendered := t.render(contentWidth)
433− return strings.Count(rendered, "\n") + 1
436−// Render implements lazylist.Item.
301+// BlurStyle returns the blur style.
437−func (t *ThinkingItem) Render(width int) string {
302+func (t *ThinkingItem) BlurStyle() lipgloss.Style {
438− return t.render(width)
303+ return t.sty.Chat.Message.AssistantBlurred
441−// Draw implements list.Item.
306+// HighlightStyle returns the highlight style.
442−func (t *ThinkingItem) Draw(scr uv.Screen, area uv.Rectangle) {
307+func (t *ThinkingItem) HighlightStyle() lipgloss.Style {
443− width := area.Dx()
308+ return t.sty.TextSelection
446− // Calculate content width accounting for frame size
447− contentWidth := width
448− style := t.CurrentStyle()
450− contentWidth -= style.GetHorizontalFrameSize()
453− rendered := t.render(contentWidth)
456− rendered = style.Render(rendered)
459− tempBuf := uv.NewScreenBuffer(width, height)
460− styled := uv.NewStyledString(rendered)
461− styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
463− t.ApplyHighlight(&tempBuf, width, height, style)
464− tempBuf.Draw(scr, area)
467−// render renders the thinking content.
311+// Render implements lazylist.Item.
468−func (t *ThinkingItem) render(width int) string {
312+func (t *ThinkingItem) Render(width int) string {
469 cappedWidth := min(width, t.maxWidth)
313 cappedWidth := min(width, t.maxWidth)
471− if cached, ok := t.cache[cappedWidth]; ok {
475 renderer := common.PlainMarkdownRenderer(cappedWidth - 1)
315 renderer := common.PlainMarkdownRenderer(cappedWidth - 1)
476 rendered, err := renderer.Render(t.thinking)
316 rendered, err := renderer.Render(t.thinking)
477 if err != nil {
317 if err != nil {
@@ -501,20 +341,11 @@ func (t *ThinkingItem) render(width int) string {
502 result := t.sty.PanelMuted.Width(cappedWidth).Padding(0, 1).Render(fullContent)
342 result := t.sty.PanelMuted.Width(cappedWidth).Padding(0, 1).Render(fullContent)
504− t.cache[cappedWidth] = result
505 return result
344 return result
508−// SetHighlight implements list.Highlightable.
509−func (t *ThinkingItem) SetHighlight(startLine, startCol, endLine, endCol int) {
510− t.BaseHighlightable.SetHighlight(startLine, startCol, endLine, endCol)
511− t.cache = make(map[int]string)
514// SectionHeaderItem represents a section header (e.g., assistant info).
347// SectionHeaderItem represents a section header (e.g., assistant info).
515type SectionHeaderItem struct {
348type SectionHeaderItem struct {
517− list.BaseHighlightable
518 id string
349 id string
519 modelName string
350 modelName string
520 duration time.Duration
351 duration time.Duration
@@ -531,7 +362,6 @@ func NewSectionHeaderItem(id, modelName string, duration time.Duration, sty *sty
531 isSectionHeader: true,
362 isSectionHeader: true,
532 sty: sty,
363 sty: sty,
@@ -545,49 +375,25 @@ func (s *SectionHeaderItem) IsSectionHeader() bool {
545 return s.isSectionHeader
375 return s.isSectionHeader
548−// Height implements list.Item.
378+// FocusStyle returns the focus style.
549−func (s *SectionHeaderItem) Height(width int) int {
379+func (s *SectionHeaderItem) FocusStyle() lipgloss.Style {
550− return 1
380+ return s.sty.Chat.Message.AssistantFocused
383+// BlurStyle returns the blur style.
384+func (s *SectionHeaderItem) BlurStyle() lipgloss.Style {
385+ return s.sty.Chat.Message.AssistantBlurred
553// Render implements lazylist.Item.
388// Render implements lazylist.Item.
554func (s *SectionHeaderItem) Render(width int) string {
389func (s *SectionHeaderItem) Render(width int) string {
555− return s.sty.Chat.Message.SectionHeader.Render(fmt.Sprintf("%s %s %s",
390+ content := s.sty.Chat.Message.SectionHeader.Render(fmt.Sprintf("%s %s %s",
556 s.sty.Subtle.Render(styles.ModelIcon),
391 s.sty.Subtle.Render(styles.ModelIcon),
557 s.sty.Muted.Render(s.modelName),
392 s.sty.Muted.Render(s.modelName),
558 s.sty.Subtle.Render(s.duration.String()),
393 s.sty.Subtle.Render(s.duration.String()),
562−// Draw implements list.Item.
563−func (s *SectionHeaderItem) Draw(scr uv.Screen, area uv.Rectangle) {
567− // Calculate content width accounting for frame size
568− contentWidth := width
569− style := s.CurrentStyle()
571− contentWidth -= style.GetHorizontalFrameSize()
574− infoMsg := s.sty.Subtle.Render(s.duration.String())
575− icon := s.sty.Subtle.Render(styles.ModelIcon)
576− modelFormatted := s.sty.Muted.Render(s.modelName)
577− content := fmt.Sprintf("%s %s %s", icon, modelFormatted, infoMsg)
579− content = s.sty.Chat.Message.SectionHeader.Render(content)
582− content = style.Render(content)
585− tempBuf := uv.NewScreenBuffer(width, height)
396+ return content
586− styled := uv.NewStyledString(content)
587− styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
589− s.ApplyHighlight(&tempBuf, width, height, style)
590− tempBuf.Draw(scr, area)
593// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
399// GetMessageItems extracts [MessageItem]s from a [message.Message]. It returns
@@ -595,8 +401,7 @@ func (s *SectionHeaderItem) Draw(scr uv.Screen, area uv.Rectangle) {
596// For assistant messages with tool calls, pass a toolResults map to link results.
402// For assistant messages with tool calls, pass a toolResults map to link results.
597// Use BuildToolResultMap to create this map from all messages in a session.
403// Use BuildToolResultMap to create this map from all messages in a session.
598−func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
404+func GetMessageItems(sty *styles.Styles, msg *message.Message, toolResults map[string]message.ToolResult) []MessageItem {
599− sty := styles.DefaultStyles()
600 var items []MessageItem
405 var items []MessageItem
602 // Skip tool result messages - they're displayed inline with tool calls
407 // Skip tool result messages - they're displayed inline with tool calls
@@ -622,10 +427,10 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
622 item := NewMessageContentItem(
427 item := NewMessageContentItem(
623 fmt.Sprintf("%s-content", msg.ID),
428 fmt.Sprintf("%s-content", msg.ID),
625 true, // User messages are markdown
431 true, // User messages are markdown
628− item.SetFocusStyles(&focusStyle, &blurStyle)
629 items = append(items, item)
434 items = append(items, item)
@@ -636,8 +441,9 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
636 fmt.Sprintf("%s-attachment-%d", msg.ID, i),
441 fmt.Sprintf("%s-attachment-%d", msg.ID, i),
637 filename,
442 filename,
638 attachment.Path,
443 attachment.Path,
446+ item.SetHighlightStyle(ToStyler(sty.TextSelection))
641 item.SetFocusStyles(&focusStyle, &blurStyle)
447 item.SetFocusStyles(&focusStyle, &blurStyle)
642 items = append(items, item)
448 items = append(items, item)
@@ -666,7 +472,7 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
666 fmt.Sprintf("%s-header", msg.ID),
472 fmt.Sprintf("%s-header", msg.ID),
667 modelName,
473 modelName,
668 duration,
474 duration,
671 items = append(items, header)
477 items = append(items, header)
@@ -684,9 +490,8 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
684 reasoning.Thinking,
490 reasoning.Thinking,
685 duration,
491 duration,
686 reasoning.FinishedAt > 0,
492 reasoning.FinishedAt > 0,
689− item.SetFocusStyles(&focusStyle, &blurStyle)
690 items = append(items, item)
495 items = append(items, item)
@@ -703,10 +508,10 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
703 item := NewMessageContentItem(
508 item := NewMessageContentItem(
704 fmt.Sprintf("%s-content", msg.ID),
509 fmt.Sprintf("%s-content", msg.ID),
705 "*Canceled*",
510 "*Canceled*",
709− item.SetFocusStyles(&focusStyle, &blurStyle)
710 items = append(items, item)
515 items = append(items, item)
711 case message.FinishReasonError:
516 case message.FinishReasonError:
712 // Render error
517 // Render error
@@ -719,20 +524,20 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
719 item := NewMessageContentItem(
524 item := NewMessageContentItem(
720 fmt.Sprintf("%s-error", msg.ID),
525 fmt.Sprintf("%s-error", msg.ID),
721 errorContent,
526 errorContent,
725− item.SetFocusStyles(&focusStyle, &blurStyle)
726 items = append(items, item)
531 items = append(items, item)
728 } else if content != "" {
533 } else if content != "" {
729 item := NewMessageContentItem(
534 item := NewMessageContentItem(
730 fmt.Sprintf("%s-content", msg.ID),
535 fmt.Sprintf("%s-content", msg.ID),
732 true, // Assistant messages are markdown
538 true, // Assistant messages are markdown
735− item.SetFocusStyles(&focusStyle, &blurStyle)
736 items = append(items, item)
541 items = append(items, item)
@@ -757,9 +562,11 @@ func GetMessageItems(msg *message.Message, toolResults map[string]message.ToolRe
758 false, // cancelled state would need to be tracked separately
563 false, // cancelled state would need to be tracked separately
759 false, // nested state would be detected from tool results
564 false, // nested state would be detected from tool results
568+ item.SetHighlightStyle(ToStyler(sty.TextSelection))
763 // Tool calls use muted style with optional focus border
570 // Tool calls use muted style with optional focus border
764 item.SetFocusStyles(&sty.Chat.Message.ToolCallFocused, &sty.Chat.Message.ToolCallBlurred)
571 item.SetFocusStyles(&sty.Chat.Message.ToolCallFocused, &sty.Chat.Message.ToolCallBlurred)
@@ -788,3 +595,299 @@ func BuildToolResultMap(messages []*message.Message) map[string]message.ToolResu
789 return resultMap
596 return resultMap
599+// BaseFocusable provides common focus state and styling for items.
600+// Embed this type to add focus behavior to any item.
601+type BaseFocusable struct {
603+ focusStyle *lipgloss.Style
604+ blurStyle *lipgloss.Style
607+// Focus implements Focusable interface.
608+func (b *BaseFocusable) Focus(width int, content string) string {
609+ if b.focusStyle != nil {
610+ return b.focusStyle.Render(content)
615+// Blur implements Focusable interface.
616+func (b *BaseFocusable) Blur(width int, content string) string {
617+ if b.blurStyle != nil {
618+ return b.blurStyle.Render(content)
623+// Focus implements Focusable interface.
624+// func (b *BaseFocusable) Focus() {
628+// Blur implements Focusable interface.
629+// func (b *BaseFocusable) Blur() {
633+// Focused implements Focusable interface.
634+func (b *BaseFocusable) Focused() bool {
638+// HasFocusStyles returns true if both focus and blur styles are configured.
639+func (b *BaseFocusable) HasFocusStyles() bool {
640+ return b.focusStyle != nil && b.blurStyle != nil
643+// CurrentStyle returns the current style based on focus state.
644+// Returns nil if no styles are configured, or if the current state's style is nil.
645+func (b *BaseFocusable) CurrentStyle() *lipgloss.Style {
652+// SetFocusStyles sets the focus and blur styles.
653+func (b *BaseFocusable) SetFocusStyles(focusStyle, blurStyle *lipgloss.Style) {
654+ b.focusStyle = focusStyle
655+ b.blurStyle = blurStyle
658+// CellStyler defines a function that styles a [uv.Style].
659+type CellStyler func(uv.Style) uv.Style
661+// BaseHighlightable provides common highlight state for items.
662+// Embed this type to add highlight behavior to any item.
663+type BaseHighlightable struct {
664+ highlightStartLine int
665+ highlightStartCol int
666+ highlightEndLine int
668+ highlightStyle CellStyler
671+// SetHighlight implements Highlightable interface.
672+func (b *BaseHighlightable) SetHighlight(startLine, startCol, endLine, endCol int) {
673+ b.highlightStartLine = startLine
674+ b.highlightStartCol = startCol
675+ b.highlightEndLine = endLine
676+ b.highlightEndCol = endCol
679+// GetHighlight implements Highlightable interface.
680+func (b *BaseHighlightable) GetHighlight() (startLine, startCol, endLine, endCol int) {
681+ return b.highlightStartLine, b.highlightStartCol, b.highlightEndLine, b.highlightEndCol
684+// HasHighlight returns true if a highlight region is set.
685+func (b *BaseHighlightable) HasHighlight() bool {
686+ return b.highlightStartLine >= 0 || b.highlightStartCol >= 0 ||
687+ b.highlightEndLine >= 0 || b.highlightEndCol >= 0
690+// SetHighlightStyle sets the style function used for highlighting.
691+func (b *BaseHighlightable) SetHighlightStyle(style CellStyler) {
692+ b.highlightStyle = style
695+// GetHighlightStyle returns the current highlight style function.
696+func (b *BaseHighlightable) GetHighlightStyle() CellStyler {
697+ return b.highlightStyle
700+// InitHighlight initializes the highlight fields with default values.
701+func (b *BaseHighlightable) InitHighlight() {
702+ b.highlightStartLine = -1
703+ b.highlightStartCol = -1
704+ b.highlightEndLine = -1
705+ b.highlightEndCol = -1
706+ b.highlightStyle = ToStyler(lipgloss.NewStyle().Reverse(true))
709+// Highlight implements Highlightable interface.
710+func (b *BaseHighlightable) Highlight(width int, content string, startLine, startCol, endLine, endCol int) string {
711+ b.SetHighlight(startLine, startCol, endLine, endCol)
712+ return b.RenderWithHighlight(content, width, nil)
715+// RenderWithHighlight renders content with optional focus styling and highlighting.
716+// This is a helper that combines common rendering logic for all items.
717+// The content parameter should be the raw rendered content before focus styling.
718+// The style parameter should come from CurrentStyle() and may be nil.
719+func (b *BaseHighlightable) RenderWithHighlight(content string, width int, style *lipgloss.Style) string {
720+ // Apply focus/blur styling if configured
723+ rendered = style.Render(rendered)
726+ if !b.HasHighlight() {
730+ height := lipgloss.Height(rendered)
732+ // Create temp buffer to draw content with highlighting
733+ tempBuf := uv.NewScreenBuffer(width, height)
735+ // Draw the rendered content to temp buffer
736+ styled := uv.NewStyledString(rendered)
737+ styled.Draw(&tempBuf, uv.Rect(0, 0, width, height))
739+ // Apply highlighting if active
740+ b.ApplyHighlight(&tempBuf, width, height, style)
742+ return tempBuf.Render()
745+// ApplyHighlight applies highlighting to a screen buffer.
746+// This should be called after drawing content to the buffer.
747+func (b *BaseHighlightable) ApplyHighlight(buf *uv.ScreenBuffer, width, height int, style *lipgloss.Style) {
748+ if b.highlightStartLine < 0 {
753+ topMargin, topBorder, topPadding int
754+ rightMargin, rightBorder, rightPadding int
755+ bottomMargin, bottomBorder, bottomPadding int
756+ leftMargin, leftBorder, leftPadding int
759+ topMargin, rightMargin, bottomMargin, leftMargin = style.GetMargin()
760+ topBorder, rightBorder, bottomBorder, leftBorder = style.GetBorderTopSize(),
761+ style.GetBorderRightSize(),
762+ style.GetBorderBottomSize(),
763+ style.GetBorderLeftSize()
764+ topPadding, rightPadding, bottomPadding, leftPadding = style.GetPadding()
767+ slog.Info("Applying highlight",
768+ "highlightStartLine", b.highlightStartLine,
769+ "highlightStartCol", b.highlightStartCol,
770+ "highlightEndLine", b.highlightEndLine,
771+ "highlightEndCol", b.highlightEndCol,
774+ "margins", fmt.Sprintf("%d,%d,%d,%d", topMargin, rightMargin, bottomMargin, leftMargin),
775+ "borders", fmt.Sprintf("%d,%d,%d,%d", topBorder, rightBorder, bottomBorder, leftBorder),
776+ "paddings", fmt.Sprintf("%d,%d,%d,%d", topPadding, rightPadding, bottomPadding, leftPadding),
779+ // Calculate content area offsets
780+ contentArea := image.Rectangle{
782+ X: leftMargin + leftBorder + leftPadding,
783+ Y: topMargin + topBorder + topPadding,
786+ X: width - (rightMargin + rightBorder + rightPadding),
787+ Y: height - (bottomMargin + bottomBorder + bottomPadding),
791+ for y := b.highlightStartLine; y <= b.highlightEndLine && y < height; y++ {
792+ if y >= buf.Height() {
798+ // Determine column range for this line
800+ if y == b.highlightStartLine {
801+ startCol = min(b.highlightStartCol, len(line))
805+ if y == b.highlightEndLine {
806+ endCol = min(b.highlightEndCol, len(line))
809+ // Track last non-empty position as we go
812+ // Single pass: check content and track last non-empty position
813+ for x := startCol; x < endCol; x++ {
819+ // Update last content position if non-empty
820+ if cell.Content != "" && cell.Content != " " {
825+ // Only apply highlight up to last content position
826+ highlightEnd := endCol
827+ if lastContentX >= 0 {
828+ highlightEnd = lastContentX + 1
829+ } else if lastContentX == -1 {
830+ highlightEnd = startCol // No content on this line
833+ // Apply highlight style only to cells with content
834+ for x := startCol; x < highlightEnd; x++ {
835+ if !image.Pt(x, y).In(contentArea) {
839+ cell.Style = b.highlightStyle(cell.Style)
844+// ToStyler converts a [lipgloss.Style] to a [CellStyler].
845+func ToStyler(lgStyle lipgloss.Style) CellStyler {
846+ return func(uv.Style) uv.Style {
847+ return ToStyle(lgStyle)
851+// ToStyle converts an inline [lipgloss.Style] to a [uv.Style].
852+func ToStyle(lgStyle lipgloss.Style) uv.Style {
853+ var uvStyle uv.Style
855+ // Colors are already color.Color
856+ uvStyle.Fg = lgStyle.GetForeground()
857+ uvStyle.Bg = lgStyle.GetBackground()
859+ // Build attributes using bitwise OR
862+ if lgStyle.GetBold() {
863+ attrs |= uv.AttrBold
866+ if lgStyle.GetItalic() {
867+ attrs |= uv.AttrItalic
870+ if lgStyle.GetUnderline() {
871+ uvStyle.Underline = uv.UnderlineSingle
874+ if lgStyle.GetStrikethrough() {
875+ attrs |= uv.AttrStrikethrough
878+ if lgStyle.GetFaint() {
879+ attrs |= uv.AttrFaint
882+ if lgStyle.GetBlink() {
883+ attrs |= uv.AttrBlink
886+ if lgStyle.GetReverse() {
887+ attrs |= uv.AttrReverse
890+ uvStyle.Attrs = attrs