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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms) == 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), ¶ms)
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), ¶ms)
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), ¶ms) != 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), ¶ms) != 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}