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