tool.go

  1package messages
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"path/filepath"
  7	"strings"
  8	"time"
  9
 10	"charm.land/bubbles/v2/key"
 11	tea "charm.land/bubbletea/v2"
 12	"charm.land/lipgloss/v2"
 13	"github.com/atotto/clipboard"
 14	"github.com/charmbracelet/crush/internal/agent"
 15	"github.com/charmbracelet/crush/internal/agent/tools"
 16	"github.com/charmbracelet/crush/internal/diff"
 17	"github.com/charmbracelet/crush/internal/fsext"
 18	"github.com/charmbracelet/crush/internal/message"
 19	"github.com/charmbracelet/crush/internal/permission"
 20	"github.com/charmbracelet/crush/internal/tui/components/anim"
 21	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 22	"github.com/charmbracelet/crush/internal/tui/styles"
 23	"github.com/charmbracelet/crush/internal/tui/util"
 24	"github.com/charmbracelet/x/ansi"
 25)
 26
 27// ToolCallCmp defines the interface for tool call components in the chat interface.
 28// It manages the display of tool execution including pending states, results, and errors.
 29type ToolCallCmp interface {
 30	util.Model                         // Basic Bubble util.Model interface
 31	layout.Sizeable                    // Width/height management
 32	layout.Focusable                   // Focus state management
 33	GetToolCall() message.ToolCall     // Access to tool call data
 34	GetToolResult() message.ToolResult // Access to tool result data
 35	SetToolResult(message.ToolResult)  // Update tool result
 36	SetToolCall(message.ToolCall)      // Update tool call
 37	SetCancelled()                     // Mark as cancelled
 38	ParentMessageID() string           // Get parent message ID
 39	Spinning() bool                    // Animation state for pending tools
 40	GetNestedToolCalls() []ToolCallCmp // Get nested tool calls
 41	SetNestedToolCalls([]ToolCallCmp)  // Set nested tool calls
 42	SetIsNested(bool)                  // Set whether this tool call is nested
 43	ID() string
 44	SetPermissionRequested() // Mark permission request
 45	SetPermissionGranted()   // Mark permission granted
 46}
 47
 48// toolCallCmp implements the ToolCallCmp interface for displaying tool calls.
 49// It handles rendering of tool execution states including pending, completed, and error states.
 50type toolCallCmp struct {
 51	width    int  // Component width for text wrapping
 52	focused  bool // Focus state for border styling
 53	isNested bool // Whether this tool call is nested within another
 54
 55	// Tool call data and state
 56	parentMessageID     string             // ID of the message that initiated this tool call
 57	call                message.ToolCall   // The tool call being executed
 58	result              message.ToolResult // The result of the tool execution
 59	cancelled           bool               // Whether the tool call was cancelled
 60	permissionRequested bool
 61	permissionGranted   bool
 62
 63	// Animation state for pending tool calls
 64	spinning bool       // Whether to show loading animation
 65	anim     util.Model // Animation component for pending states
 66
 67	nestedToolCalls []ToolCallCmp // Nested tool calls for hierarchical display
 68}
 69
 70// ToolCallOption provides functional options for configuring tool call components
 71type ToolCallOption func(*toolCallCmp)
 72
 73// WithToolCallCancelled marks the tool call as cancelled
 74func WithToolCallCancelled() ToolCallOption {
 75	return func(m *toolCallCmp) {
 76		m.cancelled = true
 77	}
 78}
 79
 80// WithToolCallResult sets the initial tool result
 81func WithToolCallResult(result message.ToolResult) ToolCallOption {
 82	return func(m *toolCallCmp) {
 83		m.result = result
 84	}
 85}
 86
 87func WithToolCallNested(isNested bool) ToolCallOption {
 88	return func(m *toolCallCmp) {
 89		m.isNested = isNested
 90	}
 91}
 92
 93func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption {
 94	return func(m *toolCallCmp) {
 95		m.nestedToolCalls = calls
 96	}
 97}
 98
 99func WithToolPermissionRequested() ToolCallOption {
100	return func(m *toolCallCmp) {
101		m.permissionRequested = true
102	}
103}
104
105func WithToolPermissionGranted() ToolCallOption {
106	return func(m *toolCallCmp) {
107		m.permissionGranted = true
108	}
109}
110
111// NewToolCallCmp creates a new tool call component with the given parent message ID,
112// tool call, and optional configuration
113func NewToolCallCmp(parentMessageID string, tc message.ToolCall, permissions permission.Service, opts ...ToolCallOption) ToolCallCmp {
114	m := &toolCallCmp{
115		call:            tc,
116		parentMessageID: parentMessageID,
117	}
118	for _, opt := range opts {
119		opt(m)
120	}
121	t := styles.CurrentTheme()
122	m.anim = anim.New(anim.Settings{
123		Size:        15,
124		Label:       "Working",
125		GradColorA:  t.Primary,
126		GradColorB:  t.Secondary,
127		LabelColor:  t.FgBase,
128		CycleColors: true,
129	})
130	if m.isNested {
131		m.anim = anim.New(anim.Settings{
132			Size:        10,
133			GradColorA:  t.Primary,
134			GradColorB:  t.Secondary,
135			CycleColors: true,
136		})
137	}
138	return m
139}
140
141// Init initializes the tool call component and starts animations if needed.
142// Returns a command to start the animation for pending tool calls.
143func (m *toolCallCmp) Init() tea.Cmd {
144	m.spinning = m.shouldSpin()
145	return m.anim.Init()
146}
147
148// Update handles incoming messages and updates the component state.
149// Manages animation updates for pending tool calls.
150func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
151	switch msg := msg.(type) {
152	case anim.StepMsg:
153		var cmds []tea.Cmd
154		for i, nested := range m.nestedToolCalls {
155			if nested.Spinning() {
156				u, cmd := nested.Update(msg)
157				m.nestedToolCalls[i] = u.(ToolCallCmp)
158				cmds = append(cmds, cmd)
159			}
160		}
161		if m.spinning {
162			u, cmd := m.anim.Update(msg)
163			m.anim = u
164			cmds = append(cmds, cmd)
165		}
166		return m, tea.Batch(cmds...)
167	case tea.KeyPressMsg:
168		if key.Matches(msg, CopyKey) {
169			return m, m.copyTool()
170		}
171	}
172	return m, nil
173}
174
175// View renders the tool call component based on its current state.
176// Shows either a pending animation or the tool-specific rendered result.
177func (m *toolCallCmp) View() string {
178	box := m.style()
179
180	if !m.call.Finished && !m.cancelled {
181		return box.Render(m.renderPending())
182	}
183
184	if strings.HasPrefix(m.call.Name, "mcp_crush_docker") {
185		r := dockerMCPRenderer{}
186		return box.Render(r.Render(m))
187	}
188
189	r := registry.lookup(m.call.Name)
190
191	return box.Render(r.Render(m))
192}
193
194// State management methods
195
196// SetCancelled marks the tool call as cancelled
197func (m *toolCallCmp) SetCancelled() {
198	m.cancelled = true
199}
200
201func (m *toolCallCmp) copyTool() tea.Cmd {
202	content := m.formatToolForCopy()
203	return tea.Sequence(
204		tea.SetClipboard(content),
205		func() tea.Msg {
206			_ = clipboard.WriteAll(content)
207			return nil
208		},
209		util.ReportInfo("Tool content copied to clipboard"),
210	)
211}
212
213func (m *toolCallCmp) formatToolForCopy() string {
214	var parts []string
215
216	toolName := prettifyToolName(m.call.Name)
217	parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
218
219	if m.call.Input != "" {
220		params := m.formatParametersForCopy()
221		if params != "" {
222			parts = append(parts, "### Parameters:")
223			parts = append(parts, params)
224		}
225	}
226
227	if m.result.ToolCallID != "" {
228		if m.result.IsError {
229			parts = append(parts, "### Error:")
230			parts = append(parts, m.result.Content)
231		} else {
232			parts = append(parts, "### Result:")
233			content := m.formatResultForCopy()
234			if content != "" {
235				parts = append(parts, content)
236			}
237		}
238	} else if m.cancelled {
239		parts = append(parts, "### Status:")
240		parts = append(parts, "Cancelled")
241	} else {
242		parts = append(parts, "### Status:")
243		parts = append(parts, "Pending...")
244	}
245
246	return strings.Join(parts, "\n\n")
247}
248
249func (m *toolCallCmp) formatParametersForCopy() string {
250	switch m.call.Name {
251	case tools.BashToolName:
252		var params tools.BashParams
253		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
254			cmd := strings.ReplaceAll(params.Command, "\n", " ")
255			cmd = strings.ReplaceAll(cmd, "\t", "    ")
256			return fmt.Sprintf("**Command:** %s", cmd)
257		}
258	case tools.ViewToolName:
259		var params tools.ViewParams
260		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
261			var parts []string
262			parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
263			if params.Limit > 0 {
264				parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
265			}
266			if params.Offset > 0 {
267				parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
268			}
269			return strings.Join(parts, "\n")
270		}
271	case tools.EditToolName:
272		var params tools.EditParams
273		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
274			return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
275		}
276	case tools.MultiEditToolName:
277		var params tools.MultiEditParams
278		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
279			var parts []string
280			parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
281			parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
282			return strings.Join(parts, "\n")
283		}
284	case tools.WriteToolName:
285		var params tools.WriteParams
286		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
287			return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
288		}
289	case tools.FetchToolName:
290		var params tools.FetchParams
291		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
292			var parts []string
293			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
294			if params.Format != "" {
295				parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
296			}
297			if params.Timeout > 0 {
298				parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
299			}
300			return strings.Join(parts, "\n")
301		}
302	case tools.AgenticFetchToolName:
303		var params tools.AgenticFetchParams
304		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
305			var parts []string
306			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
307			if params.Prompt != "" {
308				parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
309			}
310			return strings.Join(parts, "\n")
311		}
312	case tools.WebFetchToolName:
313		var params tools.WebFetchParams
314		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
315			return fmt.Sprintf("**URL:** %s", params.URL)
316		}
317	case tools.GrepToolName:
318		var params tools.GrepParams
319		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
320			var parts []string
321			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
322			if params.Path != "" {
323				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
324			}
325			if params.Include != "" {
326				parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
327			}
328			if params.LiteralText {
329				parts = append(parts, "**Literal:** true")
330			}
331			return strings.Join(parts, "\n")
332		}
333	case tools.GlobToolName:
334		var params tools.GlobParams
335		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
336			var parts []string
337			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
338			if params.Path != "" {
339				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
340			}
341			return strings.Join(parts, "\n")
342		}
343	case tools.LSToolName:
344		var params tools.LSParams
345		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
346			path := params.Path
347			if path == "" {
348				path = "."
349			}
350			return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
351		}
352	case tools.DownloadToolName:
353		var params tools.DownloadParams
354		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
355			var parts []string
356			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
357			parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
358			if params.Timeout > 0 {
359				parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
360			}
361			return strings.Join(parts, "\n")
362		}
363	case tools.SourcegraphToolName:
364		var params tools.SourcegraphParams
365		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
366			var parts []string
367			parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
368			if params.Count > 0 {
369				parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
370			}
371			if params.ContextWindow > 0 {
372				parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
373			}
374			return strings.Join(parts, "\n")
375		}
376	case tools.DiagnosticsToolName:
377		return "**Project:** diagnostics"
378	case agent.AgentToolName:
379		var params agent.AgentParams
380		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
381			return fmt.Sprintf("**Task:**\n%s", params.Prompt)
382		}
383	}
384
385	var params map[string]any
386	if json.Unmarshal([]byte(m.call.Input), &params) == nil {
387		var parts []string
388		for key, value := range params {
389			displayKey := strings.ReplaceAll(key, "_", " ")
390			if len(displayKey) > 0 {
391				displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
392			}
393			parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
394		}
395		return strings.Join(parts, "\n")
396	}
397
398	return ""
399}
400
401func (m *toolCallCmp) formatResultForCopy() string {
402	switch m.call.Name {
403	case tools.BashToolName:
404		return m.formatBashResultForCopy()
405	case tools.ViewToolName:
406		return m.formatViewResultForCopy()
407	case tools.EditToolName:
408		return m.formatEditResultForCopy()
409	case tools.MultiEditToolName:
410		return m.formatMultiEditResultForCopy()
411	case tools.WriteToolName:
412		return m.formatWriteResultForCopy()
413	case tools.FetchToolName:
414		return m.formatFetchResultForCopy()
415	case tools.AgenticFetchToolName:
416		return m.formatAgenticFetchResultForCopy()
417	case tools.WebFetchToolName:
418		return m.formatWebFetchResultForCopy()
419	case agent.AgentToolName:
420		return m.formatAgentResultForCopy()
421	case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
422		return fmt.Sprintf("```\n%s\n```", m.result.Content)
423	default:
424		return m.result.Content
425	}
426}
427
428func (m *toolCallCmp) formatBashResultForCopy() string {
429	var meta tools.BashResponseMetadata
430	if m.result.Metadata != "" {
431		json.Unmarshal([]byte(m.result.Metadata), &meta)
432	}
433
434	output := meta.Output
435	if output == "" && m.result.Content != tools.BashNoOutput {
436		output = m.result.Content
437	}
438
439	if output == "" {
440		return ""
441	}
442
443	return fmt.Sprintf("```bash\n%s\n```", output)
444}
445
446func (m *toolCallCmp) formatViewResultForCopy() string {
447	var meta tools.ViewResponseMetadata
448	if m.result.Metadata != "" {
449		json.Unmarshal([]byte(m.result.Metadata), &meta)
450	}
451
452	if meta.Content == "" {
453		return m.result.Content
454	}
455
456	lang := ""
457	if meta.FilePath != "" {
458		ext := strings.ToLower(filepath.Ext(meta.FilePath))
459		switch ext {
460		case ".go":
461			lang = "go"
462		case ".js", ".mjs":
463			lang = "javascript"
464		case ".ts":
465			lang = "typescript"
466		case ".py":
467			lang = "python"
468		case ".rs":
469			lang = "rust"
470		case ".java":
471			lang = "java"
472		case ".c":
473			lang = "c"
474		case ".cpp", ".cc", ".cxx":
475			lang = "cpp"
476		case ".sh", ".bash":
477			lang = "bash"
478		case ".json":
479			lang = "json"
480		case ".yaml", ".yml":
481			lang = "yaml"
482		case ".xml":
483			lang = "xml"
484		case ".html":
485			lang = "html"
486		case ".css":
487			lang = "css"
488		case ".md":
489			lang = "markdown"
490		}
491	}
492
493	var result strings.Builder
494	if lang != "" {
495		result.WriteString(fmt.Sprintf("```%s\n", lang))
496	} else {
497		result.WriteString("```\n")
498	}
499	result.WriteString(meta.Content)
500	result.WriteString("\n```")
501
502	return result.String()
503}
504
505func (m *toolCallCmp) formatEditResultForCopy() string {
506	var meta tools.EditResponseMetadata
507	if m.result.Metadata == "" {
508		return m.result.Content
509	}
510
511	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
512		return m.result.Content
513	}
514
515	var params tools.EditParams
516	json.Unmarshal([]byte(m.call.Input), &params)
517
518	var result strings.Builder
519
520	if meta.OldContent != "" || meta.NewContent != "" {
521		fileName := params.FilePath
522		if fileName != "" {
523			fileName = fsext.PrettyPath(fileName)
524		}
525		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
526
527		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
528		result.WriteString("```diff\n")
529		result.WriteString(diffContent)
530		result.WriteString("\n```")
531	}
532
533	return result.String()
534}
535
536func (m *toolCallCmp) formatMultiEditResultForCopy() string {
537	var meta tools.MultiEditResponseMetadata
538	if m.result.Metadata == "" {
539		return m.result.Content
540	}
541
542	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
543		return m.result.Content
544	}
545
546	var params tools.MultiEditParams
547	json.Unmarshal([]byte(m.call.Input), &params)
548
549	var result strings.Builder
550	if meta.OldContent != "" || meta.NewContent != "" {
551		fileName := params.FilePath
552		if fileName != "" {
553			fileName = fsext.PrettyPath(fileName)
554		}
555		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
556
557		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
558		result.WriteString("```diff\n")
559		result.WriteString(diffContent)
560		result.WriteString("\n```")
561	}
562
563	return result.String()
564}
565
566func (m *toolCallCmp) formatWriteResultForCopy() string {
567	var params tools.WriteParams
568	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
569		return m.result.Content
570	}
571
572	lang := ""
573	if params.FilePath != "" {
574		ext := strings.ToLower(filepath.Ext(params.FilePath))
575		switch ext {
576		case ".go":
577			lang = "go"
578		case ".js", ".mjs":
579			lang = "javascript"
580		case ".ts":
581			lang = "typescript"
582		case ".py":
583			lang = "python"
584		case ".rs":
585			lang = "rust"
586		case ".java":
587			lang = "java"
588		case ".c":
589			lang = "c"
590		case ".cpp", ".cc", ".cxx":
591			lang = "cpp"
592		case ".sh", ".bash":
593			lang = "bash"
594		case ".json":
595			lang = "json"
596		case ".yaml", ".yml":
597			lang = "yaml"
598		case ".xml":
599			lang = "xml"
600		case ".html":
601			lang = "html"
602		case ".css":
603			lang = "css"
604		case ".md":
605			lang = "markdown"
606		}
607	}
608
609	var result strings.Builder
610	result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
611	if lang != "" {
612		result.WriteString(fmt.Sprintf("```%s\n", lang))
613	} else {
614		result.WriteString("```\n")
615	}
616	result.WriteString(params.Content)
617	result.WriteString("\n```")
618
619	return result.String()
620}
621
622func (m *toolCallCmp) formatFetchResultForCopy() string {
623	var params tools.FetchParams
624	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
625		return m.result.Content
626	}
627
628	var result strings.Builder
629	if params.URL != "" {
630		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
631	}
632	if params.Format != "" {
633		result.WriteString(fmt.Sprintf("Format: %s\n", params.Format))
634	}
635	if params.Timeout > 0 {
636		result.WriteString(fmt.Sprintf("Timeout: %ds\n", params.Timeout))
637	}
638	result.WriteString("\n")
639
640	result.WriteString(m.result.Content)
641
642	return result.String()
643}
644
645func (m *toolCallCmp) formatAgenticFetchResultForCopy() string {
646	var params tools.AgenticFetchParams
647	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
648		return m.result.Content
649	}
650
651	var result strings.Builder
652	if params.URL != "" {
653		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
654	}
655	if params.Prompt != "" {
656		result.WriteString(fmt.Sprintf("Prompt: %s\n\n", params.Prompt))
657	}
658
659	result.WriteString("```markdown\n")
660	result.WriteString(m.result.Content)
661	result.WriteString("\n```")
662
663	return result.String()
664}
665
666func (m *toolCallCmp) formatWebFetchResultForCopy() string {
667	var params tools.WebFetchParams
668	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
669		return m.result.Content
670	}
671
672	var result strings.Builder
673	result.WriteString(fmt.Sprintf("URL: %s\n\n", params.URL))
674	result.WriteString("```markdown\n")
675	result.WriteString(m.result.Content)
676	result.WriteString("\n```")
677
678	return result.String()
679}
680
681func (m *toolCallCmp) formatAgentResultForCopy() string {
682	var result strings.Builder
683
684	if len(m.nestedToolCalls) > 0 {
685		result.WriteString("### Nested Tool Calls:\n")
686		for i, nestedCall := range m.nestedToolCalls {
687			nestedContent := nestedCall.(*toolCallCmp).formatToolForCopy()
688			indentedContent := strings.ReplaceAll(nestedContent, "\n", "\n  ")
689			result.WriteString(fmt.Sprintf("%d. %s\n", i+1, indentedContent))
690			if i < len(m.nestedToolCalls)-1 {
691				result.WriteString("\n")
692			}
693		}
694
695		if m.result.Content != "" {
696			result.WriteString("\n### Final Result:\n")
697		}
698	}
699
700	if m.result.Content != "" {
701		result.WriteString(fmt.Sprintf("```markdown\n%s\n```", m.result.Content))
702	}
703
704	return result.String()
705}
706
707// SetToolCall updates the tool call data and stops spinning if finished
708func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
709	m.call = call
710	if m.call.Finished {
711		m.spinning = false
712	}
713}
714
715// ParentMessageID returns the ID of the message that initiated this tool call
716func (m *toolCallCmp) ParentMessageID() string {
717	return m.parentMessageID
718}
719
720// SetToolResult updates the tool result and stops the spinning animation
721func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
722	m.result = result
723	m.spinning = false
724}
725
726// GetToolCall returns the current tool call data
727func (m *toolCallCmp) GetToolCall() message.ToolCall {
728	return m.call
729}
730
731// GetToolResult returns the current tool result data
732func (m *toolCallCmp) GetToolResult() message.ToolResult {
733	return m.result
734}
735
736// GetNestedToolCalls returns the nested tool calls
737func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
738	return m.nestedToolCalls
739}
740
741// SetNestedToolCalls sets the nested tool calls
742func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
743	m.nestedToolCalls = calls
744	for _, nested := range m.nestedToolCalls {
745		nested.SetSize(m.width, 0)
746	}
747}
748
749// SetIsNested sets whether this tool call is nested within another
750func (m *toolCallCmp) SetIsNested(isNested bool) {
751	m.isNested = isNested
752}
753
754// Rendering methods
755
756// renderPending displays the tool name with a loading animation for pending tool calls
757func (m *toolCallCmp) renderPending() string {
758	t := styles.CurrentTheme()
759	icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
760	if m.isNested {
761		tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
762		return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
763	}
764	tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
765	return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
766}
767
768// style returns the lipgloss style for the tool call component.
769// Applies muted colors and focus-dependent border styles.
770func (m *toolCallCmp) style() lipgloss.Style {
771	t := styles.CurrentTheme()
772
773	if m.isNested {
774		return t.S().Muted
775	}
776	style := t.S().Muted.PaddingLeft(2)
777
778	if m.focused {
779		style = style.PaddingLeft(1).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
780	}
781	return style
782}
783
784// textWidth calculates the available width for text content,
785// accounting for borders and padding
786func (m *toolCallCmp) textWidth() int {
787	if m.isNested {
788		return m.width - 6
789	}
790	return m.width - 5 // take into account the border and PaddingLeft
791}
792
793// fit truncates content to fit within the specified width with ellipsis
794func (m *toolCallCmp) fit(content string, width int) string {
795	t := styles.CurrentTheme()
796	lineStyle := t.S().Muted
797	dots := lineStyle.Render("…")
798	return ansi.Truncate(content, width, dots)
799}
800
801// Focus management methods
802
803// Blur removes focus from the tool call component
804func (m *toolCallCmp) Blur() tea.Cmd {
805	m.focused = false
806	return nil
807}
808
809// Focus sets focus on the tool call component
810func (m *toolCallCmp) Focus() tea.Cmd {
811	m.focused = true
812	return nil
813}
814
815// IsFocused returns whether the tool call component is currently focused
816func (m *toolCallCmp) IsFocused() bool {
817	return m.focused
818}
819
820// Size management methods
821
822// GetSize returns the current dimensions of the tool call component
823func (m *toolCallCmp) GetSize() (int, int) {
824	return m.width, 0
825}
826
827// SetSize updates the width of the tool call component for text wrapping
828func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
829	m.width = width
830	for _, nested := range m.nestedToolCalls {
831		nested.SetSize(width, height)
832	}
833	return nil
834}
835
836// shouldSpin determines whether the tool call should show a loading animation.
837// Returns true if the tool call is not finished or if the result doesn't match the call ID.
838func (m *toolCallCmp) shouldSpin() bool {
839	return !m.call.Finished && !m.cancelled
840}
841
842// Spinning returns whether the tool call is currently showing a loading animation
843func (m *toolCallCmp) Spinning() bool {
844	if m.spinning {
845		return true
846	}
847	for _, nested := range m.nestedToolCalls {
848		if nested.Spinning() {
849			return true
850		}
851	}
852	return m.spinning
853}
854
855func (m *toolCallCmp) ID() string {
856	return m.call.ID
857}
858
859// SetPermissionRequested marks that a permission request was made for this tool call
860func (m *toolCallCmp) SetPermissionRequested() {
861	m.permissionRequested = true
862}
863
864// SetPermissionGranted marks that permission was granted for this tool call
865func (m *toolCallCmp) SetPermissionGranted() {
866	m.permissionGranted = true
867}