on main help sign in
chore: cleanup the UI
commit2ce602bd248f8b6bc332e4197a20ebf72013c607 authorKujtim Hoxha <kujtimii.h@gmail.com> dateWed 16 Jul 2025, 09:50 (1 year ago) parent6c15eaf
3 files changed+77−42
internal/tui/components/anim/anim.go+43−19 internal/tui/components/chat/chat.go+6−5 internal/tui/components/chat/messages/messages.go+28−18
internal/tui/components/anim/anim.go+43−19
@@ -80,6 +80,7 @@ type Anim struct {
8080 cyclingCharWidth int
8181 label []string
8282 labelWidth int
83+ labelColor color.Color
8384 startTime time.Time
8485 birthOffsets []time.Duration
8586 initialFrames [][]string // frames for the initial characters
@@ -112,6 +113,7 @@ func New(opts Settings) (a Anim) {
112113 a.startTime = time.Now()
113114 a.cyclingCharWidth = opts.Size
114115 a.labelWidth = lipgloss.Width(opts.Label)
116+ a.labelColor = opts.LabelColor
115117
116118 // Total width of anim, in cells.
117119 a.width = opts.Size
@@ -119,25 +121,8 @@ func New(opts Settings) (a Anim) {
119121 a.width += labelGapWidth + lipgloss.Width(opts.Label)
120122 }
121123
122− if a.labelWidth > 0 {
123− // Pre-render the label.
124− // XXX: We should really get the graphemes for the label, not the runes.
125− labelRunes := []rune(opts.Label)
126− a.label = make([]string, len(labelRunes))
127− for i := range a.label {
128− a.label[i] = lipgloss.NewStyle().
129− Foreground(opts.LabelColor).
130− Render(string(labelRunes[i]))
131− }
132−
133− // Pre-render the ellipsis frames which come after the label.
134− a.ellipsisFrames = make([]string, len(ellipsisFrames))
135− for i, frame := range ellipsisFrames {
136− a.ellipsisFrames[i] = lipgloss.NewStyle().
137− Foreground(opts.LabelColor).
138− Render(frame)
139− }
140− }
124+ // Render the label
125+ a.renderLabel(opts.Label)
141126
142127 // Pre-generate gradient.
143128 var ramp []color.Color
@@ -208,6 +193,45 @@ func New(opts Settings) (a Anim) {
208193 return a
209194}
210195
196+// SetLabel updates the label text and re-renders it.
197+func (a *Anim) SetLabel(newLabel string) {
198+ a.labelWidth = lipgloss.Width(newLabel)
199+
200+ // Update total width
201+ a.width = a.cyclingCharWidth
202+ if newLabel != "" {
203+ a.width += labelGapWidth + a.labelWidth
204+ }
205+
206+ // Re-render the label
207+ a.renderLabel(newLabel)
208+}
209+
210+// renderLabel renders the label with the current label color.
211+func (a *Anim) renderLabel(label string) {
212+ if a.labelWidth > 0 {
213+ // Pre-render the label.
214+ labelRunes := []rune(label)
215+ a.label = make([]string, len(labelRunes))
216+ for i := range a.label {
217+ a.label[i] = lipgloss.NewStyle().
218+ Foreground(a.labelColor).
219+ Render(string(labelRunes[i]))
220+ }
221+
222+ // Pre-render the ellipsis frames which come after the label.
223+ a.ellipsisFrames = make([]string, len(ellipsisFrames))
224+ for i, frame := range ellipsisFrames {
225+ a.ellipsisFrames[i] = lipgloss.NewStyle().
226+ Foreground(a.labelColor).
227+ Render(frame)
228+ }
229+ } else {
230+ a.label = nil
231+ a.ellipsisFrames = nil
232+ }
233+}
234+
211235// Width returns the total width of the animation.
212236func (a Anim) Width() (w int) {
213237 w = a.width
internal/tui/components/chat/chat.go+6−5
@@ -304,14 +304,15 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
304304 shouldShowMessage := m.shouldShowAssistantMessage(msg)
305305 hasToolCallsOnly := len(msg.ToolCalls()) > 0 && msg.Content().Text == ""
306306
307+ var cmd tea.Cmd
307308 if shouldShowMessage {
309+ items := m.listCmp.Items()
310+ uiMsg := items[assistantIndex].(messages.MessageCmp)
311+ uiMsg.SetMessage(msg)
308312 m.listCmp.UpdateItem(
309313 assistantIndex,
310− messages.NewMessageCmp(
311− msg,
312− ),
314+ uiMsg,
313315 )
314−
315316 if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn {
316317 m.listCmp.AppendItem(
317318 messages.NewAssistantSection(
@@ -324,7 +325,7 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi
324325 m.listCmp.DeleteItem(assistantIndex)
325326 }
326327
327− return nil
328+ return cmd
328329}
329330
330331// shouldShowAssistantMessage determines if an assistant message should be displayed.
internal/tui/components/chat/messages/messages.go+28−18
@@ -25,11 +25,12 @@ import (
2525// MessageCmp defines the interface for message components in the chat interface.
2626// It combines standard UI model interfaces with message-specific functionality.
2727type MessageCmp interface {
28− util.Model // Basic Bubble Tea model interface
29− layout.Sizeable // Width/height management
30− layout.Focusable // Focus state management
31− GetMessage() message.Message // Access to underlying message data
32− Spinning() bool // Animation state for loading messages
28+ util.Model // Basic Bubble Tea model interface
29+ layout.Sizeable // Width/height management
30+ layout.Focusable // Focus state management
31+ GetMessage() message.Message // Access to underlying message data
32+ SetMessage(msg message.Message) // Update the message content
33+ Spinning() bool // Animation state for loading messages
3334}
3435
3536// messageCmp implements the MessageCmp interface for displaying chat messages.
@@ -42,7 +43,7 @@ type messageCmp struct {
4243 // Core message data and state
4344 message message.Message // The underlying message content
4445 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
4647
4748 // Thinking viewport for displaying reasoning content
4849 thinkingViewport viewport.Model
@@ -88,7 +89,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8889 m.spinning = m.shouldSpin()
8990 if m.spinning {
9091 u, cmd := m.anim.Update(msg)
91− m.anim = u.(util.Model)
92+ m.anim = u.(anim.Anim)
9293 return m, cmd
9394 }
9495 }
@@ -98,7 +99,7 @@ func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9899// View renders the message component based on its current state.
99100// Returns different views for spinning, user, and assistant messages.
100101func (m *messageCmp) View() string {
101− if m.spinning {
102+ if m.spinning && m.message.ReasoningContent().Thinking == "" {
102103 return m.style().PaddingLeft(1).Render(m.anim.View())
103104 }
104105 if m.message.ID != "" {
@@ -118,6 +119,10 @@ func (m *messageCmp) GetMessage() message.Message {
118119 return m.message
119120}
120121
122+func (m *messageCmp) SetMessage(msg message.Message) {
123+ m.message = msg
124+}
125+
121126// textWidth calculates the available width for text content,
122127// accounting for borders and padding
123128func (m *messageCmp) textWidth() int {
@@ -158,6 +163,7 @@ func (m *messageCmp) renderAssistantMessage() string {
158163 thinkingContent := ""
159164
160165 if thinking || m.message.ReasoningContent().Thinking != "" {
166+ m.anim.SetLabel("Thinking")
161167 thinkingContent = m.renderThinkingContent()
162168 } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn {
163169 content = ""
@@ -230,7 +236,7 @@ func (m *messageCmp) renderThinkingContent() string {
230236 }
231237 lines := strings.Split(reasoningContent.Thinking, "\n")
232238 var content strings.Builder
233− lineStyle := t.S().Muted.Background(t.BgBaseLighter)
239+ lineStyle := t.S().Subtle.Background(t.BgBaseLighter)
234240 for _, line := range lines {
235241 if line == "" {
236242 continue
@@ -246,15 +252,18 @@ func (m *messageCmp) renderThinkingContent() string {
246252 var footer string
247253 if reasoningContent.StartedAt > 0 {
248254 duration := m.message.ThinkingDuration()
249− opts := core.StatusOpts{
250− Title: "Thinking...",
251− Description: duration.String(),
252− }
253255 if reasoningContent.FinishedAt > 0 {
254− opts.NoIcon = true
255− opts.Title = "Thought for"
256+ m.anim.SetLabel("")
257+ opts := core.StatusOpts{
258+ Title: "Thought for",
259+ Description: duration.String(),
260+ NoIcon: true,
261+ }
262+ footer = t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
263+ } else {
264+ footer = m.anim.View()
256265 }
257− footer = t.S().Base.PaddingLeft(1).Render(core.Status(opts, m.textWidth()-1))
266+
258267 }
259268 return lineStyle.Width(m.textWidth()).Padding(0, 1).Render(m.thinkingViewport.View()) + "\n\n" + footer
260269}
@@ -273,11 +282,12 @@ func (m *messageCmp) shouldSpin() bool {
273282 if m.message.Content().Text != "" {
274283 return false
275284 }
285+ if len(m.message.ToolCalls()) > 0 {
286+ return false
287+ }
276288 return true
277289}
278290
279−// Focus management methods
280−
281291// Blur removes focus from the message component
282292func (m *messageCmp) Blur() tea.Cmd {
283293 m.focused = false