1package messages
2
3import (
4 "fmt"
5
6 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/crush/internal/message"
8 "github.com/charmbracelet/crush/internal/tui/components/anim"
9 "github.com/charmbracelet/crush/internal/tui/components/core/layout"
10 "github.com/charmbracelet/crush/internal/tui/styles"
11 "github.com/charmbracelet/crush/internal/tui/util"
12 "github.com/charmbracelet/lipgloss/v2"
13 "github.com/charmbracelet/x/ansi"
14)
15
16// ToolCallCmp defines the interface for tool call components in the chat interface.
17// It manages the display of tool execution including pending states, results, and errors.
18type ToolCallCmp interface {
19 util.Model // Basic Bubble Tea model interface
20 layout.Sizeable // Width/height management
21 layout.Focusable // Focus state management
22 GetToolCall() message.ToolCall // Access to tool call data
23 GetToolResult() message.ToolResult // Access to tool result data
24 SetToolResult(message.ToolResult) // Update tool result
25 SetToolCall(message.ToolCall) // Update tool call
26 SetCancelled() // Mark as cancelled
27 ParentMessageID() string // Get parent message ID
28 Spinning() bool // Animation state for pending tools
29 GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
30 SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls
31 SetIsNested(bool) // Set whether this tool call is nested
32}
33
34// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
35// It handles rendering of tool execution states including pending, completed, and error states.
36type toolCallCmp struct {
37 width int // Component width for text wrapping
38 focused bool // Focus state for border styling
39 isNested bool // Whether this tool call is nested within another
40
41 // Tool call data and state
42 parentMessageID string // ID of the message that initiated this tool call
43 call message.ToolCall // The tool call being executed
44 result message.ToolResult // The result of the tool execution
45 cancelled bool // Whether the tool call was cancelled
46
47 // Animation state for pending tool calls
48 spinning bool // Whether to show loading animation
49 anim util.Model // Animation component for pending states
50
51 nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
52}
53
54// ToolCallOption provides functional options for configuring tool call components
55type ToolCallOption func(*toolCallCmp)
56
57// WithToolCallCancelled marks the tool call as cancelled
58func WithToolCallCancelled() ToolCallOption {
59 return func(m *toolCallCmp) {
60 m.cancelled = true
61 }
62}
63
64// WithToolCallResult sets the initial tool result
65func WithToolCallResult(result message.ToolResult) ToolCallOption {
66 return func(m *toolCallCmp) {
67 m.result = result
68 }
69}
70
71func WithToolCallNested(isNested bool) ToolCallOption {
72 return func(m *toolCallCmp) {
73 m.isNested = isNested
74 }
75}
76
77func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption {
78 return func(m *toolCallCmp) {
79 m.nestedToolCalls = calls
80 }
81}
82
83// NewToolCallCmp creates a new tool call component with the given parent message ID,
84// tool call, and optional configuration
85func NewToolCallCmp(parentMessageID string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp {
86 m := &toolCallCmp{
87 call: tc,
88 parentMessageID: parentMessageID,
89 }
90 for _, opt := range opts {
91 opt(m)
92 }
93 t := styles.CurrentTheme()
94 m.anim = anim.New(anim.Settings{
95 Size: 15,
96 Label: "Working",
97 GradColorA: t.Primary,
98 GradColorB: t.Secondary,
99 LabelColor: t.FgBase,
100 CycleColors: true,
101 })
102 if m.isNested {
103 m.anim = anim.New(anim.Settings{
104 Size: 10,
105 GradColorA: t.Primary,
106 GradColorB: t.Secondary,
107 CycleColors: true,
108 })
109 }
110 return m
111}
112
113// Init initializes the tool call component and starts animations if needed.
114// Returns a command to start the animation for pending tool calls.
115func (m *toolCallCmp) Init() tea.Cmd {
116 m.spinning = m.shouldSpin()
117 if m.spinning {
118 return m.anim.Init()
119 }
120 return nil
121}
122
123// Update handles incoming messages and updates the component state.
124// Manages animation updates for pending tool calls.
125func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
126 switch msg := msg.(type) {
127 case anim.StepMsg:
128 var cmds []tea.Cmd
129 for i, nested := range m.nestedToolCalls {
130 if nested.Spinning() {
131 u, cmd := nested.Update(msg)
132 m.nestedToolCalls[i] = u.(ToolCallCmp)
133 cmds = append(cmds, cmd)
134 }
135 }
136 if m.spinning {
137 u, cmd := m.anim.Update(msg)
138 m.anim = u.(util.Model)
139 cmds = append(cmds, cmd)
140 }
141 return m, tea.Batch(cmds...)
142 }
143 return m, nil
144}
145
146// View renders the tool call component based on its current state.
147// Shows either a pending animation or the tool-specific rendered result.
148func (m *toolCallCmp) View() string {
149 box := m.style()
150
151 if !m.call.Finished && !m.cancelled {
152 return box.Render(m.renderPending())
153 }
154
155 r := registry.lookup(m.call.Name)
156
157 if m.isNested {
158 return box.Render(r.Render(m))
159 }
160 return box.Render(r.Render(m))
161}
162
163// State management methods
164
165// SetCancelled marks the tool call as cancelled
166func (m *toolCallCmp) SetCancelled() {
167 m.cancelled = true
168}
169
170// SetToolCall updates the tool call data and stops spinning if finished
171func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
172 m.call = call
173 if m.call.Finished {
174 m.spinning = false
175 }
176}
177
178// ParentMessageID returns the ID of the message that initiated this tool call
179func (m *toolCallCmp) ParentMessageID() string {
180 return m.parentMessageID
181}
182
183// SetToolResult updates the tool result and stops the spinning animation
184func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
185 m.result = result
186 m.spinning = false
187}
188
189// GetToolCall returns the current tool call data
190func (m *toolCallCmp) GetToolCall() message.ToolCall {
191 return m.call
192}
193
194// GetToolResult returns the current tool result data
195func (m *toolCallCmp) GetToolResult() message.ToolResult {
196 return m.result
197}
198
199// GetNestedToolCalls returns the nested tool calls
200func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
201 return m.nestedToolCalls
202}
203
204// SetNestedToolCalls sets the nested tool calls
205func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
206 m.nestedToolCalls = calls
207 for _, nested := range m.nestedToolCalls {
208 nested.SetSize(m.width, 0)
209 }
210}
211
212// SetIsNested sets whether this tool call is nested within another
213func (m *toolCallCmp) SetIsNested(isNested bool) {
214 m.isNested = isNested
215}
216
217// Rendering methods
218
219// renderPending displays the tool name with a loading animation for pending tool calls
220func (m *toolCallCmp) renderPending() string {
221 t := styles.CurrentTheme()
222 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
223 if m.isNested {
224 tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
225 return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
226 }
227 tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
228 return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
229}
230
231// style returns the lipgloss style for the tool call component.
232// Applies muted colors and focus-dependent border styles.
233func (m *toolCallCmp) style() lipgloss.Style {
234 t := styles.CurrentTheme()
235
236 if m.isNested {
237 return t.S().Muted
238 }
239 style := t.S().Muted.PaddingLeft(4)
240
241 if m.focused {
242 style = style.PaddingLeft(3).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
243 }
244 return style
245}
246
247// textWidth calculates the available width for text content,
248// accounting for borders and padding
249func (m *toolCallCmp) textWidth() int {
250 if m.isNested {
251 return m.width - 6
252 }
253 return m.width - 5 // take into account the border and PaddingLeft
254}
255
256// fit truncates content to fit within the specified width with ellipsis
257func (m *toolCallCmp) fit(content string, width int) string {
258 t := styles.CurrentTheme()
259 lineStyle := t.S().Muted
260 dots := lineStyle.Render("…")
261 return ansi.Truncate(content, width, dots)
262}
263
264// Focus management methods
265
266// Blur removes focus from the tool call component
267func (m *toolCallCmp) Blur() tea.Cmd {
268 m.focused = false
269 return nil
270}
271
272// Focus sets focus on the tool call component
273func (m *toolCallCmp) Focus() tea.Cmd {
274 m.focused = true
275 return nil
276}
277
278// IsFocused returns whether the tool call component is currently focused
279func (m *toolCallCmp) IsFocused() bool {
280 return m.focused
281}
282
283// Size management methods
284
285// GetSize returns the current dimensions of the tool call component
286func (m *toolCallCmp) GetSize() (int, int) {
287 return m.width, 0
288}
289
290// SetSize updates the width of the tool call component for text wrapping
291func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
292 m.width = width
293 for _, nested := range m.nestedToolCalls {
294 nested.SetSize(width, height)
295 }
296 return nil
297}
298
299// shouldSpin determines whether the tool call should show a loading animation.
300// Returns true if the tool call is not finished or if the result doesn't match the call ID.
301func (m *toolCallCmp) shouldSpin() bool {
302 return !m.call.Finished
303}
304
305// Spinning returns whether the tool call is currently showing a loading animation
306func (m *toolCallCmp) Spinning() bool {
307 if m.spinning {
308 return true
309 }
310 for _, nested := range m.nestedToolCalls {
311 if nested.Spinning() {
312 return true
313 }
314 }
315 return m.spinning
316}