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