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}