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