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