@@ -3,10 +3,10 @@ package messages
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
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
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
53+ nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
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 {
73+func WithToolCallNested(isNested bool) ToolCallOption {
74+ return func(m *toolCallCmp) {
75+ m.isNested = isNested
79+func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption {
80+ return func(m *toolCallCmp) {
81+ m.nestedToolCalls = calls
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{
72 parentMessageId: parentMessageId,
90 parentMessageId: parentMessageId,
73− anim: anim.New(15, "Working"),
75 for _, opt := range opts {
92 for _, opt := range opts {
95+ m.anim = anim.New(15, "Working")
97+ m.anim = anim.New(10, "")
@@ -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()
@@ -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:
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)
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)
130+ return m, tea.Batch(cmds...)
104 return m, nil
132 return m, nil
@@ -114,6 +142,15 @@ func (m *toolCallCmp) View() string {
116 r := registry.lookup(m.call.Name)
144 r := registry.lookup(m.call.Name)
148+ lipgloss.JoinHorizontal(lipgloss.Left,
117 return box.PaddingLeft(1).Render(r.Render(m))
154 return box.PaddingLeft(1).Render(r.Render(m))
@@ -153,10 +190,31 @@ func (m *toolCallCmp) GetToolResult() message.ToolResult {
153 return m.result
190 return m.result
193+// GetNestedToolCalls returns the nested tool calls
194+func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
195+ return m.nestedToolCalls
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)
206+// SetIsNested sets whether this tool call is nested within another
207+func (m *toolCallCmp) SetIsNested(isNested bool) {
208+ m.isNested = isNested
156// Rendering methods
211// Rendering methods
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 {
216+ return fmt.Sprintf("└ %s: %s", prettifyToolName(m.call.Name), m.anim.View())
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())
@@ -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()
226+ return styles.BaseStyle().
227+ Foreground(t.TextMuted())
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)
221 return nil
286 return nil
@@ -234,5 +299,13 @@ func (m *toolCallCmp) shouldSpin() bool {
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 {
305+ for _, nested := range m.nestedToolCalls {
306+ if nested.Spinning() {
237 return m.spinning
310 return m.spinning