tool.go

  1package messages
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"path/filepath"
  7	"strings"
  8	"time"
  9
 10	"github.com/atotto/clipboard"
 11	"github.com/charmbracelet/bubbles/v2/key"
 12	tea "github.com/charmbracelet/bubbletea/v2"
 13	"github.com/charmbracelet/crush/internal/diff"
 14	"github.com/charmbracelet/crush/internal/fsext"
 15	"github.com/charmbracelet/crush/internal/llm/agent"
 16	"github.com/charmbracelet/crush/internal/llm/tools"
 17	"github.com/charmbracelet/crush/internal/message"
 18	"github.com/charmbracelet/crush/internal/permission"
 19	"github.com/charmbracelet/crush/internal/tui/components/anim"
 20	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 21	"github.com/charmbracelet/crush/internal/tui/styles"
 22	"github.com/charmbracelet/crush/internal/tui/util"
 23	"github.com/charmbracelet/lipgloss/v2"
 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     anim.Spinner // 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		Static:      isReduceAnimations(),
124		Size:        15,
125		Label:       "Working",
126		GradColorA:  t.Primary,
127		GradColorB:  t.Secondary,
128		LabelColor:  t.FgBase,
129		CycleColors: true,
130	})
131	if m.isNested {
132		m.anim = anim.New(anim.Settings{
133			Static:      isReduceAnimations(),
134			Size:        10,
135			GradColorA:  t.Primary,
136			GradColorB:  t.Secondary,
137			CycleColors: true,
138		})
139	}
140	return m
141}
142
143// Init initializes the tool call component and starts animations if needed.
144// Returns a command to start the animation for pending tool calls.
145func (m *toolCallCmp) Init() tea.Cmd {
146	m.spinning = m.shouldSpin()
147	return m.anim.Init()
148}
149
150// Update handles incoming messages and updates the component state.
151// Manages animation updates for pending tool calls.
152func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
153	switch msg := msg.(type) {
154	case anim.StepMsg:
155		var cmds []tea.Cmd
156		for i, nested := range m.nestedToolCalls {
157			if nested.Spinning() {
158				u, cmd := nested.Update(msg)
159				m.nestedToolCalls[i] = u.(ToolCallCmp)
160				cmds = append(cmds, cmd)
161			}
162		}
163		if m.spinning {
164			var cmd tea.Cmd
165			m.anim, cmd = m.anim.Update(msg)
166			cmds = append(cmds, cmd)
167		}
168		return m, tea.Batch(cmds...)
169	case tea.KeyPressMsg:
170		if key.Matches(msg, CopyKey) {
171			return m, m.copyTool()
172		}
173	}
174	return m, nil
175}
176
177// View renders the tool call component based on its current state.
178// Shows either a pending animation or the tool-specific rendered result.
179func (m *toolCallCmp) View() string {
180	box := m.style()
181
182	if !m.call.Finished && !m.cancelled {
183		return box.Render(m.renderPending())
184	}
185
186	r := registry.lookup(m.call.Name)
187
188	if m.isNested {
189		return box.Render(r.Render(m))
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:** %s", (time.Duration(params.Timeout)*time.Second).String()))
299			}
300			return strings.Join(parts, "\n")
301		}
302	case tools.GrepToolName:
303		var params tools.GrepParams
304		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
305			var parts []string
306			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
307			if params.Path != "" {
308				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
309			}
310			if params.Include != "" {
311				parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
312			}
313			if params.LiteralText {
314				parts = append(parts, "**Literal:** true")
315			}
316			return strings.Join(parts, "\n")
317		}
318	case tools.GlobToolName:
319		var params tools.GlobParams
320		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
321			var parts []string
322			parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
323			if params.Path != "" {
324				parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
325			}
326			return strings.Join(parts, "\n")
327		}
328	case tools.LSToolName:
329		var params tools.LSParams
330		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
331			path := params.Path
332			if path == "" {
333				path = "."
334			}
335			return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
336		}
337	case tools.DownloadToolName:
338		var params tools.DownloadParams
339		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
340			var parts []string
341			parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
342			parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
343			if params.Timeout > 0 {
344				parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
345			}
346			return strings.Join(parts, "\n")
347		}
348	case tools.SourcegraphToolName:
349		var params tools.SourcegraphParams
350		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
351			var parts []string
352			parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
353			if params.Count > 0 {
354				parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
355			}
356			if params.ContextWindow > 0 {
357				parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
358			}
359			return strings.Join(parts, "\n")
360		}
361	case tools.DiagnosticsToolName:
362		return "**Project:** diagnostics"
363	case agent.AgentToolName:
364		var params agent.AgentParams
365		if json.Unmarshal([]byte(m.call.Input), &params) == nil {
366			return fmt.Sprintf("**Task:**\n%s", params.Prompt)
367		}
368	}
369
370	var params map[string]any
371	if json.Unmarshal([]byte(m.call.Input), &params) == nil {
372		var parts []string
373		for key, value := range params {
374			displayKey := strings.ReplaceAll(key, "_", " ")
375			if len(displayKey) > 0 {
376				displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
377			}
378			parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
379		}
380		return strings.Join(parts, "\n")
381	}
382
383	return ""
384}
385
386func (m *toolCallCmp) formatResultForCopy() string {
387	switch m.call.Name {
388	case tools.BashToolName:
389		return m.formatBashResultForCopy()
390	case tools.ViewToolName:
391		return m.formatViewResultForCopy()
392	case tools.EditToolName:
393		return m.formatEditResultForCopy()
394	case tools.MultiEditToolName:
395		return m.formatMultiEditResultForCopy()
396	case tools.WriteToolName:
397		return m.formatWriteResultForCopy()
398	case tools.FetchToolName:
399		return m.formatFetchResultForCopy()
400	case agent.AgentToolName:
401		return m.formatAgentResultForCopy()
402	case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
403		return fmt.Sprintf("```\n%s\n```", m.result.Content)
404	default:
405		return m.result.Content
406	}
407}
408
409func (m *toolCallCmp) formatBashResultForCopy() string {
410	var meta tools.BashResponseMetadata
411	if m.result.Metadata != "" {
412		json.Unmarshal([]byte(m.result.Metadata), &meta)
413	}
414
415	output := meta.Output
416	if output == "" && m.result.Content != tools.BashNoOutput {
417		output = m.result.Content
418	}
419
420	if output == "" {
421		return ""
422	}
423
424	return fmt.Sprintf("```bash\n%s\n```", output)
425}
426
427func (m *toolCallCmp) formatViewResultForCopy() string {
428	var meta tools.ViewResponseMetadata
429	if m.result.Metadata != "" {
430		json.Unmarshal([]byte(m.result.Metadata), &meta)
431	}
432
433	if meta.Content == "" {
434		return m.result.Content
435	}
436
437	lang := ""
438	if meta.FilePath != "" {
439		ext := strings.ToLower(filepath.Ext(meta.FilePath))
440		switch ext {
441		case ".go":
442			lang = "go"
443		case ".js", ".mjs":
444			lang = "javascript"
445		case ".ts":
446			lang = "typescript"
447		case ".py":
448			lang = "python"
449		case ".rs":
450			lang = "rust"
451		case ".java":
452			lang = "java"
453		case ".c":
454			lang = "c"
455		case ".cpp", ".cc", ".cxx":
456			lang = "cpp"
457		case ".sh", ".bash":
458			lang = "bash"
459		case ".json":
460			lang = "json"
461		case ".yaml", ".yml":
462			lang = "yaml"
463		case ".xml":
464			lang = "xml"
465		case ".html":
466			lang = "html"
467		case ".css":
468			lang = "css"
469		case ".md":
470			lang = "markdown"
471		}
472	}
473
474	var result strings.Builder
475	if lang != "" {
476		result.WriteString(fmt.Sprintf("```%s\n", lang))
477	} else {
478		result.WriteString("```\n")
479	}
480	result.WriteString(meta.Content)
481	result.WriteString("\n```")
482
483	return result.String()
484}
485
486func (m *toolCallCmp) formatEditResultForCopy() string {
487	var meta tools.EditResponseMetadata
488	if m.result.Metadata == "" {
489		return m.result.Content
490	}
491
492	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
493		return m.result.Content
494	}
495
496	var params tools.EditParams
497	json.Unmarshal([]byte(m.call.Input), &params)
498
499	var result strings.Builder
500
501	if meta.OldContent != "" || meta.NewContent != "" {
502		fileName := params.FilePath
503		if fileName != "" {
504			fileName = fsext.PrettyPath(fileName)
505		}
506		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
507
508		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
509		result.WriteString("```diff\n")
510		result.WriteString(diffContent)
511		result.WriteString("\n```")
512	}
513
514	return result.String()
515}
516
517func (m *toolCallCmp) formatMultiEditResultForCopy() string {
518	var meta tools.MultiEditResponseMetadata
519	if m.result.Metadata == "" {
520		return m.result.Content
521	}
522
523	if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
524		return m.result.Content
525	}
526
527	var params tools.MultiEditParams
528	json.Unmarshal([]byte(m.call.Input), &params)
529
530	var result strings.Builder
531	if meta.OldContent != "" || meta.NewContent != "" {
532		fileName := params.FilePath
533		if fileName != "" {
534			fileName = fsext.PrettyPath(fileName)
535		}
536		diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
537
538		result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
539		result.WriteString("```diff\n")
540		result.WriteString(diffContent)
541		result.WriteString("\n```")
542	}
543
544	return result.String()
545}
546
547func (m *toolCallCmp) formatWriteResultForCopy() string {
548	var params tools.WriteParams
549	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
550		return m.result.Content
551	}
552
553	lang := ""
554	if params.FilePath != "" {
555		ext := strings.ToLower(filepath.Ext(params.FilePath))
556		switch ext {
557		case ".go":
558			lang = "go"
559		case ".js", ".mjs":
560			lang = "javascript"
561		case ".ts":
562			lang = "typescript"
563		case ".py":
564			lang = "python"
565		case ".rs":
566			lang = "rust"
567		case ".java":
568			lang = "java"
569		case ".c":
570			lang = "c"
571		case ".cpp", ".cc", ".cxx":
572			lang = "cpp"
573		case ".sh", ".bash":
574			lang = "bash"
575		case ".json":
576			lang = "json"
577		case ".yaml", ".yml":
578			lang = "yaml"
579		case ".xml":
580			lang = "xml"
581		case ".html":
582			lang = "html"
583		case ".css":
584			lang = "css"
585		case ".md":
586			lang = "markdown"
587		}
588	}
589
590	var result strings.Builder
591	result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
592	if lang != "" {
593		result.WriteString(fmt.Sprintf("```%s\n", lang))
594	} else {
595		result.WriteString("```\n")
596	}
597	result.WriteString(params.Content)
598	result.WriteString("\n```")
599
600	return result.String()
601}
602
603func (m *toolCallCmp) formatFetchResultForCopy() string {
604	var params tools.FetchParams
605	if json.Unmarshal([]byte(m.call.Input), &params) != nil {
606		return m.result.Content
607	}
608
609	var result strings.Builder
610	if params.URL != "" {
611		result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
612	}
613
614	switch params.Format {
615	case "html":
616		result.WriteString("```html\n")
617	case "text":
618		result.WriteString("```\n")
619	default: // markdown
620		result.WriteString("```markdown\n")
621	}
622	result.WriteString(m.result.Content)
623	result.WriteString("\n```")
624
625	return result.String()
626}
627
628func (m *toolCallCmp) formatAgentResultForCopy() string {
629	var result strings.Builder
630
631	if len(m.nestedToolCalls) > 0 {
632		result.WriteString("### Nested Tool Calls:\n")
633		for i, nestedCall := range m.nestedToolCalls {
634			nestedContent := nestedCall.(*toolCallCmp).formatToolForCopy()
635			indentedContent := strings.ReplaceAll(nestedContent, "\n", "\n  ")
636			result.WriteString(fmt.Sprintf("%d. %s\n", i+1, indentedContent))
637			if i < len(m.nestedToolCalls)-1 {
638				result.WriteString("\n")
639			}
640		}
641
642		if m.result.Content != "" {
643			result.WriteString("\n### Final Result:\n")
644		}
645	}
646
647	if m.result.Content != "" {
648		result.WriteString(fmt.Sprintf("```markdown\n%s\n```", m.result.Content))
649	}
650
651	return result.String()
652}
653
654// SetToolCall updates the tool call data and stops spinning if finished
655func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
656	m.call = call
657	if m.call.Finished {
658		m.spinning = false
659	}
660}
661
662// ParentMessageID returns the ID of the message that initiated this tool call
663func (m *toolCallCmp) ParentMessageID() string {
664	return m.parentMessageID
665}
666
667// SetToolResult updates the tool result and stops the spinning animation
668func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
669	m.result = result
670	m.spinning = false
671}
672
673// GetToolCall returns the current tool call data
674func (m *toolCallCmp) GetToolCall() message.ToolCall {
675	return m.call
676}
677
678// GetToolResult returns the current tool result data
679func (m *toolCallCmp) GetToolResult() message.ToolResult {
680	return m.result
681}
682
683// GetNestedToolCalls returns the nested tool calls
684func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
685	return m.nestedToolCalls
686}
687
688// SetNestedToolCalls sets the nested tool calls
689func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
690	m.nestedToolCalls = calls
691	for _, nested := range m.nestedToolCalls {
692		nested.SetSize(m.width, 0)
693	}
694}
695
696// SetIsNested sets whether this tool call is nested within another
697func (m *toolCallCmp) SetIsNested(isNested bool) {
698	m.isNested = isNested
699}
700
701// Rendering methods
702
703// renderPending displays the tool name with a loading animation for pending tool calls
704func (m *toolCallCmp) renderPending() string {
705	t := styles.CurrentTheme()
706	icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
707	if m.isNested {
708		tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
709		return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
710	}
711	tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
712	return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
713}
714
715// style returns the lipgloss style for the tool call component.
716// Applies muted colors and focus-dependent border styles.
717func (m *toolCallCmp) style() lipgloss.Style {
718	t := styles.CurrentTheme()
719
720	if m.isNested {
721		return t.S().Muted
722	}
723	style := t.S().Muted.PaddingLeft(4)
724
725	if m.focused {
726		style = style.PaddingLeft(3).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
727	}
728	return style
729}
730
731// textWidth calculates the available width for text content,
732// accounting for borders and padding
733func (m *toolCallCmp) textWidth() int {
734	if m.isNested {
735		return m.width - 6
736	}
737	return m.width - 5 // take into account the border and PaddingLeft
738}
739
740// fit truncates content to fit within the specified width with ellipsis
741func (m *toolCallCmp) fit(content string, width int) string {
742	t := styles.CurrentTheme()
743	lineStyle := t.S().Muted
744	dots := lineStyle.Render("…")
745	return ansi.Truncate(content, width, dots)
746}
747
748// Focus management methods
749
750// Blur removes focus from the tool call component
751func (m *toolCallCmp) Blur() tea.Cmd {
752	m.focused = false
753	return nil
754}
755
756// Focus sets focus on the tool call component
757func (m *toolCallCmp) Focus() tea.Cmd {
758	m.focused = true
759	return nil
760}
761
762// IsFocused returns whether the tool call component is currently focused
763func (m *toolCallCmp) IsFocused() bool {
764	return m.focused
765}
766
767// Size management methods
768
769// GetSize returns the current dimensions of the tool call component
770func (m *toolCallCmp) GetSize() (int, int) {
771	return m.width, 0
772}
773
774// SetSize updates the width of the tool call component for text wrapping
775func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
776	m.width = width
777	for _, nested := range m.nestedToolCalls {
778		nested.SetSize(width, height)
779	}
780	return nil
781}
782
783// shouldSpin determines whether the tool call should show a loading animation.
784// Returns true if the tool call is not finished or if the result doesn't match the call ID.
785func (m *toolCallCmp) shouldSpin() bool {
786	return !m.call.Finished && !m.cancelled
787}
788
789// Spinning returns whether the tool call is currently showing a loading animation
790func (m *toolCallCmp) Spinning() bool {
791	if m.spinning {
792		return true
793	}
794	for _, nested := range m.nestedToolCalls {
795		if nested.Spinning() {
796			return true
797		}
798	}
799	return m.spinning
800}
801
802func (m *toolCallCmp) ID() string {
803	return m.call.ID
804}
805
806// SetPermissionRequested marks that a permission request was made for this tool call
807func (m *toolCallCmp) SetPermissionRequested() {
808	m.permissionRequested = true
809}
810
811// SetPermissionGranted marks that permission was granted for this tool call
812func (m *toolCallCmp) SetPermissionGranted() {
813	m.permissionGranted = true
814}