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