tool.go

  1package messages
  2
  3import (
  4	"fmt"
  5
  6	"github.com/charmbracelet/bubbles/v2/spinner"
  7	tea "github.com/charmbracelet/bubbletea/v2"
  8	"github.com/charmbracelet/crush/internal/message"
  9	"github.com/charmbracelet/crush/internal/tui/components/anim"
 10	"github.com/charmbracelet/crush/internal/tui/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}
 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	m.anim = anim.New(15, "Working")
 95	if m.isNested {
 96		m.anim = anim.New(10, "")
 97	}
 98	return m
 99}
100
101// Init initializes the tool call component and starts animations if needed.
102// Returns a command to start the animation for pending tool calls.
103func (m *toolCallCmp) Init() tea.Cmd {
104	m.spinning = m.shouldSpin()
105	if m.spinning {
106		return m.anim.Init()
107	}
108	return nil
109}
110
111// Update handles incoming messages and updates the component state.
112// Manages animation updates for pending tool calls.
113func (m *toolCallCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
114	switch msg := msg.(type) {
115	case anim.ColorCycleMsg, anim.StepCharsMsg, spinner.TickMsg:
116		var cmds []tea.Cmd
117		for i, nested := range m.nestedToolCalls {
118			if nested.Spinning() {
119				u, cmd := nested.Update(msg)
120				m.nestedToolCalls[i] = u.(ToolCallCmp)
121				cmds = append(cmds, cmd)
122			}
123		}
124		if m.spinning {
125			u, cmd := m.anim.Update(msg)
126			m.anim = u.(util.Model)
127			cmds = append(cmds, cmd)
128		}
129		return m, tea.Batch(cmds...)
130	}
131	return m, nil
132}
133
134// View renders the tool call component based on its current state.
135// Shows either a pending animation or the tool-specific rendered result.
136func (m *toolCallCmp) View() tea.View {
137	box := m.style()
138
139	if !m.call.Finished && !m.cancelled {
140		if m.isNested {
141			return tea.NewView(box.Render(m.renderPending()))
142		}
143		return tea.NewView(box.Render(m.renderPending()))
144	}
145
146	r := registry.lookup(m.call.Name)
147
148	if m.isNested {
149		return tea.NewView(box.Render(r.Render(m)))
150	}
151	return tea.NewView(box.Render(r.Render(m)))
152}
153
154// State management methods
155
156// SetCancelled marks the tool call as cancelled
157func (m *toolCallCmp) SetCancelled() {
158	m.cancelled = true
159}
160
161// SetToolCall updates the tool call data and stops spinning if finished
162func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
163	m.call = call
164	if m.call.Finished {
165		m.spinning = false
166	}
167}
168
169// ParentMessageId returns the ID of the message that initiated this tool call
170func (m *toolCallCmp) ParentMessageId() string {
171	return m.parentMessageID
172}
173
174// SetToolResult updates the tool result and stops the spinning animation
175func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
176	m.result = result
177	m.spinning = false
178}
179
180// GetToolCall returns the current tool call data
181func (m *toolCallCmp) GetToolCall() message.ToolCall {
182	return m.call
183}
184
185// GetToolResult returns the current tool result data
186func (m *toolCallCmp) GetToolResult() message.ToolResult {
187	return m.result
188}
189
190// GetNestedToolCalls returns the nested tool calls
191func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
192	return m.nestedToolCalls
193}
194
195// SetNestedToolCalls sets the nested tool calls
196func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
197	m.nestedToolCalls = calls
198	for _, nested := range m.nestedToolCalls {
199		nested.SetSize(m.width, 0)
200	}
201}
202
203// SetIsNested sets whether this tool call is nested within another
204func (m *toolCallCmp) SetIsNested(isNested bool) {
205	m.isNested = isNested
206}
207
208// Rendering methods
209
210// renderPending displays the tool name with a loading animation for pending tool calls
211func (m *toolCallCmp) renderPending() string {
212	t := styles.CurrentTheme()
213	icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
214	tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
215	return fmt.Sprintf("%s %s: %s", icon, tool, m.anim.View())
216}
217
218// style returns the lipgloss style for the tool call component.
219// Applies muted colors and focus-dependent border styles.
220func (m *toolCallCmp) style() lipgloss.Style {
221	t := styles.CurrentTheme()
222
223	if m.isNested {
224		return t.S().Muted
225	}
226	return t.S().Muted.PaddingLeft(4)
227}
228
229// textWidth calculates the available width for text content,
230// accounting for borders and padding
231func (m *toolCallCmp) textWidth() int {
232	return m.width - 5 // take into account the border and PaddingLeft
233}
234
235// fit truncates content to fit within the specified width with ellipsis
236func (m *toolCallCmp) fit(content string, width int) string {
237	t := styles.CurrentTheme()
238	lineStyle := t.S().Muted.Background(t.BgSubtle)
239	dots := lineStyle.Render("...")
240	return ansi.Truncate(content, width, dots)
241}
242
243// Focus management methods
244
245// Blur removes focus from the tool call component
246func (m *toolCallCmp) Blur() tea.Cmd {
247	m.focused = false
248	return nil
249}
250
251// Focus sets focus on the tool call component
252func (m *toolCallCmp) Focus() tea.Cmd {
253	m.focused = true
254	return nil
255}
256
257// IsFocused returns whether the tool call component is currently focused
258func (m *toolCallCmp) IsFocused() bool {
259	return m.focused
260}
261
262// Size management methods
263
264// GetSize returns the current dimensions of the tool call component
265func (m *toolCallCmp) GetSize() (int, int) {
266	return m.width, 0
267}
268
269// SetSize updates the width of the tool call component for text wrapping
270func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
271	m.width = width
272	for _, nested := range m.nestedToolCalls {
273		nested.SetSize(width, height)
274	}
275	return nil
276}
277
278// shouldSpin determines whether the tool call should show a loading animation.
279// Returns true if the tool call is not finished or if the result doesn't match the call ID.
280func (m *toolCallCmp) shouldSpin() bool {
281	return !m.call.Finished
282}
283
284// Spinning returns whether the tool call is currently showing a loading animation
285func (m *toolCallCmp) Spinning() bool {
286	if m.spinning {
287		return true
288	}
289	for _, nested := range m.nestedToolCalls {
290		if nested.Spinning() {
291			return true
292		}
293	}
294	return m.spinning
295}