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