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	r := registry.lookup(m.call.Name)
185
186	if m.isNested {
187		return box.Render(r.Render(m))
188	}
189	return box.Render(r.Render(m))
190}
191
192// State management methods
193
194// SetCancelled marks the tool call as cancelled
195func (m *toolCallCmp) SetCancelled() {
196	m.cancelled = true
197}
198
199func (m *toolCallCmp) copyTool() tea.Cmd {
200	content := m.formatToolForCopy()
201	return tea.Sequence(
202		tea.SetClipboard(content),
203		func() tea.Msg {
204			_ = clipboard.WriteAll(content)
205			return nil
206		},
207		util.ReportInfo("Tool content copied to clipboard"),
208	)
209}
210
211func (m *toolCallCmp) formatToolForCopy() string {
212	var parts []string
213
214	toolName := prettifyToolName(m.call.Name)
215	parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
216
217	if m.call.Input != "" {
218		params := m.formatParametersForCopy()
219		if params != "" {
220			parts = append(parts, "### Parameters:")
221			parts = append(parts, params)
222		}
223	}
224
225	if m.result.ToolCallID != "" {
226		if m.result.IsError {
227			parts = append(parts, "### Error:")
228			parts = append(parts, m.result.Content)
229		} else {
230			parts = append(parts, "### Result:")
231			content := m.formatResultForCopy()
232			if content != "" {
233				parts = append(parts, content)
234			}
235		}
236	} else if m.cancelled {
237		parts = append(parts, "### Status:")
238		parts = append(parts, "Cancelled")
239	} else {
240		parts = append(parts, "### Status:")
241		parts = append(parts, "Pending...")
242	}
243
244	return strings.Join(parts, "\n\n")
245}
246
247func (m *toolCallCmp) formatParametersForCopy() string {
248	switch m.call.Name {
249	case tools.BashToolName:
250		var params tools.BashParams
251		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
252			cmd := strings.ReplaceAll(params.Command, "\n", " ")
253			cmd = strings.ReplaceAll(cmd, "\t", "    ")
254			return fmt.Sprintf("**Command:** %s", cmd)
255		}
256	case tools.ViewToolName:
257		var params tools.ViewParams
258		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
259			var parts []string
260			parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
261			if params.Limit > 0 {
262				parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
263			}
264			if params.Offset > 0 {
265				parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
266			}
267			return strings.Join(parts, "\n")
268		}
269	case tools.EditToolName:
270		var params tools.EditParams
271		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
272			return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
273		}
274	case tools.MultiEditToolName:
275		var params tools.MultiEditParams
276		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
277			var parts []string
278			parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
279			parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
280			return strings.Join(parts, "\n")
281		}
282	case tools.WriteToolName:
283		var params tools.WriteParams
284		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
285			return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
286		}
287	case tools.FetchToolName:
288		var params tools.FetchParams
289		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
290			var parts []string
291			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
292			if params.Format != "" {
293				parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
294			}
295			if params.Timeout > 0 {
296				parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
297			}
298			return strings.Join(parts, "\n")
299		}
300	case tools.AgenticFetchToolName:
301		var params tools.AgenticFetchParams
302		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
303			var parts []string
304			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
305			if params.Prompt != "" {
306				parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
307			}
308			return strings.Join(parts, "\n")
309		}
310	case tools.WebFetchToolName:
311		var params tools.WebFetchParams
312		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
313			return fmt.Sprintf("**URL:** %s", params.URL)
314		}
315	case tools.GrepToolName:
316		var params tools.GrepParams
317		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
318			var parts []string
319			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
320			if params.Path != "" {
321				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
322			}
323			if params.Include != "" {
324				parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
325			}
326			if params.LiteralText {
327				parts = append(parts, "**Literal:** true")
328			}
329			return strings.Join(parts, "\n")
330		}
331	case tools.GlobToolName:
332		var params tools.GlobParams
333		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
334			var parts []string
335			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
336			if params.Path != "" {
337				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
338			}
339			return strings.Join(parts, "\n")
340		}
341	case tools.LSToolName:
342		var params tools.LSParams
343		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
344			path := params.Path
345			if path == "" {
346				path = "."
347			}
348			return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
349		}
350	case tools.DownloadToolName:
351		var params tools.DownloadParams
352		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
353			var parts []string
354			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
355			parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
356			if params.Timeout > 0 {
357				parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
358			}
359			return strings.Join(parts, "\n")
360		}
361	case tools.SourcegraphToolName:
362		var params tools.SourcegraphParams
363		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
364			var parts []string
365			parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
366			if params.Count > 0 {
367				parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
368			}
369			if params.ContextWindow > 0 {
370				parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
371			}
372			return strings.Join(parts, "\n")
373		}
374	case tools.DiagnosticsToolName:
375		return "**Project:** diagnostics"
376	case agent.AgentToolName:
377		var params agent.AgentParams
378		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
379			return fmt.Sprintf("**Task:**\n%s", params.Prompt)
380		}
381	}
382
383	var params map[string]any
384	if json.Unmarshal([]byte(m.call.Input), &params) == nil {
385		var parts []string
386		for key, value := range params {
387			displayKey := strings.ReplaceAll(key, "_", " ")
388			if len(displayKey) > 0 {
389				displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
390			}
391			parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
392		}
393		return strings.Join(parts, "\n")
394	}
395
396	return ""
397}
398
399func (m *toolCallCmp) formatResultForCopy() string {
400	if m.result.Data != "" {
401		if strings.HasPrefix(m.result.MIMEType, "image/") {
402			return fmt.Sprintf("[Image: %s]", m.result.MIMEType)
403		}
404		return fmt.Sprintf("[Media: %s]", m.result.MIMEType)
405	}
406
407	switch m.call.Name {
408	case tools.BashToolName:
409		return m.formatBashResultForCopy()
410	case tools.ViewToolName:
411		return m.formatViewResultForCopy()
412	case tools.EditToolName:
413		return m.formatEditResultForCopy()
414	case tools.MultiEditToolName:
415		return m.formatMultiEditResultForCopy()
416	case tools.WriteToolName:
417		return m.formatWriteResultForCopy()
418	case tools.FetchToolName:
419		return m.formatFetchResultForCopy()
420	case tools.AgenticFetchToolName:
421		return m.formatAgenticFetchResultForCopy()
422	case tools.WebFetchToolName:
423		return m.formatWebFetchResultForCopy()
424	case agent.AgentToolName:
425		return m.formatAgentResultForCopy()
426	case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
427		return fmt.Sprintf("```\n%s\n```", m.result.Content)
428	default:
429		return m.result.Content
430	}
431}
432
433func (m *toolCallCmp) formatBashResultForCopy() string {
434	var meta tools.BashResponseMetadata
435	if m.result.Metadata != "" {
436		json.Unmarshal([]byte(m.result.Metadata), &meta)
437	}
438
439	output := meta.Output
440	if output == "" && m.result.Content != tools.BashNoOutput {
441		output = m.result.Content
442	}
443
444	if output == "" {
445		return ""
446	}
447
448	return fmt.Sprintf("```bash\n%s\n```", output)
449}
450
451func (m *toolCallCmp) formatViewResultForCopy() string {
452	var meta tools.ViewResponseMetadata
453	if m.result.Metadata != "" {
454		json.Unmarshal([]byte(m.result.Metadata), &meta)
455	}
456
457	if meta.Content == "" {
458		return m.result.Content
459	}
460
461	lang := ""
462	if meta.FilePath != "" {
463		ext := strings.ToLower(filepath.Ext(meta.FilePath))
464		switch ext {
465		case ".go":
466			lang = "go"
467		case ".js", ".mjs":
468			lang = "javascript"
469		case ".ts":
470			lang = "typescript"
471		case ".py":
472			lang = "python"
473		case ".rs":
474			lang = "rust"
475		case ".java":
476			lang = "java"
477		case ".c":
478			lang = "c"
479		case ".cpp", ".cc", ".cxx":
480			lang = "cpp"
481		case ".sh", ".bash":
482			lang = "bash"
483		case ".json":
484			lang = "json"
485		case ".yaml", ".yml":
486			lang = "yaml"
487		case ".xml":
488			lang = "xml"
489		case ".html":
490			lang = "html"
491		case ".css":
492			lang = "css"
493		case ".md":
494			lang = "markdown"
495		}
496	}
497
498	var result strings.Builder
499	if lang != "" {
500		result.WriteString(fmt.Sprintf("```%s\n", lang))
501	} else {
502		result.WriteString("```\n")
503	}
504	result.WriteString(meta.Content)
505	result.WriteString("\n```")
506
507	return result.String()
508}
509
510func (m *toolCallCmp) formatEditResultForCopy() string {
511	var meta tools.EditResponseMetadata
512	if m.result.Metadata == "" {
513		return m.result.Content
514	}
515
516	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
517		return m.result.Content
518	}
519
520	var params tools.EditParams
521	json.Unmarshal([]byte(m.call.Input), &params)
522
523	var result strings.Builder
524
525	if meta.OldContent != "" || meta.NewContent != "" {
526		fileName := params.FilePath
527		if fileName != "" {
528			fileName = fsext.PrettyPath(fileName)
529		}
530		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
531
532		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
533		result.WriteString("```diff\n")
534		result.WriteString(diffContent)
535		result.WriteString("\n```")
536	}
537
538	return result.String()
539}
540
541func (m *toolCallCmp) formatMultiEditResultForCopy() string {
542	var meta tools.MultiEditResponseMetadata
543	if m.result.Metadata == "" {
544		return m.result.Content
545	}
546
547	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
548		return m.result.Content
549	}
550
551	var params tools.MultiEditParams
552	json.Unmarshal([]byte(m.call.Input), &params)
553
554	var result strings.Builder
555	if meta.OldContent != "" || meta.NewContent != "" {
556		fileName := params.FilePath
557		if fileName != "" {
558			fileName = fsext.PrettyPath(fileName)
559		}
560		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
561
562		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
563		result.WriteString("```diff\n")
564		result.WriteString(diffContent)
565		result.WriteString("\n```")
566	}
567
568	return result.String()
569}
570
571func (m *toolCallCmp) formatWriteResultForCopy() string {
572	var params tools.WriteParams
573	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
574		return m.result.Content
575	}
576
577	lang := ""
578	if params.FilePath != "" {
579		ext := strings.ToLower(filepath.Ext(params.FilePath))
580		switch ext {
581		case ".go":
582			lang = "go"
583		case ".js", ".mjs":
584			lang = "javascript"
585		case ".ts":
586			lang = "typescript"
587		case ".py":
588			lang = "python"
589		case ".rs":
590			lang = "rust"
591		case ".java":
592			lang = "java"
593		case ".c":
594			lang = "c"
595		case ".cpp", ".cc", ".cxx":
596			lang = "cpp"
597		case ".sh", ".bash":
598			lang = "bash"
599		case ".json":
600			lang = "json"
601		case ".yaml", ".yml":
602			lang = "yaml"
603		case ".xml":
604			lang = "xml"
605		case ".html":
606			lang = "html"
607		case ".css":
608			lang = "css"
609		case ".md":
610			lang = "markdown"
611		}
612	}
613
614	var result strings.Builder
615	result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
616	if lang != "" {
617		result.WriteString(fmt.Sprintf("```%s\n", lang))
618	} else {
619		result.WriteString("```\n")
620	}
621	result.WriteString(params.Content)
622	result.WriteString("\n```")
623
624	return result.String()
625}
626
627func (m *toolCallCmp) formatFetchResultForCopy() string {
628	var params tools.FetchParams
629	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
630		return m.result.Content
631	}
632
633	var result strings.Builder
634	if params.URL != "" {
635		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
636	}
637	if params.Format != "" {
638		result.WriteString(fmt.Sprintf("Format: %s\n", params.Format))
639	}
640	if params.Timeout > 0 {
641		result.WriteString(fmt.Sprintf("Timeout: %ds\n", params.Timeout))
642	}
643	result.WriteString("\n")
644
645	result.WriteString(m.result.Content)
646
647	return result.String()
648}
649
650func (m *toolCallCmp) formatAgenticFetchResultForCopy() string {
651	var params tools.AgenticFetchParams
652	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
653		return m.result.Content
654	}
655
656	var result strings.Builder
657	if params.URL != "" {
658		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
659	}
660	if params.Prompt != "" {
661		result.WriteString(fmt.Sprintf("Prompt: %s\n\n", params.Prompt))
662	}
663
664	result.WriteString("```markdown\n")
665	result.WriteString(m.result.Content)
666	result.WriteString("\n```")
667
668	return result.String()
669}
670
671func (m *toolCallCmp) formatWebFetchResultForCopy() string {
672	var params tools.WebFetchParams
673	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
674		return m.result.Content
675	}
676
677	var result strings.Builder
678	result.WriteString(fmt.Sprintf("URL: %s\n\n", params.URL))
679	result.WriteString("```markdown\n")
680	result.WriteString(m.result.Content)
681	result.WriteString("\n```")
682
683	return result.String()
684}
685
686func (m *toolCallCmp) formatAgentResultForCopy() string {
687	var result strings.Builder
688
689	if len(m.nestedToolCalls) > 0 {
690		result.WriteString("### Nested Tool Calls:\n")
691		for i, nestedCall := range m.nestedToolCalls {
692			nestedContent := nestedCall.(*toolCallCmp).formatToolForCopy()
693			indentedContent := strings.ReplaceAll(nestedContent, "\n", "\n  ")
694			result.WriteString(fmt.Sprintf("%d. %s\n", i+1, indentedContent))
695			if i < len(m.nestedToolCalls)-1 {
696				result.WriteString("\n")
697			}
698		}
699
700		if m.result.Content != "" {
701			result.WriteString("\n### Final Result:\n")
702		}
703	}
704
705	if m.result.Content != "" {
706		result.WriteString(fmt.Sprintf("```markdown\n%s\n```", m.result.Content))
707	}
708
709	return result.String()
710}
711
712// SetToolCall updates the tool call data and stops spinning if finished
713func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
714	m.call = call
715	if m.call.Finished {
716		m.spinning = false
717	}
718}
719
720// ParentMessageID returns the ID of the message that initiated this tool call
721func (m *toolCallCmp) ParentMessageID() string {
722	return m.parentMessageID
723}
724
725// SetToolResult updates the tool result and stops the spinning animation
726func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
727	m.result = result
728	m.spinning = false
729}
730
731// GetToolCall returns the current tool call data
732func (m *toolCallCmp) GetToolCall() message.ToolCall {
733	return m.call
734}
735
736// GetToolResult returns the current tool result data
737func (m *toolCallCmp) GetToolResult() message.ToolResult {
738	return m.result
739}
740
741// GetNestedToolCalls returns the nested tool calls
742func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
743	return m.nestedToolCalls
744}
745
746// SetNestedToolCalls sets the nested tool calls
747func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
748	m.nestedToolCalls = calls
749	for _, nested := range m.nestedToolCalls {
750		nested.SetSize(m.width, 0)
751	}
752}
753
754// SetIsNested sets whether this tool call is nested within another
755func (m *toolCallCmp) SetIsNested(isNested bool) {
756	m.isNested = isNested
757}
758
759// Rendering methods
760
761// renderPending displays the tool name with a loading animation for pending tool calls
762func (m *toolCallCmp) renderPending() string {
763	t := styles.CurrentTheme()
764	icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
765	if m.isNested {
766		tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
767		return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
768	}
769	tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
770	return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
771}
772
773// style returns the lipgloss style for the tool call component.
774// Applies muted colors and focus-dependent border styles.
775func (m *toolCallCmp) style() lipgloss.Style {
776	t := styles.CurrentTheme()
777
778	if m.isNested {
779		return t.S().Muted
780	}
781	style := t.S().Muted.PaddingLeft(2)
782
783	if m.focused {
784		style = style.PaddingLeft(1).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
785	}
786	return style
787}
788
789// textWidth calculates the available width for text content,
790// accounting for borders and padding
791func (m *toolCallCmp) textWidth() int {
792	if m.isNested {
793		return m.width - 6
794	}
795	return m.width - 5 // take into account the border and PaddingLeft
796}
797
798// fit truncates content to fit within the specified width with ellipsis
799func (m *toolCallCmp) fit(content string, width int) string {
800	if lipgloss.Width(content) <= width {
801		return content
802	}
803	t := styles.CurrentTheme()
804	lineStyle := t.S().Muted
805	dots := lineStyle.Render("…")
806	return ansi.Truncate(content, width, dots)
807}
808
809// Focus management methods
810
811// Blur removes focus from the tool call component
812func (m *toolCallCmp) Blur() tea.Cmd {
813	m.focused = false
814	return nil
815}
816
817// Focus sets focus on the tool call component
818func (m *toolCallCmp) Focus() tea.Cmd {
819	m.focused = true
820	return nil
821}
822
823// IsFocused returns whether the tool call component is currently focused
824func (m *toolCallCmp) IsFocused() bool {
825	return m.focused
826}
827
828// Size management methods
829
830// GetSize returns the current dimensions of the tool call component
831func (m *toolCallCmp) GetSize() (int, int) {
832	return m.width, 0
833}
834
835// SetSize updates the width of the tool call component for text wrapping
836func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
837	m.width = width
838	for _, nested := range m.nestedToolCalls {
839		nested.SetSize(width, height)
840	}
841	return nil
842}
843
844// shouldSpin determines whether the tool call should show a loading animation.
845// Returns true if the tool call is not finished or if the result doesn't match the call ID.
846func (m *toolCallCmp) shouldSpin() bool {
847	return !m.call.Finished && !m.cancelled
848}
849
850// Spinning returns whether the tool call is currently showing a loading animation
851func (m *toolCallCmp) Spinning() bool {
852	if m.spinning {
853		return true
854	}
855	for _, nested := range m.nestedToolCalls {
856		if nested.Spinning() {
857			return true
858		}
859	}
860	return m.spinning
861}
862
863func (m *toolCallCmp) ID() string {
864	return m.call.ID
865}
866
867// SetPermissionRequested marks that a permission request was made for this tool call
868func (m *toolCallCmp) SetPermissionRequested() {
869	m.permissionRequested = true
870}
871
872// SetPermissionGranted marks that permission was granted for this tool call
873func (m *toolCallCmp) SetPermissionGranted() {
874	m.permissionGranted = true
875}