@@ -25,11 +25,12 @@ import (
25// MessageCmp defines the interface for message components in the chat interface.
25// MessageCmp defines the interface for message components in the chat interface.
26// It combines standard UI model interfaces with message-specific functionality.
26// It combines standard UI model interfaces with message-specific functionality.
27type MessageCmp interface {
27type MessageCmp interface {
28− util.Model // Basic Bubble Tea model interface
28+ util.Model // Basic Bubble Tea model interface
29− layout.Sizeable // Width/height management
29+ layout.Sizeable // Width/height management
30− layout.Focusable // Focus state management
30+ layout.Focusable // Focus state management
31− GetMessage() message.Message // Access to underlying message data
31+ GetMessage() message.Message // Access to underlying message data
32+ SetMessage(msg message.Message) // Update the message content
32− Spinning() bool // Animation state for loading messages
33+ Spinning() bool // Animation state for loading messages
35// messageCmp implements the MessageCmp interface for displaying chat messages.
36// messageCmp implements the MessageCmp interface for displaying chat messages.
@@ -42,7 +43,7 @@ type messageCmp struct {
42 // Core message data and state
43 // Core message data and state
43 message message.Message // The underlying message content
44 message message.Message // The underlying message content
44 spinning bool // Whether to show loading animation
45 spinning bool // Whether to show loading animation
45− anim util.Model // Animation component for loading states
46+ anim anim.Anim // Animation component for loading states
47 // Thinking viewport for displaying reasoning content
48 // Thinking viewport for displaying reasoning content
48 thinkingViewport viewport.Model
49 thinkingViewport viewport.Model
@@ -88,7 +89,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
88 m.spinning = m.shouldSpin()
89 m.spinning = m.shouldSpin()
89 if m.spinning {
90 if m.spinning {
90 u, cmd := m.anim.Update(msg)
91 u, cmd := m.anim.Update(msg)
91− m.anim = u.(util.Model)
92+ m.anim = u.(anim.Anim)
92 return m, cmd
93 return m, cmd
@@ -98,7 +99,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
98// View renders the message component based on its current state.
99// View renders the message component based on its current state.
99// Returns different views for spinning, user, and assistant messages.
100// Returns different views for spinning, user, and assistant messages.
100func (m *messageCmp) View() string {
101func (m *messageCmp) View() string {
101− if m.spinning {
102+ if m.spinning && m.message.ReasoningContent().Thinking == "" {
102 return m.style().PaddingLeft(1).Render(m.anim.View())
103 return m.style().PaddingLeft(1).Render(m.anim.View())
104 if m.message.ID != "" {
105 if m.message.ID != "" {
@@ -118,6 +119,10 @@ func (m *messageCmp) GetMessage() message.Message {
118 return m.message
119 return m.message
122+func (m *messageCmp) SetMessage(msg message.Message) {
121// textWidth calculates the available width for text content,
126// textWidth calculates the available width for text content,
122// accounting for borders and padding
127// accounting for borders and padding
123func (m *messageCmp) textWidth() int {
128func (m *messageCmp) textWidth() int {
@@ -158,6 +163,7 @@ func (m *messageCmp) renderAssistantMessage() string {
158 thinkingContent := ""
163 thinkingContent := ""
160 if thinking || m.message.ReasoningContent().Thinking != "" {
165 if thinking || m.message.ReasoningContent().Thinking != "" {
166+ m.anim.SetLabel("Thinking")
161 thinkingContent = m.renderThinkingContent()
167 thinkingContent = m.renderThinkingContent()
162 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
168 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
163 content = ""
169 content = ""
@@ -230,7 +236,7 @@ func (m *messageCmp) renderThinkingContent() string {
231 lines := strings.Split(reasoningContent.Thinking, "\n")
237 lines := strings.Split(reasoningContent.Thinking, "\n")
232 var content strings.Builder
238 var content strings.Builder
233− lineStyle := t.S().Muted.Background(t.BgBaseLighter)
239+ lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
234 for _, line := range lines {
240 for _, line := range lines {
235 if line == "" {
241 if line == "" {
@@ -246,15 +252,18 @@ func (m *messageCmp) renderThinkingContent() string {
246 var footer string
252 var footer string
247 if reasoningContent.StartedAt > 0 {
253 if reasoningContent.StartedAt > 0 {
248 duration := m.message.ThinkingDuration()
254 duration := m.message.ThinkingDuration()
249− opts := core.StatusOpts{
250− Title: "Thinking...",
251− Description: duration.String(),
253 if reasoningContent.FinishedAt > 0 {
255 if reasoningContent.FinishedAt > 0 {
254− opts.NoIcon = true
256+ m.anim.SetLabel("")
257+ opts := core.StatusOpts{
255− opts.Title = "Thought for"
258+ Title: "Thought for",
259+ Description: duration.String(),
262+ footer = t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
264+ footer = m.anim.View()
257− footer = t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
266+
259 return lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View()) + "\n\n" + footer
268 return lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View()) + "\n\n" + footer
@@ -273,11 +282,12 @@ func (m *messageCmp) shouldSpin() bool {
273 if m.message.Content().Text != "" {
282 if m.message.Content().Text != "" {
274 return false
283 return false
285+ if len(m.message.ToolCalls()) > 0 {
276 return true
288 return true
279−// Focus management methods
281// Blur removes focus from the message component
291// Blur removes focus from the message component
282func (m *messageCmp) Blur() tea.Cmd {
292func (m *messageCmp) Blur() tea.Cmd {
283 m.focused = false
293 m.focused = false