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