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, nested bool) string {
430 icon := sty.Tool.IconPending.Render()
431 nameStyle := sty.Tool.NameNormal
432 if nested {
433 nameStyle = sty.Tool.NameNested
434 }
435 toolName := nameStyle.Render(name)
436
437 var animView string
438 if anim != nil {
439 animView = anim.Render()
440 }
441
442 return fmt.Sprintf("%s %s %s", icon, toolName, animView)
443}
444
445// toolEarlyStateContent handles error/cancelled/pending states before content rendering.
446// Returns the rendered output and true if early state was handled.
447func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int) (string, bool) {
448 var msg string
449 switch opts.Status {
450 case ToolStatusError:
451 msg = toolErrorContent(sty, opts.Result, width)
452 case ToolStatusCanceled:
453 msg = sty.Tool.StateCancelled.Render("Canceled.")
454 case ToolStatusAwaitingPermission:
455 msg = sty.Tool.StateWaiting.Render("Requesting permission...")
456 case ToolStatusRunning:
457 msg = sty.Tool.StateWaiting.Render("Waiting for tool response...")
458 default:
459 return "", false
460 }
461 return msg, true
462}
463
464// toolErrorContent formats an error message with ERROR tag.
465func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string {
466 if result == nil {
467 return ""
468 }
469 errContent := strings.ReplaceAll(result.Content, "\n", " ")
470 errTag := sty.Tool.ErrorTag.Render("ERROR")
471 tagWidth := lipgloss.Width(errTag)
472 errContent = ansi.Truncate(errContent, width-tagWidth-3, "…")
473 return fmt.Sprintf("%s %s", errTag, sty.Tool.ErrorMessage.Render(errContent))
474}
475
476// toolIcon returns the status icon for a tool call.
477// toolIcon returns the status icon for a tool call based on its status.
478func toolIcon(sty *styles.Styles, status ToolStatus) string {
479 switch status {
480 case ToolStatusSuccess:
481 return sty.Tool.IconSuccess.String()
482 case ToolStatusError:
483 return sty.Tool.IconError.String()
484 case ToolStatusCanceled:
485 return sty.Tool.IconCancelled.String()
486 default:
487 return sty.Tool.IconPending.String()
488 }
489}
490
491// toolParamList formats parameters as "main (key=value, ...)" with truncation.
492// toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
493func toolParamList(sty *styles.Styles, params []string, width int) string {
494 // minSpaceForMainParam is the min space required for the main param
495 // if this is less that the value set we will only show the main param nothing else
496 const minSpaceForMainParam = 30
497 if len(params) == 0 {
498 return ""
499 }
500
501 mainParam := params[0]
502
503 // Build key=value pairs from remaining params (consecutive key, value pairs).
504 var kvPairs []string
505 for i := 1; i+1 < len(params); i += 2 {
506 if params[i+1] != "" {
507 kvPairs = append(kvPairs, fmt.Sprintf("%s=%s", params[i], params[i+1]))
508 }
509 }
510
511 // Try to include key=value pairs if there's enough space.
512 output := mainParam
513 if len(kvPairs) > 0 {
514 partsStr := strings.Join(kvPairs, ", ")
515 if remaining := width - lipgloss.Width(partsStr) - 3; remaining >= minSpaceForMainParam {
516 output = fmt.Sprintf("%s (%s)", mainParam, partsStr)
517 }
518 }
519
520 if width >= 0 {
521 output = ansi.Truncate(output, width, "…")
522 }
523 return sty.Tool.ParamMain.Render(output)
524}
525
526// toolHeader builds the tool header line: "● ToolName params..."
527func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, nested bool, params ...string) string {
528 icon := toolIcon(sty, status)
529 nameStyle := sty.Tool.NameNormal
530 if nested {
531 nameStyle = sty.Tool.NameNested
532 }
533 toolName := nameStyle.Render(name)
534 prefix := fmt.Sprintf("%s %s ", icon, toolName)
535 prefixWidth := lipgloss.Width(prefix)
536 remainingWidth := width - prefixWidth
537 paramsStr := toolParamList(sty, params, remainingWidth)
538 return prefix + paramsStr
539}
540
541// toolOutputPlainContent renders plain text with optional expansion support.
542func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string {
543 content = stringext.NormalizeSpace(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 = stringext.NormalizeSpace(content)
577
578 lines := strings.Split(content, "\n")
579 maxLines := responseContextHeight
580 if expanded {
581 maxLines = len(lines)
582 }
583
584 // Truncate if needed.
585 displayLines := lines
586 if len(lines) > maxLines {
587 displayLines = lines[:maxLines]
588 }
589
590 bg := sty.Tool.ContentCodeBg
591 highlighted, _ := common.SyntaxHighlight(sty, strings.Join(displayLines, "\n"), path, bg)
592 highlightedLines := strings.Split(highlighted, "\n")
593
594 // Calculate line number width.
595 maxLineNumber := len(displayLines) + offset
596 maxDigits := getDigits(maxLineNumber)
597 numFmt := fmt.Sprintf("%%%dd", maxDigits)
598
599 bodyWidth := width - toolBodyLeftPaddingTotal
600 codeWidth := bodyWidth - maxDigits
601
602 var out []string
603 for i, ln := range highlightedLines {
604 lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
605
606 // Truncate accounting for padding that will be added.
607 ln = ansi.Truncate(ln, codeWidth-sty.Tool.ContentCodeLine.GetHorizontalPadding(), "…")
608
609 codeLine := sty.Tool.ContentCodeLine.
610 Width(codeWidth).
611 Render(ln)
612
613 out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
614 }
615
616 // Add truncation message if needed.
617 if len(lines) > maxLines && !expanded {
618 out = append(out, sty.Tool.ContentCodeTruncation.
619 Width(width).
620 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
621 )
622 }
623
624 return sty.Tool.Body.Render(strings.Join(out, "\n"))
625}
626
627// toolOutputImageContent renders image data with size info.
628func toolOutputImageContent(sty *styles.Styles, data, mediaType string) string {
629 dataSize := len(data) * 3 / 4
630 sizeStr := formatSize(dataSize)
631
632 return sty.Tool.Body.Render(fmt.Sprintf(
633 "%s %s %s %s",
634 sty.Tool.ResourceLoadedText.Render("Loaded Image"),
635 sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
636 sty.Tool.MediaType.Render(mediaType),
637 sty.Tool.ResourceSize.Render(sizeStr),
638 ))
639}
640
641// toolOutputSkillContent renders a skill loaded indicator.
642func toolOutputSkillContent(sty *styles.Styles, name, description string) string {
643 return sty.Tool.Body.Render(fmt.Sprintf(
644 "%s %s %s %s",
645 sty.Tool.ResourceLoadedText.Render("Loaded Skill"),
646 sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
647 sty.Tool.ResourceName.Render(name),
648 sty.Tool.ResourceSize.Render(description),
649 ))
650}
651
652// getDigits returns the number of digits in a number.
653func getDigits(n int) int {
654 if n == 0 {
655 return 1
656 }
657 if n < 0 {
658 n = -n
659 }
660 digits := 0
661 for n > 0 {
662 n /= 10
663 digits++
664 }
665 return digits
666}
667
668// formatSize formats byte size into human readable format.
669func formatSize(bytes int) string {
670 const (
671 kb = 1024
672 mb = kb * 1024
673 )
674 switch {
675 case bytes >= mb:
676 return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
677 case bytes >= kb:
678 return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
679 default:
680 return fmt.Sprintf("%d B", bytes)
681 }
682}
683
684// toolOutputDiffContent renders a diff between old and new content.
685func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool) string {
686 bodyWidth := width - toolBodyLeftPaddingTotal
687
688 formatter := common.DiffFormatter(sty).
689 Before(file, oldContent).
690 After(file, newContent).
691 Width(bodyWidth)
692
693 // Use split view for wide terminals.
694 if width > maxTextWidth {
695 formatter = formatter.Split()
696 }
697
698 formatted := formatter.String()
699 lines := strings.Split(formatted, "\n")
700
701 // Truncate if needed.
702 maxLines := responseContextHeight
703 if expanded {
704 maxLines = len(lines)
705 }
706
707 if len(lines) > maxLines && !expanded {
708 truncMsg := sty.Tool.DiffTruncation.
709 Width(bodyWidth).
710 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
711 formatted = strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
712 }
713
714 return sty.Tool.Body.Render(formatted)
715}
716
717// formatTimeout converts timeout seconds to a duration string (e.g., "30s").
718// Returns empty string if timeout is 0.
719func formatTimeout(timeout int) string {
720 if timeout == 0 {
721 return ""
722 }
723 return fmt.Sprintf("%ds", timeout)
724}
725
726// formatNonZero returns string representation of non-zero integers, empty string for zero.
727func formatNonZero(value int) string {
728 if value == 0 {
729 return ""
730 }
731 return fmt.Sprintf("%d", value)
732}
733
734// toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
735func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool) string {
736 bodyWidth := width - toolBodyLeftPaddingTotal
737
738 formatter := common.DiffFormatter(sty).
739 Before(file, meta.OldContent).
740 After(file, meta.NewContent).
741 Width(bodyWidth)
742
743 // Use split view for wide terminals.
744 if width > maxTextWidth {
745 formatter = formatter.Split()
746 }
747
748 formatted := formatter.String()
749 lines := strings.Split(formatted, "\n")
750
751 // Truncate if needed.
752 maxLines := responseContextHeight
753 if expanded {
754 maxLines = len(lines)
755 }
756
757 if len(lines) > maxLines && !expanded {
758 truncMsg := sty.Tool.DiffTruncation.
759 Width(bodyWidth).
760 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
761 formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
762 }
763
764 // Add failed edits note if any exist.
765 if len(meta.EditsFailed) > 0 {
766 noteTag := sty.Tool.NoteTag.Render("Note")
767 noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, totalEdits)
768 note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
769 formatted = formatted + "\n\n" + note
770 }
771
772 return sty.Tool.Body.Render(formatted)
773}
774
775// roundedEnumerator creates a tree enumerator with rounded corners.
776func roundedEnumerator(lPadding, width int) tree.Enumerator {
777 if width == 0 {
778 width = 2
779 }
780 if lPadding == 0 {
781 lPadding = 1
782 }
783 return func(children tree.Children, index int) string {
784 line := strings.Repeat("─", width)
785 padding := strings.Repeat(" ", lPadding)
786 if children.Length()-1 == index {
787 return padding + "╰" + line
788 }
789 return padding + "├" + line
790 }
791}
792
793// toolOutputMarkdownContent renders markdown content with optional truncation.
794func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
795 content = stringext.NormalizeSpace(content)
796
797 // Cap width for readability.
798 if width > maxTextWidth {
799 width = maxTextWidth
800 }
801
802 renderer := common.PlainMarkdownRenderer(sty, width)
803 rendered, err := renderer.Render(content)
804 if err != nil {
805 return toolOutputPlainContent(sty, content, width, expanded)
806 }
807
808 lines := strings.Split(rendered, "\n")
809 maxLines := responseContextHeight
810 if expanded {
811 maxLines = len(lines)
812 }
813
814 var out []string
815 for i, ln := range lines {
816 if i >= maxLines {
817 break
818 }
819 out = append(out, ln)
820 }
821
822 if len(lines) > maxLines && !expanded {
823 out = append(out, sty.Tool.ContentTruncation.
824 Width(width).
825 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
826 )
827 }
828
829 return sty.Tool.Body.Render(strings.Join(out, "\n"))
830}
831
832// formatToolForCopy formats the tool call for clipboard copying.
833func (t *baseToolMessageItem) formatToolForCopy() string {
834 var parts []string
835
836 toolName := prettifyToolName(t.toolCall.Name)
837 parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
838
839 if t.toolCall.Input != "" {
840 params := t.formatParametersForCopy()
841 if params != "" {
842 parts = append(parts, "### Parameters:")
843 parts = append(parts, params)
844 }
845 }
846
847 if t.result != nil && t.result.ToolCallID != "" {
848 if t.result.IsError {
849 parts = append(parts, "### Error:")
850 parts = append(parts, t.result.Content)
851 } else {
852 parts = append(parts, "### Result:")
853 content := t.formatResultForCopy()
854 if content != "" {
855 parts = append(parts, content)
856 }
857 }
858 } else if t.status == ToolStatusCanceled {
859 parts = append(parts, "### Status:")
860 parts = append(parts, "Cancelled")
861 } else {
862 parts = append(parts, "### Status:")
863 parts = append(parts, "Pending...")
864 }
865
866 return strings.Join(parts, "\n\n")
867}
868
869// formatParametersForCopy formats tool parameters for clipboard copying.
870func (t *baseToolMessageItem) formatParametersForCopy() string {
871 switch t.toolCall.Name {
872 case tools.BashToolName:
873 var params tools.BashParams
874 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
875 cmd := strings.ReplaceAll(params.Command, "\n", " ")
876 cmd = strings.ReplaceAll(cmd, "\t", " ")
877 return fmt.Sprintf("**Command:** %s", cmd)
878 }
879 case tools.ViewToolName:
880 var params tools.ViewParams
881 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
882 var parts []string
883 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
884 if params.Limit > 0 {
885 parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
886 }
887 if params.Offset > 0 {
888 parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
889 }
890 return strings.Join(parts, "\n")
891 }
892 case tools.EditToolName:
893 var params tools.EditParams
894 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
895 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
896 }
897 case tools.MultiEditToolName:
898 var params tools.MultiEditParams
899 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
900 var parts []string
901 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
902 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
903 return strings.Join(parts, "\n")
904 }
905 case tools.WriteToolName:
906 var params tools.WriteParams
907 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
908 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
909 }
910 case tools.FetchToolName:
911 var params tools.FetchParams
912 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
913 var parts []string
914 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
915 if params.Format != "" {
916 parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
917 }
918 if params.Timeout > 0 {
919 parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
920 }
921 return strings.Join(parts, "\n")
922 }
923 case tools.AgenticFetchToolName:
924 var params tools.AgenticFetchParams
925 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
926 var parts []string
927 if params.URL != "" {
928 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
929 }
930 if params.Prompt != "" {
931 parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
932 }
933 return strings.Join(parts, "\n")
934 }
935 case tools.WebFetchToolName:
936 var params tools.WebFetchParams
937 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
938 return fmt.Sprintf("**URL:** %s", params.URL)
939 }
940 case tools.GrepToolName:
941 var params tools.GrepParams
942 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
943 var parts []string
944 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
945 if params.Path != "" {
946 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
947 }
948 if params.Include != "" {
949 parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
950 }
951 if params.LiteralText {
952 parts = append(parts, "**Literal:** true")
953 }
954 return strings.Join(parts, "\n")
955 }
956 case tools.GlobToolName:
957 var params tools.GlobParams
958 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
959 var parts []string
960 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
961 if params.Path != "" {
962 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
963 }
964 return strings.Join(parts, "\n")
965 }
966 case tools.LSToolName:
967 var params tools.LSParams
968 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
969 path := params.Path
970 if path == "" {
971 path = "."
972 }
973 return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
974 }
975 case tools.DownloadToolName:
976 var params tools.DownloadParams
977 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
978 var parts []string
979 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
980 parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
981 if params.Timeout > 0 {
982 parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
983 }
984 return strings.Join(parts, "\n")
985 }
986 case tools.SourcegraphToolName:
987 var params tools.SourcegraphParams
988 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
989 var parts []string
990 parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
991 if params.Count > 0 {
992 parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
993 }
994 if params.ContextWindow > 0 {
995 parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
996 }
997 return strings.Join(parts, "\n")
998 }
999 case tools.DiagnosticsToolName:
1000 return "**Project:** diagnostics"
1001 case agent.AgentToolName:
1002 var params agent.AgentParams
1003 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1004 return fmt.Sprintf("**Task:**\n%s", params.Prompt)
1005 }
1006 }
1007
1008 var params map[string]any
1009 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1010 var parts []string
1011 for key, value := range params {
1012 displayKey := strings.ReplaceAll(key, "_", " ")
1013 if len(displayKey) > 0 {
1014 displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
1015 }
1016 parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
1017 }
1018 return strings.Join(parts, "\n")
1019 }
1020
1021 return ""
1022}
1023
1024// formatResultForCopy formats tool results for clipboard copying.
1025func (t *baseToolMessageItem) formatResultForCopy() string {
1026 if t.result == nil {
1027 return ""
1028 }
1029
1030 if t.result.Data != "" {
1031 if strings.HasPrefix(t.result.MIMEType, "image/") {
1032 return fmt.Sprintf("[Image: %s]", t.result.MIMEType)
1033 }
1034 return fmt.Sprintf("[Media: %s]", t.result.MIMEType)
1035 }
1036
1037 switch t.toolCall.Name {
1038 case tools.BashToolName:
1039 return t.formatBashResultForCopy()
1040 case tools.ViewToolName:
1041 return t.formatViewResultForCopy()
1042 case tools.EditToolName:
1043 return t.formatEditResultForCopy()
1044 case tools.MultiEditToolName:
1045 return t.formatMultiEditResultForCopy()
1046 case tools.WriteToolName:
1047 return t.formatWriteResultForCopy()
1048 case tools.FetchToolName:
1049 return t.formatFetchResultForCopy()
1050 case tools.AgenticFetchToolName:
1051 return t.formatAgenticFetchResultForCopy()
1052 case tools.WebFetchToolName:
1053 return t.formatWebFetchResultForCopy()
1054 case agent.AgentToolName:
1055 return t.formatAgentResultForCopy()
1056 case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName, tools.TodosToolName:
1057 return fmt.Sprintf("```\n%s\n```", t.result.Content)
1058 default:
1059 return t.result.Content
1060 }
1061}
1062
1063// formatBashResultForCopy formats bash tool results for clipboard.
1064func (t *baseToolMessageItem) formatBashResultForCopy() string {
1065 if t.result == nil {
1066 return ""
1067 }
1068
1069 var meta tools.BashResponseMetadata
1070 if t.result.Metadata != "" {
1071 json.Unmarshal([]byte(t.result.Metadata), &meta)
1072 }
1073
1074 output := meta.Output
1075 if output == "" && t.result.Content != tools.BashNoOutput {
1076 output = t.result.Content
1077 }
1078
1079 if output == "" {
1080 return ""
1081 }
1082
1083 return fmt.Sprintf("```bash\n%s\n```", output)
1084}
1085
1086// formatViewResultForCopy formats view tool results for clipboard.
1087func (t *baseToolMessageItem) formatViewResultForCopy() string {
1088 if t.result == nil {
1089 return ""
1090 }
1091
1092 var meta tools.ViewResponseMetadata
1093 if t.result.Metadata != "" {
1094 json.Unmarshal([]byte(t.result.Metadata), &meta)
1095 }
1096
1097 if meta.Content == "" {
1098 return t.result.Content
1099 }
1100
1101 lang := ""
1102 if meta.FilePath != "" {
1103 ext := strings.ToLower(filepath.Ext(meta.FilePath))
1104 switch ext {
1105 case ".go":
1106 lang = "go"
1107 case ".js", ".mjs":
1108 lang = "javascript"
1109 case ".ts":
1110 lang = "typescript"
1111 case ".py":
1112 lang = "python"
1113 case ".rs":
1114 lang = "rust"
1115 case ".java":
1116 lang = "java"
1117 case ".c":
1118 lang = "c"
1119 case ".cpp", ".cc", ".cxx":
1120 lang = "cpp"
1121 case ".sh", ".bash":
1122 lang = "bash"
1123 case ".json":
1124 lang = "json"
1125 case ".yaml", ".yml":
1126 lang = "yaml"
1127 case ".xml":
1128 lang = "xml"
1129 case ".html":
1130 lang = "html"
1131 case ".css":
1132 lang = "css"
1133 case ".md":
1134 lang = "markdown"
1135 }
1136 }
1137
1138 var result strings.Builder
1139 if lang != "" {
1140 fmt.Fprintf(&result, "```%s\n", lang)
1141 } else {
1142 result.WriteString("```\n")
1143 }
1144 result.WriteString(meta.Content)
1145 result.WriteString("\n```")
1146
1147 return result.String()
1148}
1149
1150// formatEditResultForCopy formats edit tool results for clipboard.
1151func (t *baseToolMessageItem) formatEditResultForCopy() string {
1152 if t.result == nil || t.result.Metadata == "" {
1153 if t.result != nil {
1154 return t.result.Content
1155 }
1156 return ""
1157 }
1158
1159 var meta tools.EditResponseMetadata
1160 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1161 return t.result.Content
1162 }
1163
1164 var params tools.EditParams
1165 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1166
1167 var result strings.Builder
1168
1169 if meta.OldContent != "" || meta.NewContent != "" {
1170 fileName := params.FilePath
1171 if fileName != "" {
1172 fileName = fsext.PrettyPath(fileName)
1173 }
1174 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1175
1176 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1177 result.WriteString("```diff\n")
1178 result.WriteString(diffContent)
1179 result.WriteString("\n```")
1180 }
1181
1182 return result.String()
1183}
1184
1185// formatMultiEditResultForCopy formats multi-edit tool results for clipboard.
1186func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1187 if t.result == nil || t.result.Metadata == "" {
1188 if t.result != nil {
1189 return t.result.Content
1190 }
1191 return ""
1192 }
1193
1194 var meta tools.MultiEditResponseMetadata
1195 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1196 return t.result.Content
1197 }
1198
1199 var params tools.MultiEditParams
1200 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1201
1202 var result strings.Builder
1203 if meta.OldContent != "" || meta.NewContent != "" {
1204 fileName := params.FilePath
1205 if fileName != "" {
1206 fileName = fsext.PrettyPath(fileName)
1207 }
1208 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1209
1210 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1211 result.WriteString("```diff\n")
1212 result.WriteString(diffContent)
1213 result.WriteString("\n```")
1214 }
1215
1216 return result.String()
1217}
1218
1219// formatWriteResultForCopy formats write tool results for clipboard.
1220func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1221 if t.result == nil {
1222 return ""
1223 }
1224
1225 var params tools.WriteParams
1226 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1227 return t.result.Content
1228 }
1229
1230 lang := ""
1231 if params.FilePath != "" {
1232 ext := strings.ToLower(filepath.Ext(params.FilePath))
1233 switch ext {
1234 case ".go":
1235 lang = "go"
1236 case ".js", ".mjs":
1237 lang = "javascript"
1238 case ".ts":
1239 lang = "typescript"
1240 case ".py":
1241 lang = "python"
1242 case ".rs":
1243 lang = "rust"
1244 case ".java":
1245 lang = "java"
1246 case ".c":
1247 lang = "c"
1248 case ".cpp", ".cc", ".cxx":
1249 lang = "cpp"
1250 case ".sh", ".bash":
1251 lang = "bash"
1252 case ".json":
1253 lang = "json"
1254 case ".yaml", ".yml":
1255 lang = "yaml"
1256 case ".xml":
1257 lang = "xml"
1258 case ".html":
1259 lang = "html"
1260 case ".css":
1261 lang = "css"
1262 case ".md":
1263 lang = "markdown"
1264 }
1265 }
1266
1267 var result strings.Builder
1268 fmt.Fprintf(&result, "File: %s\n", fsext.PrettyPath(params.FilePath))
1269 if lang != "" {
1270 fmt.Fprintf(&result, "```%s\n", lang)
1271 } else {
1272 result.WriteString("```\n")
1273 }
1274 result.WriteString(params.Content)
1275 result.WriteString("\n```")
1276
1277 return result.String()
1278}
1279
1280// formatFetchResultForCopy formats fetch tool results for clipboard.
1281func (t *baseToolMessageItem) formatFetchResultForCopy() string {
1282 if t.result == nil {
1283 return ""
1284 }
1285
1286 var params tools.FetchParams
1287 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1288 return t.result.Content
1289 }
1290
1291 var result strings.Builder
1292 if params.URL != "" {
1293 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1294 }
1295 if params.Format != "" {
1296 fmt.Fprintf(&result, "Format: %s\n", params.Format)
1297 }
1298 if params.Timeout > 0 {
1299 fmt.Fprintf(&result, "Timeout: %ds\n", params.Timeout)
1300 }
1301 result.WriteString("\n")
1302
1303 result.WriteString(t.result.Content)
1304
1305 return result.String()
1306}
1307
1308// formatAgenticFetchResultForCopy formats agentic fetch tool results for clipboard.
1309func (t *baseToolMessageItem) formatAgenticFetchResultForCopy() string {
1310 if t.result == nil {
1311 return ""
1312 }
1313
1314 var params tools.AgenticFetchParams
1315 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1316 return t.result.Content
1317 }
1318
1319 var result strings.Builder
1320 if params.URL != "" {
1321 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1322 }
1323 if params.Prompt != "" {
1324 fmt.Fprintf(&result, "Prompt: %s\n\n", params.Prompt)
1325 }
1326
1327 result.WriteString("```markdown\n")
1328 result.WriteString(t.result.Content)
1329 result.WriteString("\n```")
1330
1331 return result.String()
1332}
1333
1334// formatWebFetchResultForCopy formats web fetch tool results for clipboard.
1335func (t *baseToolMessageItem) formatWebFetchResultForCopy() string {
1336 if t.result == nil {
1337 return ""
1338 }
1339
1340 var params tools.WebFetchParams
1341 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1342 return t.result.Content
1343 }
1344
1345 var result strings.Builder
1346 fmt.Fprintf(&result, "URL: %s\n\n", params.URL)
1347 result.WriteString("```markdown\n")
1348 result.WriteString(t.result.Content)
1349 result.WriteString("\n```")
1350
1351 return result.String()
1352}
1353
1354// formatAgentResultForCopy formats agent tool results for clipboard.
1355func (t *baseToolMessageItem) formatAgentResultForCopy() string {
1356 if t.result == nil {
1357 return ""
1358 }
1359
1360 var result strings.Builder
1361
1362 if t.result.Content != "" {
1363 fmt.Fprintf(&result, "```markdown\n%s\n```", t.result.Content)
1364 }
1365
1366 return result.String()
1367}
1368
1369// prettifyToolName returns a human-readable name for tool names.
1370func prettifyToolName(name string) string {
1371 switch name {
1372 case agent.AgentToolName:
1373 return "Agent"
1374 case tools.BashToolName:
1375 return "Bash"
1376 case tools.JobOutputToolName:
1377 return "Job: Output"
1378 case tools.JobKillToolName:
1379 return "Job: Kill"
1380 case tools.DownloadToolName:
1381 return "Download"
1382 case tools.EditToolName:
1383 return "Edit"
1384 case tools.MultiEditToolName:
1385 return "Multi-Edit"
1386 case tools.FetchToolName:
1387 return "Fetch"
1388 case tools.AgenticFetchToolName:
1389 return "Agentic Fetch"
1390 case tools.WebFetchToolName:
1391 return "Fetch"
1392 case tools.WebSearchToolName:
1393 return "Search"
1394 case tools.GlobToolName:
1395 return "Glob"
1396 case tools.GrepToolName:
1397 return "Grep"
1398 case tools.LSToolName:
1399 return "List"
1400 case tools.SourcegraphToolName:
1401 return "Sourcegraph"
1402 case tools.TodosToolName:
1403 return "To-Do"
1404 case tools.ViewToolName:
1405 return "View"
1406 case tools.WriteToolName:
1407 return "Write"
1408 default:
1409 return genericPrettyName(name)
1410 }
1411}