@@ -3,10 +3,10 @@ package messages
33import (
44 "fmt"
55
6+ "github.com/charmbracelet/bubbles/v2/spinner"
67 tea "github.com/charmbracelet/bubbletea/v2"
78 "github.com/charmbracelet/lipgloss/v2"
89 "github.com/charmbracelet/x/ansi"
9− "github.com/opencode-ai/opencode/internal/logging"
1010 "github.com/opencode-ai/opencode/internal/message"
1111 "github.com/opencode-ai/opencode/internal/tui/components/anim"
1212 "github.com/opencode-ai/opencode/internal/tui/layout"
@@ -28,13 +28,17 @@ type ToolCallCmp interface {
2828 SetCancelled() // Mark as cancelled
2929 ParentMessageId() string // Get parent message ID
3030 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
3134}
3235
3336// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
3437// It handles rendering of tool execution states including pending, completed, and error states.
3538type toolCallCmp struct {
36− width int // Component width for text wrapping
37− focused bool // Focus state for border styling
39+ width int // Component width for text wrapping
40+ focused bool // Focus state for border styling
41+ isNested bool // Whether this tool call is nested within another
3842
3943 // Tool call data and state
4044 parentMessageId string // ID of the message that initiated this tool call
@@ -45,6 +49,8 @@ type toolCallCmp struct {
4549 // Animation state for pending tool calls
4650 spinning bool // Whether to show loading animation
4751 anim util.Model // Animation component for pending states
52+
53+ nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
4854}
4955
5056// ToolCallOption provides functional options for configuring tool call components
@@ -64,17 +70,32 @@ func WithToolCallResult(result message.ToolResult) ToolCallOption {
6470 }
6571}
6672
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+
6785// NewToolCallCmp creates a new tool call component with the given parent message ID,
6886// tool call, and optional configuration
6987func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
7088 m := &toolCallCmp{
7189 call: tc,
7290 parentMessageId: parentMessageId,
73− anim: anim.New(15, "Working"),
7491 }
7592 for _, opt := range opts {
7693 opt(m)
7794 }
95+ m.anim = anim.New(15, "Working")
96+ if m.isNested {
97+ m.anim = anim.New(10, "")
98+ }
7899 return m
79100}
80101
@@ -82,7 +103,6 @@ func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCal
82103// Returns a command to start the animation for pending tool calls.
83104func (m *toolCallCmp) Init() tea.Cmd {
84105 m.spinning = m.shouldSpin()
85− logging.Info("Initializing tool call spinner", "tool_call", m.call.Name, "spinning", m.spinning)
86106 if m.spinning {
87107 return m.anim.Init()
88108 }
@@ -92,14 +112,22 @@ func (m *toolCallCmp) Init() tea.Cmd {
92112// Update handles incoming messages and updates the component state.
93113// Manages animation updates for pending tool calls.
94114func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
95− logging.Debug("Tool call update", "msg", msg)
96115 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+ }
98125 if m.spinning {
99126 u, cmd := m.anim.Update(msg)
100127 m.anim = u.(util.Model)
101− return m, cmd
128+ cmds = append(cmds, cmd)
102129 }
130+ return m, tea.Batch(cmds...)
103131 }
104132 return m, nil
105133}
@@ -114,6 +142,15 @@ func (m *toolCallCmp) View() string {
114142 }
115143
116144 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+ }
117154 return box.PaddingLeft(1).Render(r.Render(m))
118155}
119156
@@ -153,10 +190,31 @@ func (m *toolCallCmp) GetToolResult() message.ToolResult {
153190 return m.result
154191}
155192
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+
156211// Rendering methods
157212
158213// renderPending displays the tool name with a loading animation for pending tool calls
159214func (m *toolCallCmp) renderPending() string {
215+ if m.isNested {
216+ return fmt.Sprintf("└ %s: %s", prettifyToolName(m.call.Name), m.anim.View())
217+ }
160218 return fmt.Sprintf("%s: %s", prettifyToolName(m.call.Name), m.anim.View())
161219}
162220
@@ -164,6 +222,10 @@ func (m *toolCallCmp) renderPending() string {
164222// Applies muted colors and focus-dependent border styles.
165223func (m *toolCallCmp) style() lipgloss.Style {
166224 t := theme.CurrentTheme()
225+ if m.isNested {
226+ return styles.BaseStyle().
227+ Foreground(t.TextMuted())
228+ }
167229 borderStyle := lipgloss.NormalBorder()
168230 if m.focused {
169231 borderStyle = lipgloss.DoubleBorder()
@@ -218,6 +280,9 @@ func (m *toolCallCmp) GetSize() (int, int) {
218280// SetSize updates the width of the tool call component for text wrapping
219281func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
220282 m.width = width
283+ for _, nested := range m.nestedToolCalls {
284+ nested.SetSize(width, height)
285+ }
221286 return nil
222287}
223288
@@ -234,5 +299,13 @@ func (m *toolCallCmp) shouldSpin() bool {
234299
235300// Spinning returns whether the tool call is currently showing a loading animation
236301func (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+ }
237310 return m.spinning
238311}