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 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 anim.Spinner // 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 Static: isReduceAnimations(),
124 Size: 15,
125 Label: "Working",
126 GradColorA: t.Primary,
127 GradColorB: t.Secondary,
128 LabelColor: t.FgBase,
129 CycleColors: true,
130 })
131 if m.isNested {
132 m.anim = anim.New(anim.Settings{
133 Static: isReduceAnimations(),
134 Size: 10,
135 GradColorA: t.Primary,
136 GradColorB: t.Secondary,
137 CycleColors: true,
138 })
139 }
140 return m
141}
142
143// Init initializes the tool call component and starts animations if needed.
144// Returns a command to start the animation for pending tool calls.
145func (m *toolCallCmp) Init() tea.Cmd {
146 m.spinning = m.shouldSpin()
147 return m.anim.Init()
148}
149
150// Update handles incoming messages and updates the component state.
151// Manages animation updates for pending tool calls.
152func (m *toolCallCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
153 switch msg := msg.(type) {
154 case anim.StepMsg:
155 var cmds []tea.Cmd
156 for i, nested := range m.nestedToolCalls {
157 if nested.Spinning() {
158 u, cmd := nested.Update(msg)
159 m.nestedToolCalls[i] = u.(ToolCallCmp)
160 cmds = append(cmds, cmd)
161 }
162 }
163 if m.spinning {
164 var cmd tea.Cmd
165 m.anim, cmd = m.anim.Update(msg)
166 cmds = append(cmds, cmd)
167 }
168 return m, tea.Batch(cmds...)
169 case tea.KeyPressMsg:
170 if key.Matches(msg, CopyKey) {
171 return m, m.copyTool()
172 }
173 }
174 return m, nil
175}
176
177// View renders the tool call component based on its current state.
178// Shows either a pending animation or the tool-specific rendered result.
179func (m *toolCallCmp) View() string {
180 box := m.style()
181
182 if !m.call.Finished && !m.cancelled {
183 return box.Render(m.renderPending())
184 }
185
186 r := registry.lookup(m.call.Name)
187
188 if m.isNested {
189 return box.Render(r.Render(m))
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:** %s", (time.Duration(params.Timeout)*time.Second).String()))
299 }
300 return strings.Join(parts, "\n")
301 }
302 case tools.GrepToolName:
303 var params tools.GrepParams
304 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
305 var parts []string
306 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
307 if params.Path != "" {
308 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
309 }
310 if params.Include != "" {
311 parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
312 }
313 if params.LiteralText {
314 parts = append(parts, "**Literal:** true")
315 }
316 return strings.Join(parts, "\n")
317 }
318 case tools.GlobToolName:
319 var params tools.GlobParams
320 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
321 var parts []string
322 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
323 if params.Path != "" {
324 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
325 }
326 return strings.Join(parts, "\n")
327 }
328 case tools.LSToolName:
329 var params tools.LSParams
330 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
331 path := params.Path
332 if path == "" {
333 path = "."
334 }
335 return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
336 }
337 case tools.DownloadToolName:
338 var params tools.DownloadParams
339 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
340 var parts []string
341 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
342 parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
343 if params.Timeout > 0 {
344 parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
345 }
346 return strings.Join(parts, "\n")
347 }
348 case tools.SourcegraphToolName:
349 var params tools.SourcegraphParams
350 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
351 var parts []string
352 parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
353 if params.Count > 0 {
354 parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
355 }
356 if params.ContextWindow > 0 {
357 parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
358 }
359 return strings.Join(parts, "\n")
360 }
361 case tools.DiagnosticsToolName:
362 return "**Project:** diagnostics"
363 case agent.AgentToolName:
364 var params agent.AgentParams
365 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
366 return fmt.Sprintf("**Task:**\n%s", params.Prompt)
367 }
368 }
369
370 var params map[string]any
371 if json.Unmarshal([]byte(m.call.Input), ¶ms) == nil {
372 var parts []string
373 for key, value := range params {
374 displayKey := strings.ReplaceAll(key, "_", " ")
375 if len(displayKey) > 0 {
376 displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
377 }
378 parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
379 }
380 return strings.Join(parts, "\n")
381 }
382
383 return ""
384}
385
386func (m *toolCallCmp) formatResultForCopy() string {
387 switch m.call.Name {
388 case tools.BashToolName:
389 return m.formatBashResultForCopy()
390 case tools.ViewToolName:
391 return m.formatViewResultForCopy()
392 case tools.EditToolName:
393 return m.formatEditResultForCopy()
394 case tools.MultiEditToolName:
395 return m.formatMultiEditResultForCopy()
396 case tools.WriteToolName:
397 return m.formatWriteResultForCopy()
398 case tools.FetchToolName:
399 return m.formatFetchResultForCopy()
400 case agent.AgentToolName:
401 return m.formatAgentResultForCopy()
402 case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName:
403 return fmt.Sprintf("```\n%s\n```", m.result.Content)
404 default:
405 return m.result.Content
406 }
407}
408
409func (m *toolCallCmp) formatBashResultForCopy() string {
410 var meta tools.BashResponseMetadata
411 if m.result.Metadata != "" {
412 json.Unmarshal([]byte(m.result.Metadata), &meta)
413 }
414
415 output := meta.Output
416 if output == "" && m.result.Content != tools.BashNoOutput {
417 output = m.result.Content
418 }
419
420 if output == "" {
421 return ""
422 }
423
424 return fmt.Sprintf("```bash\n%s\n```", output)
425}
426
427func (m *toolCallCmp) formatViewResultForCopy() string {
428 var meta tools.ViewResponseMetadata
429 if m.result.Metadata != "" {
430 json.Unmarshal([]byte(m.result.Metadata), &meta)
431 }
432
433 if meta.Content == "" {
434 return m.result.Content
435 }
436
437 lang := ""
438 if meta.FilePath != "" {
439 ext := strings.ToLower(filepath.Ext(meta.FilePath))
440 switch ext {
441 case ".go":
442 lang = "go"
443 case ".js", ".mjs":
444 lang = "javascript"
445 case ".ts":
446 lang = "typescript"
447 case ".py":
448 lang = "python"
449 case ".rs":
450 lang = "rust"
451 case ".java":
452 lang = "java"
453 case ".c":
454 lang = "c"
455 case ".cpp", ".cc", ".cxx":
456 lang = "cpp"
457 case ".sh", ".bash":
458 lang = "bash"
459 case ".json":
460 lang = "json"
461 case ".yaml", ".yml":
462 lang = "yaml"
463 case ".xml":
464 lang = "xml"
465 case ".html":
466 lang = "html"
467 case ".css":
468 lang = "css"
469 case ".md":
470 lang = "markdown"
471 }
472 }
473
474 var result strings.Builder
475 if lang != "" {
476 result.WriteString(fmt.Sprintf("```%s\n", lang))
477 } else {
478 result.WriteString("```\n")
479 }
480 result.WriteString(meta.Content)
481 result.WriteString("\n```")
482
483 return result.String()
484}
485
486func (m *toolCallCmp) formatEditResultForCopy() string {
487 var meta tools.EditResponseMetadata
488 if m.result.Metadata == "" {
489 return m.result.Content
490 }
491
492 if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
493 return m.result.Content
494 }
495
496 var params tools.EditParams
497 json.Unmarshal([]byte(m.call.Input), ¶ms)
498
499 var result strings.Builder
500
501 if meta.OldContent != "" || meta.NewContent != "" {
502 fileName := params.FilePath
503 if fileName != "" {
504 fileName = fsext.PrettyPath(fileName)
505 }
506 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
507
508 result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
509 result.WriteString("```diff\n")
510 result.WriteString(diffContent)
511 result.WriteString("\n```")
512 }
513
514 return result.String()
515}
516
517func (m *toolCallCmp) formatMultiEditResultForCopy() string {
518 var meta tools.MultiEditResponseMetadata
519 if m.result.Metadata == "" {
520 return m.result.Content
521 }
522
523 if json.Unmarshal([]byte(m.result.Metadata), &meta) != nil {
524 return m.result.Content
525 }
526
527 var params tools.MultiEditParams
528 json.Unmarshal([]byte(m.call.Input), ¶ms)
529
530 var result strings.Builder
531 if meta.OldContent != "" || meta.NewContent != "" {
532 fileName := params.FilePath
533 if fileName != "" {
534 fileName = fsext.PrettyPath(fileName)
535 }
536 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
537
538 result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
539 result.WriteString("```diff\n")
540 result.WriteString(diffContent)
541 result.WriteString("\n```")
542 }
543
544 return result.String()
545}
546
547func (m *toolCallCmp) formatWriteResultForCopy() string {
548 var params tools.WriteParams
549 if json.Unmarshal([]byte(m.call.Input), ¶ms) != nil {
550 return m.result.Content
551 }
552
553 lang := ""
554 if params.FilePath != "" {
555 ext := strings.ToLower(filepath.Ext(params.FilePath))
556 switch ext {
557 case ".go":
558 lang = "go"
559 case ".js", ".mjs":
560 lang = "javascript"
561 case ".ts":
562 lang = "typescript"
563 case ".py":
564 lang = "python"
565 case ".rs":
566 lang = "rust"
567 case ".java":
568 lang = "java"
569 case ".c":
570 lang = "c"
571 case ".cpp", ".cc", ".cxx":
572 lang = "cpp"
573 case ".sh", ".bash":
574 lang = "bash"
575 case ".json":
576 lang = "json"
577 case ".yaml", ".yml":
578 lang = "yaml"
579 case ".xml":
580 lang = "xml"
581 case ".html":
582 lang = "html"
583 case ".css":
584 lang = "css"
585 case ".md":
586 lang = "markdown"
587 }
588 }
589
590 var result strings.Builder
591 result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
592 if lang != "" {
593 result.WriteString(fmt.Sprintf("```%s\n", lang))
594 } else {
595 result.WriteString("```\n")
596 }
597 result.WriteString(params.Content)
598 result.WriteString("\n```")
599
600 return result.String()
601}
602
603func (m *toolCallCmp) formatFetchResultForCopy() string {
604 var params tools.FetchParams
605 if json.Unmarshal([]byte(m.call.Input), ¶ms) != nil {
606 return m.result.Content
607 }
608
609 var result strings.Builder
610 if params.URL != "" {
611 result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
612 }
613
614 switch params.Format {
615 case "html":
616 result.WriteString("```html\n")
617 case "text":
618 result.WriteString("```\n")
619 default: // markdown
620 result.WriteString("```markdown\n")
621 }
622 result.WriteString(m.result.Content)
623 result.WriteString("\n```")
624
625 return result.String()
626}
627
628func (m *toolCallCmp) formatAgentResultForCopy() string {
629 var result strings.Builder
630
631 if len(m.nestedToolCalls) > 0 {
632 result.WriteString("### Nested Tool Calls:\n")
633 for i, nestedCall := range m.nestedToolCalls {
634 nestedContent := nestedCall.(*toolCallCmp).formatToolForCopy()
635 indentedContent := strings.ReplaceAll(nestedContent, "\n", "\n ")
636 result.WriteString(fmt.Sprintf("%d. %s\n", i+1, indentedContent))
637 if i < len(m.nestedToolCalls)-1 {
638 result.WriteString("\n")
639 }
640 }
641
642 if m.result.Content != "" {
643 result.WriteString("\n### Final Result:\n")
644 }
645 }
646
647 if m.result.Content != "" {
648 result.WriteString(fmt.Sprintf("```markdown\n%s\n```", m.result.Content))
649 }
650
651 return result.String()
652}
653
654// SetToolCall updates the tool call data and stops spinning if finished
655func (m *toolCallCmp) SetToolCall(call message.ToolCall) {
656 m.call = call
657 if m.call.Finished {
658 m.spinning = false
659 }
660}
661
662// ParentMessageID returns the ID of the message that initiated this tool call
663func (m *toolCallCmp) ParentMessageID() string {
664 return m.parentMessageID
665}
666
667// SetToolResult updates the tool result and stops the spinning animation
668func (m *toolCallCmp) SetToolResult(result message.ToolResult) {
669 m.result = result
670 m.spinning = false
671}
672
673// GetToolCall returns the current tool call data
674func (m *toolCallCmp) GetToolCall() message.ToolCall {
675 return m.call
676}
677
678// GetToolResult returns the current tool result data
679func (m *toolCallCmp) GetToolResult() message.ToolResult {
680 return m.result
681}
682
683// GetNestedToolCalls returns the nested tool calls
684func (m *toolCallCmp) GetNestedToolCalls() []ToolCallCmp {
685 return m.nestedToolCalls
686}
687
688// SetNestedToolCalls sets the nested tool calls
689func (m *toolCallCmp) SetNestedToolCalls(calls []ToolCallCmp) {
690 m.nestedToolCalls = calls
691 for _, nested := range m.nestedToolCalls {
692 nested.SetSize(m.width, 0)
693 }
694}
695
696// SetIsNested sets whether this tool call is nested within another
697func (m *toolCallCmp) SetIsNested(isNested bool) {
698 m.isNested = isNested
699}
700
701// Rendering methods
702
703// renderPending displays the tool name with a loading animation for pending tool calls
704func (m *toolCallCmp) renderPending() string {
705 t := styles.CurrentTheme()
706 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
707 if m.isNested {
708 tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name))
709 return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
710 }
711 tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name))
712 return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View())
713}
714
715// style returns the lipgloss style for the tool call component.
716// Applies muted colors and focus-dependent border styles.
717func (m *toolCallCmp) style() lipgloss.Style {
718 t := styles.CurrentTheme()
719
720 if m.isNested {
721 return t.S().Muted
722 }
723 style := t.S().Muted.PaddingLeft(4)
724
725 if m.focused {
726 style = style.PaddingLeft(3).BorderStyle(focusedMessageBorder).BorderLeft(true).BorderForeground(t.GreenDark)
727 }
728 return style
729}
730
731// textWidth calculates the available width for text content,
732// accounting for borders and padding
733func (m *toolCallCmp) textWidth() int {
734 if m.isNested {
735 return m.width - 6
736 }
737 return m.width - 5 // take into account the border and PaddingLeft
738}
739
740// fit truncates content to fit within the specified width with ellipsis
741func (m *toolCallCmp) fit(content string, width int) string {
742 t := styles.CurrentTheme()
743 lineStyle := t.S().Muted
744 dots := lineStyle.Render("…")
745 return ansi.Truncate(content, width, dots)
746}
747
748// Focus management methods
749
750// Blur removes focus from the tool call component
751func (m *toolCallCmp) Blur() tea.Cmd {
752 m.focused = false
753 return nil
754}
755
756// Focus sets focus on the tool call component
757func (m *toolCallCmp) Focus() tea.Cmd {
758 m.focused = true
759 return nil
760}
761
762// IsFocused returns whether the tool call component is currently focused
763func (m *toolCallCmp) IsFocused() bool {
764 return m.focused
765}
766
767// Size management methods
768
769// GetSize returns the current dimensions of the tool call component
770func (m *toolCallCmp) GetSize() (int, int) {
771 return m.width, 0
772}
773
774// SetSize updates the width of the tool call component for text wrapping
775func (m *toolCallCmp) SetSize(width int, height int) tea.Cmd {
776 m.width = width
777 for _, nested := range m.nestedToolCalls {
778 nested.SetSize(width, height)
779 }
780 return nil
781}
782
783// shouldSpin determines whether the tool call should show a loading animation.
784// Returns true if the tool call is not finished or if the result doesn't match the call ID.
785func (m *toolCallCmp) shouldSpin() bool {
786 return !m.call.Finished && !m.cancelled
787}
788
789// Spinning returns whether the tool call is currently showing a loading animation
790func (m *toolCallCmp) Spinning() bool {
791 if m.spinning {
792 return true
793 }
794 for _, nested := range m.nestedToolCalls {
795 if nested.Spinning() {
796 return true
797 }
798 }
799 return m.spinning
800}
801
802func (m *toolCallCmp) ID() string {
803 return m.call.ID
804}
805
806// SetPermissionRequested marks that a permission request was made for this tool call
807func (m *toolCallCmp) SetPermissionRequested() {
808 m.permissionRequested = true
809}
810
811// SetPermissionGranted marks that permission was granted for this tool call
812func (m *toolCallCmp) SetPermissionGranted() {
813 m.permissionGranted = true
814}