1package chat
2
3import (
4 "encoding/json"
5 "fmt"
6 "path/filepath"
7 "strings"
8 "time"
9
10 tea "charm.land/bubbletea/v2"
11 "charm.land/lipgloss/v2"
12 "charm.land/lipgloss/v2/tree"
13 "github.com/charmbracelet/crush/internal/agent"
14 "github.com/charmbracelet/crush/internal/agent/tools"
15 "github.com/charmbracelet/crush/internal/diff"
16 "github.com/charmbracelet/crush/internal/fsext"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/ui/anim"
19 "github.com/charmbracelet/crush/internal/ui/common"
20 "github.com/charmbracelet/crush/internal/ui/styles"
21 "github.com/charmbracelet/x/ansi"
22)
23
24// responseContextHeight limits the number of lines displayed in tool output.
25const responseContextHeight = 10
26
27// toolBodyLeftPaddingTotal represents the padding that should be applied to each tool body
28const toolBodyLeftPaddingTotal = 2
29
30// ToolStatus represents the current state of a tool call.
31type ToolStatus int
32
33const (
34 ToolStatusAwaitingPermission ToolStatus = iota
35 ToolStatusRunning
36 ToolStatusSuccess
37 ToolStatusError
38 ToolStatusCanceled
39)
40
41// ToolMessageItem represents a tool call message in the chat UI.
42type ToolMessageItem interface {
43 MessageItem
44
45 ToolCall() message.ToolCall
46 SetToolCall(tc message.ToolCall)
47 SetResult(res *message.ToolResult)
48 MessageID() string
49 SetMessageID(id string)
50 SetStatus(status ToolStatus)
51 Status() ToolStatus
52}
53
54// Compactable is an interface for tool items that can render in a compacted mode.
55// When compact mode is enabled, tools render as a compact single-line header.
56type Compactable interface {
57 SetCompact(compact bool)
58}
59
60// SpinningState contains the state passed to SpinningFunc for custom spinning logic.
61type SpinningState struct {
62 ToolCall message.ToolCall
63 Result *message.ToolResult
64 Status ToolStatus
65}
66
67// IsCanceled returns true if the tool status is canceled.
68func (s *SpinningState) IsCanceled() bool {
69 return s.Status == ToolStatusCanceled
70}
71
72// HasResult returns true if the result is not nil.
73func (s *SpinningState) HasResult() bool {
74 return s.Result != nil
75}
76
77// SpinningFunc is a function type for custom spinning logic.
78// Returns true if the tool should show the spinning animation.
79type SpinningFunc func(state SpinningState) bool
80
81// DefaultToolRenderContext implements the default [ToolRenderer] interface.
82type DefaultToolRenderContext struct{}
83
84// RenderTool implements the [ToolRenderer] interface.
85func (d *DefaultToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
86 return "TODO: Implement Tool Renderer For: " + opts.ToolCall.Name
87}
88
89// ToolRenderOpts contains the data needed to render a tool call.
90type ToolRenderOpts struct {
91 ToolCall message.ToolCall
92 Result *message.ToolResult
93 Anim *anim.Anim
94 ExpandedContent bool
95 Compact bool
96 IsSpinning bool
97 Status ToolStatus
98}
99
100// IsPending returns true if the tool call is still pending (not finished and
101// not canceled).
102func (o *ToolRenderOpts) IsPending() bool {
103 return !o.ToolCall.Finished && !o.IsCanceled()
104}
105
106// IsCanceled returns true if the tool status is canceled.
107func (o *ToolRenderOpts) IsCanceled() bool {
108 return o.Status == ToolStatusCanceled
109}
110
111// HasResult returns true if the result is not nil.
112func (o *ToolRenderOpts) HasResult() bool {
113 return o.Result != nil
114}
115
116// HasEmptyResult returns true if the result is nil or has empty content.
117func (o *ToolRenderOpts) HasEmptyResult() bool {
118 return o.Result == nil || o.Result.Content == ""
119}
120
121// ToolRenderer represents an interface for rendering tool calls.
122type ToolRenderer interface {
123 RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string
124}
125
126// ToolRendererFunc is a function type that implements the [ToolRenderer] interface.
127type ToolRendererFunc func(sty *styles.Styles, width int, opts *ToolRenderOpts) string
128
129// RenderTool implements the ToolRenderer interface.
130func (f ToolRendererFunc) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
131 return f(sty, width, opts)
132}
133
134// baseToolMessageItem represents a tool call message that can be displayed in the UI.
135type baseToolMessageItem struct {
136 *highlightableMessageItem
137 *cachedMessageItem
138 *focusableMessageItem
139
140 toolRenderer ToolRenderer
141 toolCall message.ToolCall
142 result *message.ToolResult
143 messageID string
144 status ToolStatus
145 // we use this so we can efficiently cache
146 // tools that have a capped width (e.x bash.. and others)
147 hasCappedWidth bool
148 // isCompact indicates this tool should render in compact mode.
149 isCompact bool
150 // spinningFunc allows tools to override the default spinning logic.
151 // If nil, uses the default: !toolCall.Finished && !canceled.
152 spinningFunc SpinningFunc
153
154 sty *styles.Styles
155 anim *anim.Anim
156 expandedContent bool
157}
158
159// newBaseToolMessageItem is the internal constructor for base tool message items.
160func newBaseToolMessageItem(
161 sty *styles.Styles,
162 toolCall message.ToolCall,
163 result *message.ToolResult,
164 toolRenderer ToolRenderer,
165 canceled bool,
166) *baseToolMessageItem {
167 // we only do full width for diffs (as far as I know)
168 hasCappedWidth := toolCall.Name != tools.EditToolName && toolCall.Name != tools.MultiEditToolName
169
170 status := ToolStatusRunning
171 if canceled {
172 status = ToolStatusCanceled
173 }
174
175 t := &baseToolMessageItem{
176 highlightableMessageItem: defaultHighlighter(sty),
177 cachedMessageItem: &cachedMessageItem{},
178 focusableMessageItem: &focusableMessageItem{},
179 sty: sty,
180 toolRenderer: toolRenderer,
181 toolCall: toolCall,
182 result: result,
183 status: status,
184 hasCappedWidth: hasCappedWidth,
185 }
186 t.anim = anim.New(anim.Settings{
187 ID: toolCall.ID,
188 Size: 15,
189 GradColorA: sty.Primary,
190 GradColorB: sty.Secondary,
191 LabelColor: sty.FgBase,
192 CycleColors: true,
193 })
194
195 return t
196}
197
198// NewToolMessageItem creates a new [ToolMessageItem] based on the tool call name.
199//
200// It returns a specific tool message item type if implemented, otherwise it
201// returns a generic tool message item. The messageID is the ID of the assistant
202// message containing this tool call.
203func NewToolMessageItem(
204 sty *styles.Styles,
205 messageID string,
206 toolCall message.ToolCall,
207 result *message.ToolResult,
208 canceled bool,
209) ToolMessageItem {
210 var item ToolMessageItem
211 switch toolCall.Name {
212 case tools.BashToolName:
213 item = NewBashToolMessageItem(sty, toolCall, result, canceled)
214 case tools.JobOutputToolName:
215 item = NewJobOutputToolMessageItem(sty, toolCall, result, canceled)
216 case tools.JobKillToolName:
217 item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
218 case tools.ViewToolName:
219 item = NewViewToolMessageItem(sty, toolCall, result, canceled)
220 case tools.WriteToolName:
221 item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
222 case tools.EditToolName:
223 item = NewEditToolMessageItem(sty, toolCall, result, canceled)
224 case tools.MultiEditToolName:
225 item = NewMultiEditToolMessageItem(sty, toolCall, result, canceled)
226 case tools.GlobToolName:
227 item = NewGlobToolMessageItem(sty, toolCall, result, canceled)
228 case tools.GrepToolName:
229 item = NewGrepToolMessageItem(sty, toolCall, result, canceled)
230 case tools.LSToolName:
231 item = NewLSToolMessageItem(sty, toolCall, result, canceled)
232 case tools.DownloadToolName:
233 item = NewDownloadToolMessageItem(sty, toolCall, result, canceled)
234 case tools.FetchToolName:
235 item = NewFetchToolMessageItem(sty, toolCall, result, canceled)
236 case tools.SourcegraphToolName:
237 item = NewSourcegraphToolMessageItem(sty, toolCall, result, canceled)
238 case tools.DiagnosticsToolName:
239 item = NewDiagnosticsToolMessageItem(sty, toolCall, result, canceled)
240 case agent.AgentToolName:
241 item = NewAgentToolMessageItem(sty, toolCall, result, canceled)
242 case tools.AgenticFetchToolName:
243 item = NewAgenticFetchToolMessageItem(sty, toolCall, result, canceled)
244 case tools.WebFetchToolName:
245 item = NewWebFetchToolMessageItem(sty, toolCall, result, canceled)
246 case tools.WebSearchToolName:
247 item = NewWebSearchToolMessageItem(sty, toolCall, result, canceled)
248 case tools.TodosToolName:
249 item = NewTodosToolMessageItem(sty, toolCall, result, canceled)
250 case tools.ReferencesToolName:
251 item = NewReferencesToolMessageItem(sty, toolCall, result, canceled)
252 case tools.LSPRestartToolName:
253 item = NewLSPRestartToolMessageItem(sty, toolCall, result, canceled)
254 default:
255 if strings.HasPrefix(toolCall.Name, "mcp_") {
256 item = NewMCPToolMessageItem(sty, toolCall, result, canceled)
257 } else {
258 // TODO: Implement other tool items
259 item = newBaseToolMessageItem(
260 sty,
261 toolCall,
262 result,
263 &DefaultToolRenderContext{},
264 canceled,
265 )
266 }
267 }
268 item.SetMessageID(messageID)
269 return item
270}
271
272// SetCompact implements the Compactable interface.
273func (t *baseToolMessageItem) SetCompact(compact bool) {
274 t.isCompact = compact
275 t.clearCache()
276}
277
278// ID returns the unique identifier for this tool message item.
279func (t *baseToolMessageItem) ID() string {
280 return t.toolCall.ID
281}
282
283// StartAnimation starts the assistant message animation if it should be spinning.
284func (t *baseToolMessageItem) StartAnimation() tea.Cmd {
285 if !t.isSpinning() {
286 return nil
287 }
288 return t.anim.Start()
289}
290
291// Animate progresses the assistant message animation if it should be spinning.
292func (t *baseToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd {
293 if !t.isSpinning() {
294 return nil
295 }
296 return t.anim.Animate(msg)
297}
298
299// RawRender implements [MessageItem].
300func (t *baseToolMessageItem) RawRender(width int) string {
301 toolItemWidth := width - messageLeftPaddingTotal
302 if t.hasCappedWidth {
303 toolItemWidth = cappedMessageWidth(width)
304 }
305
306 content, height, ok := t.getCachedRender(toolItemWidth)
307 // if we are spinning or there is no cache rerender
308 if !ok || t.isSpinning() {
309 content = t.toolRenderer.RenderTool(t.sty, toolItemWidth, &ToolRenderOpts{
310 ToolCall: t.toolCall,
311 Result: t.result,
312 Anim: t.anim,
313 ExpandedContent: t.expandedContent,
314 Compact: t.isCompact,
315 IsSpinning: t.isSpinning(),
316 Status: t.computeStatus(),
317 })
318 height = lipgloss.Height(content)
319 // cache the rendered content
320 t.setCachedRender(content, toolItemWidth, height)
321 }
322
323 return t.renderHighlighted(content, toolItemWidth, height)
324}
325
326// Render renders the tool message item at the given width.
327func (t *baseToolMessageItem) Render(width int) string {
328 style := t.sty.Chat.Message.ToolCallBlurred
329 if t.focused {
330 style = t.sty.Chat.Message.ToolCallFocused
331 }
332
333 if t.isCompact {
334 style = t.sty.Chat.Message.ToolCallCompact
335 }
336
337 return style.Render(t.RawRender(width))
338}
339
340// ToolCall returns the tool call associated with this message item.
341func (t *baseToolMessageItem) ToolCall() message.ToolCall {
342 return t.toolCall
343}
344
345// SetToolCall sets the tool call associated with this message item.
346func (t *baseToolMessageItem) SetToolCall(tc message.ToolCall) {
347 t.toolCall = tc
348 t.clearCache()
349}
350
351// SetResult sets the tool result associated with this message item.
352func (t *baseToolMessageItem) SetResult(res *message.ToolResult) {
353 t.result = res
354 t.clearCache()
355}
356
357// MessageID returns the ID of the message containing this tool call.
358func (t *baseToolMessageItem) MessageID() string {
359 return t.messageID
360}
361
362// SetMessageID sets the ID of the message containing this tool call.
363func (t *baseToolMessageItem) SetMessageID(id string) {
364 t.messageID = id
365}
366
367// SetStatus sets the tool status.
368func (t *baseToolMessageItem) SetStatus(status ToolStatus) {
369 t.status = status
370 t.clearCache()
371}
372
373// Status returns the current tool status.
374func (t *baseToolMessageItem) Status() ToolStatus {
375 return t.status
376}
377
378// computeStatus computes the effective status considering the result.
379func (t *baseToolMessageItem) computeStatus() ToolStatus {
380 if t.result != nil {
381 if t.result.IsError {
382 return ToolStatusError
383 }
384 return ToolStatusSuccess
385 }
386 return t.status
387}
388
389// isSpinning returns true if the tool should show animation.
390func (t *baseToolMessageItem) isSpinning() bool {
391 if t.spinningFunc != nil {
392 return t.spinningFunc(SpinningState{
393 ToolCall: t.toolCall,
394 Result: t.result,
395 Status: t.status,
396 })
397 }
398 return !t.toolCall.Finished && t.status != ToolStatusCanceled
399}
400
401// SetSpinningFunc sets a custom function to determine if the tool should spin.
402func (t *baseToolMessageItem) SetSpinningFunc(fn SpinningFunc) {
403 t.spinningFunc = fn
404}
405
406// ToggleExpanded toggles the expanded state of the thinking box.
407func (t *baseToolMessageItem) ToggleExpanded() {
408 t.expandedContent = !t.expandedContent
409 t.clearCache()
410}
411
412// HandleMouseClick implements MouseClickable.
413func (t *baseToolMessageItem) HandleMouseClick(btn ansi.MouseButton, x, y int) bool {
414 if btn != ansi.MouseLeft {
415 return false
416 }
417 t.ToggleExpanded()
418 return true
419}
420
421// HandleKeyEvent implements KeyEventHandler.
422func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
423 if k := key.String(); k == "c" || k == "y" {
424 text := t.formatToolForCopy()
425 return true, common.CopyToClipboard(text, "Tool content copied to clipboard")
426 }
427 return false, nil
428}
429
430// pendingTool renders a tool that is still in progress with an animation.
431func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
432 icon := sty.Tool.IconPending.Render()
433 toolName := sty.Tool.NameNormal.Render(name)
434
435 var animView string
436 if anim != nil {
437 animView = anim.Render()
438 }
439
440 return fmt.Sprintf("%s %s %s", icon, toolName, animView)
441}
442
443// toolEarlyStateContent handles error/cancelled/pending states before content rendering.
444// Returns the rendered output and true if early state was handled.
445func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int) (string, bool) {
446 var msg string
447 switch opts.Status {
448 case ToolStatusError:
449 msg = toolErrorContent(sty, opts.Result, width)
450 case ToolStatusCanceled:
451 msg = sty.Tool.StateCancelled.Render("Canceled.")
452 case ToolStatusAwaitingPermission:
453 msg = sty.Tool.StateWaiting.Render("Requesting permission...")
454 case ToolStatusRunning:
455 msg = sty.Tool.StateWaiting.Render("Waiting for tool response...")
456 default:
457 return "", false
458 }
459 return msg, true
460}
461
462// toolErrorContent formats an error message with ERROR tag.
463func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string {
464 if result == nil {
465 return ""
466 }
467 errContent := strings.ReplaceAll(result.Content, "\n", " ")
468 errTag := sty.Tool.ErrorTag.Render("ERROR")
469 tagWidth := lipgloss.Width(errTag)
470 errContent = ansi.Truncate(errContent, width-tagWidth-3, "…")
471 return fmt.Sprintf("%s %s", errTag, sty.Tool.ErrorMessage.Render(errContent))
472}
473
474// toolIcon returns the status icon for a tool call.
475// toolIcon returns the status icon for a tool call based on its status.
476func toolIcon(sty *styles.Styles, status ToolStatus) string {
477 switch status {
478 case ToolStatusSuccess:
479 return sty.Tool.IconSuccess.String()
480 case ToolStatusError:
481 return sty.Tool.IconError.String()
482 case ToolStatusCanceled:
483 return sty.Tool.IconCancelled.String()
484 default:
485 return sty.Tool.IconPending.String()
486 }
487}
488
489// toolParamList formats parameters as "main (key=value, ...)" with truncation.
490// toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
491func toolParamList(sty *styles.Styles, params []string, width int) string {
492 // minSpaceForMainParam is the min space required for the main param
493 // if this is less that the value set we will only show the main param nothing else
494 const minSpaceForMainParam = 30
495 if len(params) == 0 {
496 return ""
497 }
498
499 mainParam := params[0]
500
501 // Build key=value pairs from remaining params (consecutive key, value pairs).
502 var kvPairs []string
503 for i := 1; i+1 < len(params); i += 2 {
504 if params[i+1] != "" {
505 kvPairs = append(kvPairs, fmt.Sprintf("%s=%s", params[i], params[i+1]))
506 }
507 }
508
509 // Try to include key=value pairs if there's enough space.
510 output := mainParam
511 if len(kvPairs) > 0 {
512 partsStr := strings.Join(kvPairs, ", ")
513 if remaining := width - lipgloss.Width(partsStr) - 3; remaining >= minSpaceForMainParam {
514 output = fmt.Sprintf("%s (%s)", mainParam, partsStr)
515 }
516 }
517
518 if width >= 0 {
519 output = ansi.Truncate(output, width, "…")
520 }
521 return sty.Tool.ParamMain.Render(output)
522}
523
524// toolHeader builds the tool header line: "● ToolName params..."
525func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, nested bool, params ...string) string {
526 icon := toolIcon(sty, status)
527 nameStyle := sty.Tool.NameNormal
528 if nested {
529 nameStyle = sty.Tool.NameNested
530 }
531 toolName := nameStyle.Render(name)
532 prefix := fmt.Sprintf("%s %s ", icon, toolName)
533 prefixWidth := lipgloss.Width(prefix)
534 remainingWidth := width - prefixWidth
535 paramsStr := toolParamList(sty, params, remainingWidth)
536 return prefix + paramsStr
537}
538
539// toolOutputPlainContent renders plain text with optional expansion support.
540func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string {
541 content = strings.ReplaceAll(content, "\r\n", "\n")
542 content = strings.ReplaceAll(content, "\t", " ")
543 content = strings.TrimSpace(content)
544 lines := strings.Split(content, "\n")
545
546 maxLines := responseContextHeight
547 if expanded {
548 maxLines = len(lines) // Show all
549 }
550
551 var out []string
552 for i, ln := range lines {
553 if i >= maxLines {
554 break
555 }
556 ln = " " + ln
557 if lipgloss.Width(ln) > width {
558 ln = ansi.Truncate(ln, width, "…")
559 }
560 out = append(out, sty.Tool.ContentLine.Width(width).Render(ln))
561 }
562
563 wasTruncated := len(lines) > responseContextHeight
564
565 if !expanded && wasTruncated {
566 out = append(out, sty.Tool.ContentTruncation.
567 Width(width).
568 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-responseContextHeight)))
569 }
570
571 return strings.Join(out, "\n")
572}
573
574// toolOutputCodeContent renders code with syntax highlighting and line numbers.
575func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, width int, expanded bool) string {
576 content = strings.ReplaceAll(content, "\r\n", "\n")
577 content = strings.ReplaceAll(content, "\t", " ")
578
579 lines := strings.Split(content, "\n")
580 maxLines := responseContextHeight
581 if expanded {
582 maxLines = len(lines)
583 }
584
585 // Truncate if needed.
586 displayLines := lines
587 if len(lines) > maxLines {
588 displayLines = lines[:maxLines]
589 }
590
591 bg := sty.Tool.ContentCodeBg
592 highlighted, _ := common.SyntaxHighlight(sty, strings.Join(displayLines, "\n"), path, bg)
593 highlightedLines := strings.Split(highlighted, "\n")
594
595 // Calculate line number width.
596 maxLineNumber := len(displayLines) + offset
597 maxDigits := getDigits(maxLineNumber)
598 numFmt := fmt.Sprintf("%%%dd", maxDigits)
599
600 bodyWidth := width - toolBodyLeftPaddingTotal
601 codeWidth := bodyWidth - maxDigits - 4 // -4 for line number padding
602
603 var out []string
604 for i, ln := range highlightedLines {
605 lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
606
607 if lipgloss.Width(ln) > codeWidth {
608 ln = ansi.Truncate(ln, codeWidth, "…")
609 }
610
611 codeLine := sty.Tool.ContentCodeLine.
612 Width(codeWidth).
613 PaddingLeft(2).
614 Render(ln)
615
616 out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
617 }
618
619 // Add truncation message if needed.
620 if len(lines) > maxLines && !expanded {
621 out = append(out, sty.Tool.ContentCodeTruncation.
622 Width(bodyWidth).
623 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
624 )
625 }
626
627 return sty.Tool.Body.Render(strings.Join(out, "\n"))
628}
629
630// toolOutputImageContent renders image data with size info.
631func toolOutputImageContent(sty *styles.Styles, data, mediaType string) string {
632 dataSize := len(data) * 3 / 4
633 sizeStr := formatSize(dataSize)
634
635 loaded := sty.Base.Foreground(sty.Green).Render("Loaded")
636 arrow := sty.Base.Foreground(sty.GreenDark).Render("→")
637 typeStyled := sty.Base.Render(mediaType)
638 sizeStyled := sty.Subtle.Render(sizeStr)
639
640 return sty.Tool.Body.Render(fmt.Sprintf("%s %s %s %s", loaded, arrow, typeStyled, sizeStyled))
641}
642
643// getDigits returns the number of digits in a number.
644func getDigits(n int) int {
645 if n == 0 {
646 return 1
647 }
648 if n < 0 {
649 n = -n
650 }
651 digits := 0
652 for n > 0 {
653 n /= 10
654 digits++
655 }
656 return digits
657}
658
659// formatSize formats byte size into human readable format.
660func formatSize(bytes int) string {
661 const (
662 kb = 1024
663 mb = kb * 1024
664 )
665 switch {
666 case bytes >= mb:
667 return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
668 case bytes >= kb:
669 return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
670 default:
671 return fmt.Sprintf("%d B", bytes)
672 }
673}
674
675// toolOutputDiffContent renders a diff between old and new content.
676func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool) string {
677 bodyWidth := width - toolBodyLeftPaddingTotal
678
679 formatter := common.DiffFormatter(sty).
680 Before(file, oldContent).
681 After(file, newContent).
682 Width(bodyWidth)
683
684 // Use split view for wide terminals.
685 if width > maxTextWidth {
686 formatter = formatter.Split()
687 }
688
689 formatted := formatter.String()
690 lines := strings.Split(formatted, "\n")
691
692 // Truncate if needed.
693 maxLines := responseContextHeight
694 if expanded {
695 maxLines = len(lines)
696 }
697
698 if len(lines) > maxLines && !expanded {
699 truncMsg := sty.Tool.DiffTruncation.
700 Width(bodyWidth).
701 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
702 formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
703 }
704
705 return sty.Tool.Body.Render(formatted)
706}
707
708// formatTimeout converts timeout seconds to a duration string (e.g., "30s").
709// Returns empty string if timeout is 0.
710func formatTimeout(timeout int) string {
711 if timeout == 0 {
712 return ""
713 }
714 return fmt.Sprintf("%ds", timeout)
715}
716
717// formatNonZero returns string representation of non-zero integers, empty string for zero.
718func formatNonZero(value int) string {
719 if value == 0 {
720 return ""
721 }
722 return fmt.Sprintf("%d", value)
723}
724
725// toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
726func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool) string {
727 bodyWidth := width - toolBodyLeftPaddingTotal
728
729 formatter := common.DiffFormatter(sty).
730 Before(file, meta.OldContent).
731 After(file, meta.NewContent).
732 Width(bodyWidth)
733
734 // Use split view for wide terminals.
735 if width > maxTextWidth {
736 formatter = formatter.Split()
737 }
738
739 formatted := formatter.String()
740 lines := strings.Split(formatted, "\n")
741
742 // Truncate if needed.
743 maxLines := responseContextHeight
744 if expanded {
745 maxLines = len(lines)
746 }
747
748 if len(lines) > maxLines && !expanded {
749 truncMsg := sty.Tool.DiffTruncation.
750 Width(bodyWidth).
751 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
752 formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
753 }
754
755 // Add failed edits note if any exist.
756 if len(meta.EditsFailed) > 0 {
757 noteTag := sty.Tool.NoteTag.Render("Note")
758 noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, totalEdits)
759 note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
760 formatted = formatted + "\n\n" + note
761 }
762
763 return sty.Tool.Body.Render(formatted)
764}
765
766// roundedEnumerator creates a tree enumerator with rounded corners.
767func roundedEnumerator(lPadding, width int) tree.Enumerator {
768 if width == 0 {
769 width = 2
770 }
771 if lPadding == 0 {
772 lPadding = 1
773 }
774 return func(children tree.Children, index int) string {
775 line := strings.Repeat("─", width)
776 padding := strings.Repeat(" ", lPadding)
777 if children.Length()-1 == index {
778 return padding + "╰" + line
779 }
780 return padding + "├" + line
781 }
782}
783
784// toolOutputMarkdownContent renders markdown content with optional truncation.
785func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
786 content = strings.ReplaceAll(content, "\r\n", "\n")
787 content = strings.ReplaceAll(content, "\t", " ")
788 content = strings.TrimSpace(content)
789
790 // Cap width for readability.
791 if width > maxTextWidth {
792 width = maxTextWidth
793 }
794
795 renderer := common.PlainMarkdownRenderer(sty, width)
796 rendered, err := renderer.Render(content)
797 if err != nil {
798 return toolOutputPlainContent(sty, content, width, expanded)
799 }
800
801 lines := strings.Split(rendered, "\n")
802 maxLines := responseContextHeight
803 if expanded {
804 maxLines = len(lines)
805 }
806
807 var out []string
808 for i, ln := range lines {
809 if i >= maxLines {
810 break
811 }
812 out = append(out, ln)
813 }
814
815 if len(lines) > maxLines && !expanded {
816 out = append(out, sty.Tool.ContentTruncation.
817 Width(width).
818 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
819 )
820 }
821
822 return sty.Tool.Body.Render(strings.Join(out, "\n"))
823}
824
825// formatToolForCopy formats the tool call for clipboard copying.
826func (t *baseToolMessageItem) formatToolForCopy() string {
827 var parts []string
828
829 toolName := prettifyToolName(t.toolCall.Name)
830 parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
831
832 if t.toolCall.Input != "" {
833 params := t.formatParametersForCopy()
834 if params != "" {
835 parts = append(parts, "### Parameters:")
836 parts = append(parts, params)
837 }
838 }
839
840 if t.result != nil && t.result.ToolCallID != "" {
841 if t.result.IsError {
842 parts = append(parts, "### Error:")
843 parts = append(parts, t.result.Content)
844 } else {
845 parts = append(parts, "### Result:")
846 content := t.formatResultForCopy()
847 if content != "" {
848 parts = append(parts, content)
849 }
850 }
851 } else if t.status == ToolStatusCanceled {
852 parts = append(parts, "### Status:")
853 parts = append(parts, "Cancelled")
854 } else {
855 parts = append(parts, "### Status:")
856 parts = append(parts, "Pending...")
857 }
858
859 return strings.Join(parts, "\n\n")
860}
861
862// formatParametersForCopy formats tool parameters for clipboard copying.
863func (t *baseToolMessageItem) formatParametersForCopy() string {
864 switch t.toolCall.Name {
865 case tools.BashToolName:
866 var params tools.BashParams
867 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
868 cmd := strings.ReplaceAll(params.Command, "\n", " ")
869 cmd = strings.ReplaceAll(cmd, "\t", " ")
870 return fmt.Sprintf("**Command:** %s", cmd)
871 }
872 case tools.ViewToolName:
873 var params tools.ViewParams
874 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
875 var parts []string
876 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
877 if params.Limit > 0 {
878 parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
879 }
880 if params.Offset > 0 {
881 parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
882 }
883 return strings.Join(parts, "\n")
884 }
885 case tools.EditToolName:
886 var params tools.EditParams
887 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
888 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
889 }
890 case tools.MultiEditToolName:
891 var params tools.MultiEditParams
892 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
893 var parts []string
894 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
895 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
896 return strings.Join(parts, "\n")
897 }
898 case tools.WriteToolName:
899 var params tools.WriteParams
900 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
901 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
902 }
903 case tools.FetchToolName:
904 var params tools.FetchParams
905 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
906 var parts []string
907 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
908 if params.Format != "" {
909 parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
910 }
911 if params.Timeout > 0 {
912 parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
913 }
914 return strings.Join(parts, "\n")
915 }
916 case tools.AgenticFetchToolName:
917 var params tools.AgenticFetchParams
918 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
919 var parts []string
920 if params.URL != "" {
921 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
922 }
923 if params.Prompt != "" {
924 parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
925 }
926 return strings.Join(parts, "\n")
927 }
928 case tools.WebFetchToolName:
929 var params tools.WebFetchParams
930 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
931 return fmt.Sprintf("**URL:** %s", params.URL)
932 }
933 case tools.GrepToolName:
934 var params tools.GrepParams
935 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
936 var parts []string
937 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
938 if params.Path != "" {
939 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
940 }
941 if params.Include != "" {
942 parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
943 }
944 if params.LiteralText {
945 parts = append(parts, "**Literal:** true")
946 }
947 return strings.Join(parts, "\n")
948 }
949 case tools.GlobToolName:
950 var params tools.GlobParams
951 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
952 var parts []string
953 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
954 if params.Path != "" {
955 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
956 }
957 return strings.Join(parts, "\n")
958 }
959 case tools.LSToolName:
960 var params tools.LSParams
961 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
962 path := params.Path
963 if path == "" {
964 path = "."
965 }
966 return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
967 }
968 case tools.DownloadToolName:
969 var params tools.DownloadParams
970 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
971 var parts []string
972 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
973 parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
974 if params.Timeout > 0 {
975 parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
976 }
977 return strings.Join(parts, "\n")
978 }
979 case tools.SourcegraphToolName:
980 var params tools.SourcegraphParams
981 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
982 var parts []string
983 parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
984 if params.Count > 0 {
985 parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
986 }
987 if params.ContextWindow > 0 {
988 parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
989 }
990 return strings.Join(parts, "\n")
991 }
992 case tools.DiagnosticsToolName:
993 return "**Project:** diagnostics"
994 case agent.AgentToolName:
995 var params agent.AgentParams
996 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
997 return fmt.Sprintf("**Task:**\n%s", params.Prompt)
998 }
999 }
1000
1001 var params map[string]any
1002 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1003 var parts []string
1004 for key, value := range params {
1005 displayKey := strings.ReplaceAll(key, "_", " ")
1006 if len(displayKey) > 0 {
1007 displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
1008 }
1009 parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
1010 }
1011 return strings.Join(parts, "\n")
1012 }
1013
1014 return ""
1015}
1016
1017// formatResultForCopy formats tool results for clipboard copying.
1018func (t *baseToolMessageItem) formatResultForCopy() string {
1019 if t.result == nil {
1020 return ""
1021 }
1022
1023 if t.result.Data != "" {
1024 if strings.HasPrefix(t.result.MIMEType, "image/") {
1025 return fmt.Sprintf("[Image: %s]", t.result.MIMEType)
1026 }
1027 return fmt.Sprintf("[Media: %s]", t.result.MIMEType)
1028 }
1029
1030 switch t.toolCall.Name {
1031 case tools.BashToolName:
1032 return t.formatBashResultForCopy()
1033 case tools.ViewToolName:
1034 return t.formatViewResultForCopy()
1035 case tools.EditToolName:
1036 return t.formatEditResultForCopy()
1037 case tools.MultiEditToolName:
1038 return t.formatMultiEditResultForCopy()
1039 case tools.WriteToolName:
1040 return t.formatWriteResultForCopy()
1041 case tools.FetchToolName:
1042 return t.formatFetchResultForCopy()
1043 case tools.AgenticFetchToolName:
1044 return t.formatAgenticFetchResultForCopy()
1045 case tools.WebFetchToolName:
1046 return t.formatWebFetchResultForCopy()
1047 case agent.AgentToolName:
1048 return t.formatAgentResultForCopy()
1049 case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName, tools.TodosToolName:
1050 return fmt.Sprintf("```\n%s\n```", t.result.Content)
1051 default:
1052 return t.result.Content
1053 }
1054}
1055
1056// formatBashResultForCopy formats bash tool results for clipboard.
1057func (t *baseToolMessageItem) formatBashResultForCopy() string {
1058 if t.result == nil {
1059 return ""
1060 }
1061
1062 var meta tools.BashResponseMetadata
1063 if t.result.Metadata != "" {
1064 json.Unmarshal([]byte(t.result.Metadata), &meta)
1065 }
1066
1067 output := meta.Output
1068 if output == "" && t.result.Content != tools.BashNoOutput {
1069 output = t.result.Content
1070 }
1071
1072 if output == "" {
1073 return ""
1074 }
1075
1076 return fmt.Sprintf("```bash\n%s\n```", output)
1077}
1078
1079// formatViewResultForCopy formats view tool results for clipboard.
1080func (t *baseToolMessageItem) formatViewResultForCopy() string {
1081 if t.result == nil {
1082 return ""
1083 }
1084
1085 var meta tools.ViewResponseMetadata
1086 if t.result.Metadata != "" {
1087 json.Unmarshal([]byte(t.result.Metadata), &meta)
1088 }
1089
1090 if meta.Content == "" {
1091 return t.result.Content
1092 }
1093
1094 lang := ""
1095 if meta.FilePath != "" {
1096 ext := strings.ToLower(filepath.Ext(meta.FilePath))
1097 switch ext {
1098 case ".go":
1099 lang = "go"
1100 case ".js", ".mjs":
1101 lang = "javascript"
1102 case ".ts":
1103 lang = "typescript"
1104 case ".py":
1105 lang = "python"
1106 case ".rs":
1107 lang = "rust"
1108 case ".java":
1109 lang = "java"
1110 case ".c":
1111 lang = "c"
1112 case ".cpp", ".cc", ".cxx":
1113 lang = "cpp"
1114 case ".sh", ".bash":
1115 lang = "bash"
1116 case ".json":
1117 lang = "json"
1118 case ".yaml", ".yml":
1119 lang = "yaml"
1120 case ".xml":
1121 lang = "xml"
1122 case ".html":
1123 lang = "html"
1124 case ".css":
1125 lang = "css"
1126 case ".md":
1127 lang = "markdown"
1128 }
1129 }
1130
1131 var result strings.Builder
1132 if lang != "" {
1133 result.WriteString(fmt.Sprintf("```%s\n", lang))
1134 } else {
1135 result.WriteString("```\n")
1136 }
1137 result.WriteString(meta.Content)
1138 result.WriteString("\n```")
1139
1140 return result.String()
1141}
1142
1143// formatEditResultForCopy formats edit tool results for clipboard.
1144func (t *baseToolMessageItem) formatEditResultForCopy() string {
1145 if t.result == nil || t.result.Metadata == "" {
1146 if t.result != nil {
1147 return t.result.Content
1148 }
1149 return ""
1150 }
1151
1152 var meta tools.EditResponseMetadata
1153 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1154 return t.result.Content
1155 }
1156
1157 var params tools.EditParams
1158 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1159
1160 var result strings.Builder
1161
1162 if meta.OldContent != "" || meta.NewContent != "" {
1163 fileName := params.FilePath
1164 if fileName != "" {
1165 fileName = fsext.PrettyPath(fileName)
1166 }
1167 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1168
1169 result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
1170 result.WriteString("```diff\n")
1171 result.WriteString(diffContent)
1172 result.WriteString("\n```")
1173 }
1174
1175 return result.String()
1176}
1177
1178// formatMultiEditResultForCopy formats multi-edit tool results for clipboard.
1179func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1180 if t.result == nil || t.result.Metadata == "" {
1181 if t.result != nil {
1182 return t.result.Content
1183 }
1184 return ""
1185 }
1186
1187 var meta tools.MultiEditResponseMetadata
1188 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1189 return t.result.Content
1190 }
1191
1192 var params tools.MultiEditParams
1193 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1194
1195 var result strings.Builder
1196 if meta.OldContent != "" || meta.NewContent != "" {
1197 fileName := params.FilePath
1198 if fileName != "" {
1199 fileName = fsext.PrettyPath(fileName)
1200 }
1201 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1202
1203 result.WriteString(fmt.Sprintf("Changes: +%d -%d\n", additions, removals))
1204 result.WriteString("```diff\n")
1205 result.WriteString(diffContent)
1206 result.WriteString("\n```")
1207 }
1208
1209 return result.String()
1210}
1211
1212// formatWriteResultForCopy formats write tool results for clipboard.
1213func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1214 if t.result == nil {
1215 return ""
1216 }
1217
1218 var params tools.WriteParams
1219 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1220 return t.result.Content
1221 }
1222
1223 lang := ""
1224 if params.FilePath != "" {
1225 ext := strings.ToLower(filepath.Ext(params.FilePath))
1226 switch ext {
1227 case ".go":
1228 lang = "go"
1229 case ".js", ".mjs":
1230 lang = "javascript"
1231 case ".ts":
1232 lang = "typescript"
1233 case ".py":
1234 lang = "python"
1235 case ".rs":
1236 lang = "rust"
1237 case ".java":
1238 lang = "java"
1239 case ".c":
1240 lang = "c"
1241 case ".cpp", ".cc", ".cxx":
1242 lang = "cpp"
1243 case ".sh", ".bash":
1244 lang = "bash"
1245 case ".json":
1246 lang = "json"
1247 case ".yaml", ".yml":
1248 lang = "yaml"
1249 case ".xml":
1250 lang = "xml"
1251 case ".html":
1252 lang = "html"
1253 case ".css":
1254 lang = "css"
1255 case ".md":
1256 lang = "markdown"
1257 }
1258 }
1259
1260 var result strings.Builder
1261 result.WriteString(fmt.Sprintf("File: %s\n", fsext.PrettyPath(params.FilePath)))
1262 if lang != "" {
1263 result.WriteString(fmt.Sprintf("```%s\n", lang))
1264 } else {
1265 result.WriteString("```\n")
1266 }
1267 result.WriteString(params.Content)
1268 result.WriteString("\n```")
1269
1270 return result.String()
1271}
1272
1273// formatFetchResultForCopy formats fetch tool results for clipboard.
1274func (t *baseToolMessageItem) formatFetchResultForCopy() string {
1275 if t.result == nil {
1276 return ""
1277 }
1278
1279 var params tools.FetchParams
1280 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1281 return t.result.Content
1282 }
1283
1284 var result strings.Builder
1285 if params.URL != "" {
1286 result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
1287 }
1288 if params.Format != "" {
1289 result.WriteString(fmt.Sprintf("Format: %s\n", params.Format))
1290 }
1291 if params.Timeout > 0 {
1292 result.WriteString(fmt.Sprintf("Timeout: %ds\n", params.Timeout))
1293 }
1294 result.WriteString("\n")
1295
1296 result.WriteString(t.result.Content)
1297
1298 return result.String()
1299}
1300
1301// formatAgenticFetchResultForCopy formats agentic fetch tool results for clipboard.
1302func (t *baseToolMessageItem) formatAgenticFetchResultForCopy() string {
1303 if t.result == nil {
1304 return ""
1305 }
1306
1307 var params tools.AgenticFetchParams
1308 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1309 return t.result.Content
1310 }
1311
1312 var result strings.Builder
1313 if params.URL != "" {
1314 result.WriteString(fmt.Sprintf("URL: %s\n", params.URL))
1315 }
1316 if params.Prompt != "" {
1317 result.WriteString(fmt.Sprintf("Prompt: %s\n\n", params.Prompt))
1318 }
1319
1320 result.WriteString("```markdown\n")
1321 result.WriteString(t.result.Content)
1322 result.WriteString("\n```")
1323
1324 return result.String()
1325}
1326
1327// formatWebFetchResultForCopy formats web fetch tool results for clipboard.
1328func (t *baseToolMessageItem) formatWebFetchResultForCopy() string {
1329 if t.result == nil {
1330 return ""
1331 }
1332
1333 var params tools.WebFetchParams
1334 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1335 return t.result.Content
1336 }
1337
1338 var result strings.Builder
1339 result.WriteString(fmt.Sprintf("URL: %s\n\n", params.URL))
1340 result.WriteString("```markdown\n")
1341 result.WriteString(t.result.Content)
1342 result.WriteString("\n```")
1343
1344 return result.String()
1345}
1346
1347// formatAgentResultForCopy formats agent tool results for clipboard.
1348func (t *baseToolMessageItem) formatAgentResultForCopy() string {
1349 if t.result == nil {
1350 return ""
1351 }
1352
1353 var result strings.Builder
1354
1355 if t.result.Content != "" {
1356 result.WriteString(fmt.Sprintf("```markdown\n%s\n```", t.result.Content))
1357 }
1358
1359 return result.String()
1360}
1361
1362// prettifyToolName returns a human-readable name for tool names.
1363func prettifyToolName(name string) string {
1364 switch name {
1365 case agent.AgentToolName:
1366 return "Agent"
1367 case tools.BashToolName:
1368 return "Bash"
1369 case tools.JobOutputToolName:
1370 return "Job: Output"
1371 case tools.JobKillToolName:
1372 return "Job: Kill"
1373 case tools.DownloadToolName:
1374 return "Download"
1375 case tools.EditToolName:
1376 return "Edit"
1377 case tools.MultiEditToolName:
1378 return "Multi-Edit"
1379 case tools.FetchToolName:
1380 return "Fetch"
1381 case tools.AgenticFetchToolName:
1382 return "Agentic Fetch"
1383 case tools.WebFetchToolName:
1384 return "Fetch"
1385 case tools.WebSearchToolName:
1386 return "Search"
1387 case tools.GlobToolName:
1388 return "Glob"
1389 case tools.GrepToolName:
1390 return "Grep"
1391 case tools.LSToolName:
1392 return "List"
1393 case tools.SourcegraphToolName:
1394 return "Sourcegraph"
1395 case tools.TodosToolName:
1396 return "To-Do"
1397 case tools.ViewToolName:
1398 return "View"
1399 case tools.WriteToolName:
1400 return "Write"
1401 default:
1402 return name
1403 }
1404}