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