on main help sign in
handle agent tool
9a69a31Kujtim Hoxha
internal/tui/components/anim/anim.go+22−1
@@ -75,6 +75,11 @@ func cycleColors(id string) tea.Cmd {
75 })
75 })
76}
76}
77
77
78+type Animation interface {
79+ util.Model
80+ ID() string
81+}
82+
78// anim is the model that manages the animation that displays while the
83// anim is the model that manages the animation that displays while the
79// output is being generated.
84// output is being generated.
80type anim struct {
85type anim struct {
@@ -88,7 +93,15 @@ type anim struct {
88 id string
93 id string
89}
94}
90
95
96+type animOption func(*anim)
97+
98+func WithId(id string) animOption {
99+ return func(a *anim) {
100+ a.id = id
101+ }
102+}
103+
91−func New(cyclingCharsSize uint, label string) util.Model {
104+func New(cyclingCharsSize uint, label string, opts ...animOption) Animation {
92 // #nosec G115
105 // #nosec G115
93 n := min(int(cyclingCharsSize), maxCyclingChars)
106 n := min(int(cyclingCharsSize), maxCyclingChars)
94
107
@@ -105,6 +118,10 @@ func New(cyclingCharsSize uint, label string) util.Model {
105 id: id.String(),
118 id: id.String(),
106 }
119 }
107
120
121+ for _, opt := range opts {
122+ opt(&c)
123+ }
124+
108 // If we're in truecolor mode (and there are enough cycling characters)
125 // If we're in truecolor mode (and there are enough cycling characters)
109 // color the cycling characters with a gradient ramp.
126 // color the cycling characters with a gradient ramp.
110 const minRampSize = 3
127 const minRampSize = 3
@@ -204,6 +221,10 @@ func (a anim) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
204 }
221 }
205}
222}
206
223
224+func (a anim) ID() string {
225+ return a.id
226+}
227+
207func (a *anim) updateChars(chars *[]cyclingChar) {
228func (a *anim) updateChars(chars *[]cyclingChar) {
208 for i, c := range *chars {
229 for i, c := range *chars {
209 switch c.state(a.start) {
230 switch c.state(a.start) {
internal/tui/components/chat/list.go+66−10
@@ -8,7 +8,7 @@ import (
8 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/opencode-ai/opencode/internal/app"
10 "github.com/opencode-ai/opencode/internal/app"
11− "github.com/opencode-ai/opencode/internal/logging"
11+ "github.com/opencode-ai/opencode/internal/llm/agent"
12 "github.com/opencode-ai/opencode/internal/message"
12 "github.com/opencode-ai/opencode/internal/message"
13 "github.com/opencode-ai/opencode/internal/pubsub"
13 "github.com/opencode-ai/opencode/internal/pubsub"
14 "github.com/opencode-ai/opencode/internal/session"
14 "github.com/opencode-ai/opencode/internal/session"
@@ -106,9 +106,54 @@ func (m *messageListCmp) View() string {
106}
106}
107
107
108// handleChildSession handles messages from child sessions (agent tools).
108// handleChildSession handles messages from child sessions (agent tools).
109−// TODO: update the agent tool message with the changes
110−func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) {
109+func (m *messageListCmp) handleChildSession(event pubsub.Event[message.Message]) tea.Cmd {
111− // Implementation pending
110+ var cmds []tea.Cmd
111+ if len(event.Payload.ToolCalls()) == 0 {
112+ return nil
113+ }
114+ items := m.listCmp.Items()
115+ toolCallInx := NotFound
116+ var toolCall messages.ToolCallCmp
117+ for i := len(items) - 1; i >= 0; i-- {
118+ if msg, ok := items[i].(messages.ToolCallCmp); ok {
119+ if msg.GetToolCall().ID == event.Payload.SessionID {
120+ toolCallInx = i
121+ toolCall = msg
122+ }
123+ }
124+ }
125+ if toolCallInx == NotFound {
126+ return nil
127+ }
128+ nestedToolCalls := toolCall.GetNestedToolCalls()
129+ for _, tc := range event.Payload.ToolCalls() {
130+ found := false
131+ for existingInx, existingTC := range nestedToolCalls {
132+ if existingTC.GetToolCall().ID == tc.ID {
133+ nestedToolCalls[existingInx].SetToolCall(tc)
134+ found = true
135+ break
136+ }
137+ }
138+ if !found {
139+ nestedCall := messages.NewToolCallCmp(
140+ event.Payload.ID,
141+ tc,
142+ messages.WithToolCallNested(true),
143+ )
144+ cmds = append(cmds, nestedCall.Init())
145+ nestedToolCalls = append(
146+ nestedToolCalls,
147+ nestedCall,
148+ )
149+ }
150+ }
151+ toolCall.SetNestedToolCalls(nestedToolCalls)
152+ m.listCmp.UpdateItem(
153+ toolCallInx,
154+ toolCall,
155+ )
156+ return tea.Batch(cmds...)
112}
157}
113
158
114// handleMessageEvent processes different types of message events (created/updated).
159// handleMessageEvent processes different types of message events (created/updated).
@@ -116,16 +161,16 @@ func (m *messageListCmp) handleMessageEvent(event pubsub.Event[message.Message])
116 switch event.Type {
161 switch event.Type {
117 case pubsub.CreatedEvent:
162 case pubsub.CreatedEvent:
118 if event.Payload.SessionID != m.session.ID {
163 if event.Payload.SessionID != m.session.ID {
119− m.handleChildSession(event)
164+ return m.handleChildSession(event)
120− return nil
121 }
165 }
122−
123 if m.messageExists(event.Payload.ID) {
166 if m.messageExists(event.Payload.ID) {
124 return nil
167 return nil
125 }
168 }
126−
127 return m.handleNewMessage(event.Payload)
169 return m.handleNewMessage(event.Payload)
128 case pubsub.UpdatedEvent:
170 case pubsub.UpdatedEvent:
171+ if event.Payload.SessionID != m.session.ID {
172+ return m.handleChildSession(event)
173+ }
129 return m.handleUpdateAssistantMessage(event.Payload)
174 return m.handleUpdateAssistantMessage(event.Payload)
130 }
175 }
131 return nil
176 return nil
@@ -196,8 +241,6 @@ func (m *messageListCmp) handleUpdateAssistantMessage(msg message.Message) tea.C
196 // Find existing assistant message and tool calls for this message
241 // Find existing assistant message and tool calls for this message
197 assistantIndex, existingToolCalls := m.findAssistantMessageAndToolCalls(items, msg.ID)
242 assistantIndex, existingToolCalls := m.findAssistantMessageAndToolCalls(items, msg.ID)
198
243
199− logging.Info("Update Assistant Message", "msg", msg, "assistantMessageInx", assistantIndex, "toolCalls", existingToolCalls)
200−
201 // Handle assistant message content
244 // Handle assistant message content
202 if cmd := m.updateAssistantMessageContent(msg, assistantIndex); cmd != nil {
245 if cmd := m.updateAssistantMessageContent(msg, assistantIndex); cmd != nil {
203 cmds = append(cmds, cmd)
246 cmds = append(cmds, cmd)
@@ -389,6 +432,19 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult
389 for _, tc := range msg.ToolCalls() {
432 for _, tc := range msg.ToolCalls() {
390 options := m.buildToolCallOptions(tc, msg, toolResultMap)
433 options := m.buildToolCallOptions(tc, msg, toolResultMap)
391 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...))
434 uiMessages = append(uiMessages, messages.NewToolCallCmp(msg.ID, tc, options...))
435+ // If this tool call is the agent tool, fetch nested tool calls
436+ if tc.Name == agent.AgentToolName {
437+ nestedMessages, _ := m.app.Messages.List(context.Background(), tc.ID)
438+ nestedUIMessages := m.convertMessagesToUI(nestedMessages, make(map[string]message.ToolResult))
439+ nestedToolCalls := make([]messages.ToolCallCmp, 0, len(nestedUIMessages))
440+ for _, nestedMsg := range nestedUIMessages {
441+ if toolCall, ok := nestedMsg.(messages.ToolCallCmp); ok {
442+ toolCall.SetIsNested(true)
443+ nestedToolCalls = append(nestedToolCalls, toolCall)
444+ }
445+ }
446+ uiMessages[len(uiMessages)-1].(messages.ToolCallCmp).SetNestedToolCalls(nestedToolCalls)
447+ }
392 }
448 }
393
449
394 return uiMessages
450 return uiMessages
internal/tui/components/chat/messages/messages.go+2−1
@@ -7,6 +7,7 @@ import (
7 "strings"
7 "strings"
8 "time"
8 "time"
9
9
10+ "github.com/charmbracelet/bubbles/v2/spinner"
10 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/charmbracelet/lipgloss/v2"
12 "github.com/opencode-ai/opencode/internal/llm/models"
13 "github.com/opencode-ai/opencode/internal/llm/models"
@@ -80,7 +81,7 @@ func (m *messageCmp) Init() tea.Cmd {
80// Manages animation updates for spinning messages and stops animation when appropriate.
81// Manages animation updates for spinning messages and stops animation when appropriate.
81func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
82func (m *messageCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
82 switch msg := msg.(type) {
83 switch msg := msg.(type) {
83− case anim.ColorCycleMsg, anim.StepCharsMsg:
84+ case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
84 m.spinning = m.shouldSpin()
85 m.spinning = m.shouldSpin()
85 if m.spinning {
86 if m.spinning {
86 u, cmd := m.anim.Update(msg)
87 u, cmd := m.anim.Update(msg)
internal/tui/components/chat/messages/renderer.go+54−1
@@ -91,7 +91,14 @@ func (pb *paramBuilder) build() []string {
91
91
92// renderWithParams provides a common rendering pattern for tools with parameters
92// renderWithParams provides a common rendering pattern for tools with parameters
93func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []string, contentRenderer func() string) string {
93func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []string, contentRenderer func() string) string {
94+ width := v.textWidth()
95+ if v.isNested {
96+ width -= 3 // Adjust for nested tool call indentation
97+ }
94− header := makeHeader(toolName, v.textWidth(), args...)
98+ header := makeHeader(toolName, width, args...)
99+ if v.isNested {
100+ return v.style().Render(header)
101+ }
95 if res, done := earlyState(header, v); done {
102 if res, done := earlyState(header, v); done {
96 return res
103 return res
97 }
104 }
@@ -117,6 +124,7 @@ func init() {
117 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
124 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
118 registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
125 registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
119 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
126 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
127+ registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
120}
128}
121
129
122// -----------------------------------------------------------------------------
130// -----------------------------------------------------------------------------
@@ -467,6 +475,51 @@ func (dr diagnosticsRenderer) Render(v *toolCallCmp) string {
467 })
475 })
468}
476}
469
477
478+// -----------------------------------------------------------------------------
479+// Task renderer
480+// -----------------------------------------------------------------------------
481+
482+// agentRenderer handles project-wide diagnostic information
483+type agentRenderer struct {
484+ baseRenderer
485+}
486+
487+// Render displays agent task parameters and result content
488+func (tr agentRenderer) Render(v *toolCallCmp) string {
489+ var params agent.AgentParams
490+ if err := tr.unmarshalParams(v.call.Input, &params); err != nil {
491+ return tr.renderError(v, "Invalid task parameters")
492+ }
493+ prompt := params.Prompt
494+ prompt = strings.ReplaceAll(prompt, "\n", " ")
495+ args := newParamBuilder().addMain(prompt).build()
496+
497+ header := makeHeader("Task", v.textWidth(), args...)
498+ parts := []string{header}
499+ for _, call := range v.nestedToolCalls {
500+ parts = append(parts, call.View())
501+ }
502+
503+ if v.result.ToolCallID == "" {
504+ v.spinning = true
505+ parts = append(parts, v.anim.View())
506+ } else {
507+ v.spinning = false
508+ }
509+
510+ header = lipgloss.JoinVertical(
511+ lipgloss.Left,
512+ parts...,
513+ )
514+
515+ if v.result.ToolCallID == "" {
516+ return header
517+ }
518+
519+ body := renderPlainContent(v, v.result.Content)
520+ return joinHeaderBody(header, body)
521+}
522+
470// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
523// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
471func makeHeader(tool string, width int, params ...string) string {
524func makeHeader(tool string, width int, params ...string) string {
472 prefix := tool + ": "
525 prefix := tool + ": "
internal/tui/components/chat/messages/tool.go+81−8
@@ -3,10 +3,10 @@ package messages
3import (
3import (
4 "fmt"
4 "fmt"
5
5
6+ "github.com/charmbracelet/bubbles/v2/spinner"
6 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/lipgloss/v2"
8 "github.com/charmbracelet/x/ansi"
9 "github.com/charmbracelet/x/ansi"
9− "github.com/opencode-ai/opencode/internal/logging"
10 "github.com/opencode-ai/opencode/internal/message"
10 "github.com/opencode-ai/opencode/internal/message"
11 "github.com/opencode-ai/opencode/internal/tui/components/anim"
11 "github.com/opencode-ai/opencode/internal/tui/components/anim"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
12 "github.com/opencode-ai/opencode/internal/tui/layout"
@@ -28,13 +28,17 @@ type ToolCallCmp interface {
28 SetCancelled() // Mark as cancelled
28 SetCancelled() // Mark as cancelled
29 ParentMessageId() string // Get parent message ID
29 ParentMessageId() string // Get parent message ID
30 Spinning() bool // Animation state for pending tools
30 Spinning() bool // Animation state for pending tools
31+ GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
32+ SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls
33+ SetIsNested(bool) // Set whether this tool call is nested
31}
34}
32
35
33// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
36// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
34// It handles rendering of tool execution states including pending, completed, and error states.
37// It handles rendering of tool execution states including pending, completed, and error states.
35type toolCallCmp struct {
38type toolCallCmp struct {
36− width int // Component width for text wrapping
39+ width int // Component width for text wrapping
37− focused bool // Focus state for border styling
40+ focused bool // Focus state for border styling
41+ isNested bool // Whether this tool call is nested within another
38
42
39 // Tool call data and state
43 // Tool call data and state
40 parentMessageId string // ID of the message that initiated this tool call
44 parentMessageId string // ID of the message that initiated this tool call
@@ -45,6 +49,8 @@ type toolCallCmp struct {
45 // Animation state for pending tool calls
49 // Animation state for pending tool calls
46 spinning bool // Whether to show loading animation
50 spinning bool // Whether to show loading animation
47 anim util.Model // Animation component for pending states
51 anim util.Model // Animation component for pending states
52+
53+ nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
48}
54}
49
55
50// ToolCallOption provides functional options for configuring tool call components
56// ToolCallOption provides functional options for configuring tool call components
@@ -64,17 +70,32 @@ func WithToolCallResult(result message.ToolResult) ToolCallOption {
64 }
70 }
65}
71}
66
72
73+func WithToolCallNested(isNested bool) ToolCallOption {
74+ return func(m *toolCallCmp) {
75+ m.isNested = isNested
76+ }
77+}
78+
79+func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption {
80+ return func(m *toolCallCmp) {
81+ m.nestedToolCalls = calls
82+ }
83+}
84+
67// NewToolCallCmp creates a new tool call component with the given parent message ID,
85// NewToolCallCmp creates a new tool call component with the given parent message ID,
68// tool call, and optional configuration
86// tool call, and optional configuration
69func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
87func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
70 m := &toolCallCmp{
88 m := &toolCallCmp{
71 call: tc,
89 call: tc,
72 parentMessageId: parentMessageId,
90 parentMessageId: parentMessageId,
73− anim: anim.New(15, "Working"),
74 }
91 }
75 for _, opt := range opts {
92 for _, opt := range opts {
76 opt(m)
93 opt(m)
77 }
94 }
95+ m.anim = anim.New(15, "Working")
96+ if m.isNested {
97+ m.anim = anim.New(10, "")
98+ }
78 return m
99 return m
79}
100}
80
101
@@ -82,7 +103,6 @@ func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCal
82// Returns a command to start the animation for pending tool calls.
103// Returns a command to start the animation for pending tool calls.
83func (m *toolCallCmp) Init() tea.Cmd {
104func (m *toolCallCmp) Init() tea.Cmd {
84 m.spinning = m.shouldSpin()
105 m.spinning = m.shouldSpin()
85− logging.Info("Initializing tool call spinner", "tool_call", m.call.Name, "spinning", m.spinning)
86 if m.spinning {
106 if m.spinning {
87 return m.anim.Init()
107 return m.anim.Init()
88 }
108 }
@@ -92,14 +112,22 @@ func (m *toolCallCmp) Init() tea.Cmd {
92// Update handles incoming messages and updates the component state.
112// Update handles incoming messages and updates the component state.
93// Manages animation updates for pending tool calls.
113// Manages animation updates for pending tool calls.
94func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
95− logging.Debug("Tool call update", "msg", msg)
96 switch msg := msg.(type) {
115 switch msg := msg.(type) {
97− case anim.ColorCycleMsg, anim.StepCharsMsg:
116+ case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
117+ var cmds []tea.Cmd
118+ for i, nested := range m.nestedToolCalls {
119+ if nested.Spinning() {
120+ u, cmd := nested.Update(msg)
121+ m.nestedToolCalls[i] = u.(ToolCallCmp)
122+ cmds = append(cmds, cmd)
123+ }
124+ }
98 if m.spinning {
125 if m.spinning {
99 u, cmd := m.anim.Update(msg)
126 u, cmd := m.anim.Update(msg)
100 m.anim = u.(util.Model)
127 m.anim = u.(util.Model)
101− return m, cmd
128+ cmds = append(cmds, cmd)
102 }
129 }
130+ return m, tea.Batch(cmds...)
103 }
131 }
104 return m, nil
132 return m, nil
105}
133}
@@ -114,6 +142,15 @@ func (m *toolCallCmp) View() string {
114 }
142 }
115
143
116 r := registry.lookup(m.call.Name)
144 r := registry.lookup(m.call.Name)
145+
146+ if m.isNested {
147+ return box.Render(
148+ lipgloss.JoinHorizontal(lipgloss.Left,
149+ " └ ",
150+ r.Render(m),
151+ ),
152+ )
153+ }
117 return box.PaddingLeft(1).Render(r.Render(m))
154 return box.PaddingLeft(1).Render(r.Render(m))
118}
155}
119
156
@@ -153,10 +190,31 @@ func (m *toolCallCmp) GetToolResult() message.ToolResult {
153 return m.result
190 return m.result
154}
191}
155
192
193+// GetNestedToolCalls returns the nested tool calls
194+func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
195+ return m.nestedToolCalls
196+}
197+
198+// SetNestedToolCalls sets the nested tool calls
199+func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
200+ m.nestedToolCalls = calls
201+ for _, nested := range m.nestedToolCalls {
202+ nested.SetSize(m.width, 0)
203+ }
204+}
205+
206+// SetIsNested sets whether this tool call is nested within another
207+func (m *toolCallCmp) SetIsNested(isNested bool) {
208+ m.isNested = isNested
209+}
210+
156// Rendering methods
211// Rendering methods
157
212
158// renderPending displays the tool name with a loading animation for pending tool calls
213// renderPending displays the tool name with a loading animation for pending tool calls
159func (m *toolCallCmp) renderPending() string {
214func (m *toolCallCmp) renderPending() string {
215+ if m.isNested {
216+ return fmt.Sprintf("└ %s: %s", prettifyToolName(m.call.Name), m.anim.View())
217+ }
160 return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View())
218 return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View())
161}
219}
162
220
@@ -164,6 +222,10 @@ func (m *toolCallCmp) renderPending() string {
164// Applies muted colors and focus-dependent border styles.
222// Applies muted colors and focus-dependent border styles.
165func (m *toolCallCmp) style() lipgloss.Style {
223func (m *toolCallCmp) style() lipgloss.Style {
166 t := theme.CurrentTheme()
224 t := theme.CurrentTheme()
225+ if m.isNested {
226+ return styles.BaseStyle().
227+ Foreground(t.TextMuted())
228+ }
167 borderStyle := lipgloss.NormalBorder()
229 borderStyle := lipgloss.NormalBorder()
168 if m.focused {
230 if m.focused {
169 borderStyle = lipgloss.DoubleBorder()
231 borderStyle = lipgloss.DoubleBorder()
@@ -218,6 +280,9 @@ func (m *toolCallCmp) GetSize() (int, int) {
218// SetSize updates the width of the tool call component for text wrapping
280// SetSize updates the width of the tool call component for text wrapping
219func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
281func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
220 m.width = width
282 m.width = width
283+ for _, nested := range m.nestedToolCalls {
284+ nested.SetSize(width, height)
285+ }
221 return nil
286 return nil
222}
287}
223
288
@@ -234,5 +299,13 @@ func (m *toolCallCmp) shouldSpin() bool {
234
299
235// Spinning returns whether the tool call is currently showing a loading animation
300// Spinning returns whether the tool call is currently showing a loading animation
236func (m *toolCallCmp) Spinning() bool {
301func (m *toolCallCmp) Spinning() bool {
302+ if m.spinning {
303+ return true
304+ }
305+ for _, nested := range m.nestedToolCalls {
306+ if nested.Spinning() {
307+ return true
308+ }
309+ }
237 return m.spinning
310 return m.spinning
238}
311}
internal/tui/components/core/list/list.go+2−1
@@ -6,6 +6,7 @@ import (
6
6
7 "github.com/charmbracelet/bubbles/v2/help"
7 "github.com/charmbracelet/bubbles/v2/help"
8 "github.com/charmbracelet/bubbles/v2/key"
8 "github.com/charmbracelet/bubbles/v2/key"
9+ "github.com/charmbracelet/bubbles/v2/spinner"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/charmbracelet/lipgloss/v2"
11 "github.com/opencode-ai/opencode/internal/tui/components/anim"
12 "github.com/opencode-ai/opencode/internal/tui/components/anim"
@@ -182,7 +183,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
182 switch msg := msg.(type) {
183 switch msg := msg.(type) {
183 case tea.KeyPressMsg:
184 case tea.KeyPressMsg:
184 return m.handleKeyPress(msg)
185 return m.handleKeyPress(msg)
185− case anim.ColorCycleMsg, anim.StepCharsMsg:
186+ case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
186 return m.handleAnimationMsg(msg)
187 return m.handleAnimationMsg(msg)
187 }
188 }
188
189