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 strings.HasPrefix(toolCall.Name, "mcp_") {
259 item = NewMCPToolMessageItem(sty, toolCall, result, canceled)
260 } else {
261 item = NewGenericToolMessageItem(sty, toolCall, result, canceled)
262 }
263 }
264 item.SetMessageID(messageID)
265 return item
266}
267
268// SetCompact implements the Compactable interface.
269func (t *baseToolMessageItem) SetCompact(compact bool) {
270 t.isCompact = compact
271 t.clearCache()
272}
273
274// ID returns the unique identifier for this tool message item.
275func (t *baseToolMessageItem) ID() string {
276 return t.toolCall.ID
277}
278
279// StartAnimation starts the assistant message animation if it should be spinning.
280func (t *baseToolMessageItem) StartAnimation() tea.Cmd {
281 if !t.isSpinning() {
282 return nil
283 }
284 return t.anim.Start()
285}
286
287// Animate progresses the assistant message animation if it should be spinning.
288func (t *baseToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd {
289 if !t.isSpinning() {
290 return nil
291 }
292 return t.anim.Animate(msg)
293}
294
295// RawRender implements [MessageItem].
296func (t *baseToolMessageItem) RawRender(width int) string {
297 toolItemWidth := width - MessageLeftPaddingTotal
298 if t.hasCappedWidth {
299 toolItemWidth = cappedMessageWidth(width)
300 }
301
302 content, height, ok := t.getCachedRender(toolItemWidth)
303 // if we are spinning or there is no cache rerender
304 if !ok || t.isSpinning() {
305 content = t.toolRenderer.RenderTool(t.sty, toolItemWidth, &ToolRenderOpts{
306 ToolCall: t.toolCall,
307 Result: t.result,
308 Anim: t.anim,
309 ExpandedContent: t.expandedContent,
310 Compact: t.isCompact,
311 IsSpinning: t.isSpinning(),
312 Status: t.computeStatus(),
313 })
314 height = lipgloss.Height(content)
315 // cache the rendered content
316 t.setCachedRender(content, toolItemWidth, height)
317 }
318
319 return t.renderHighlighted(content, toolItemWidth, height)
320}
321
322// Render renders the tool message item at the given width.
323func (t *baseToolMessageItem) Render(width int) string {
324 var prefix string
325 if t.isCompact {
326 prefix = t.sty.Chat.Message.ToolCallCompact.Render()
327 } else if t.focused {
328 prefix = t.sty.Chat.Message.ToolCallFocused.Render()
329 } else {
330 prefix = t.sty.Chat.Message.ToolCallBlurred.Render()
331 }
332 lines := strings.Split(t.RawRender(width), "\n")
333 for i, ln := range lines {
334 lines[i] = prefix + ln
335 }
336 return strings.Join(lines, "\n")
337}
338
339// ToolCall returns the tool call associated with this message item.
340func (t *baseToolMessageItem) ToolCall() message.ToolCall {
341 return t.toolCall
342}
343
344// SetToolCall sets the tool call associated with this message item.
345func (t *baseToolMessageItem) SetToolCall(tc message.ToolCall) {
346 t.toolCall = tc
347 t.clearCache()
348}
349
350// SetResult sets the tool result associated with this message item.
351func (t *baseToolMessageItem) SetResult(res *message.ToolResult) {
352 t.result = res
353 t.clearCache()
354}
355
356// MessageID returns the ID of the message containing this tool call.
357func (t *baseToolMessageItem) MessageID() string {
358 return t.messageID
359}
360
361// SetMessageID sets the ID of the message containing this tool call.
362func (t *baseToolMessageItem) SetMessageID(id string) {
363 t.messageID = id
364}
365
366// SetStatus sets the tool status.
367func (t *baseToolMessageItem) SetStatus(status ToolStatus) {
368 t.status = status
369 t.clearCache()
370}
371
372// Status returns the current tool status.
373func (t *baseToolMessageItem) Status() ToolStatus {
374 return t.status
375}
376
377// computeStatus computes the effective status considering the result.
378func (t *baseToolMessageItem) computeStatus() ToolStatus {
379 if t.result != nil {
380 if t.result.IsError {
381 return ToolStatusError
382 }
383 return ToolStatusSuccess
384 }
385 return t.status
386}
387
388// isSpinning returns true if the tool should show animation.
389func (t *baseToolMessageItem) isSpinning() bool {
390 if t.spinningFunc != nil {
391 return t.spinningFunc(SpinningState{
392 ToolCall: t.toolCall,
393 Result: t.result,
394 Status: t.status,
395 })
396 }
397 return !t.toolCall.Finished && t.status != ToolStatusCanceled
398}
399
400// SetSpinningFunc sets a custom function to determine if the tool should spin.
401func (t *baseToolMessageItem) SetSpinningFunc(fn SpinningFunc) {
402 t.spinningFunc = fn
403}
404
405// ToggleExpanded toggles the expanded state of the thinking box.
406func (t *baseToolMessageItem) ToggleExpanded() bool {
407 t.expandedContent = !t.expandedContent
408 t.clearCache()
409 return t.expandedContent
410}
411
412// HandleMouseClick implements MouseClickable.
413func (t *baseToolMessageItem) HandleMouseClick(btn ansi.MouseButton, x, y int) bool {
414 return btn == ansi.MouseLeft
415}
416
417// HandleKeyEvent implements KeyEventHandler.
418func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
419 if k := key.String(); k == "c" || k == "y" {
420 text := t.formatToolForCopy()
421 return true, common.CopyToClipboard(text, "Tool content copied to clipboard")
422 }
423 return false, nil
424}
425
426// pendingTool renders a tool that is still in progress with an animation.
427func pendingTool(sty *styles.Styles, name string, anim *anim.Anim, nested bool) string {
428 icon := sty.Tool.IconPending.Render()
429 nameStyle := sty.Tool.NameNormal
430 if nested {
431 nameStyle = sty.Tool.NameNested
432 }
433 toolName := nameStyle.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 = stringext.NormalizeSpace(content)
542 lines := strings.Split(content, "\n")
543
544 maxLines := responseContextHeight
545 if expanded {
546 maxLines = len(lines) // Show all
547 }
548
549 var out []string
550 for i, ln := range lines {
551 if i >= maxLines {
552 break
553 }
554 ln = " " + ln
555 if lipgloss.Width(ln) > width {
556 ln = ansi.Truncate(ln, width, "…")
557 }
558 out = append(out, sty.Tool.ContentLine.Width(width).Render(ln))
559 }
560
561 wasTruncated := len(lines) > responseContextHeight
562
563 if !expanded && wasTruncated {
564 out = append(out, sty.Tool.ContentTruncation.
565 Width(width).
566 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-responseContextHeight)))
567 }
568
569 return strings.Join(out, "\n")
570}
571
572// toolOutputCodeContent renders code with syntax highlighting and line numbers.
573func toolOutputCodeContent(sty *styles.Styles, path, content string, offset, width int, expanded bool) string {
574 content = stringext.NormalizeSpace(content)
575
576 lines := strings.Split(content, "\n")
577 maxLines := responseContextHeight
578 if expanded {
579 maxLines = len(lines)
580 }
581
582 // Truncate if needed.
583 displayLines := lines
584 if len(lines) > maxLines {
585 displayLines = lines[:maxLines]
586 }
587
588 bg := sty.Tool.ContentCodeBg
589 highlighted, _ := common.SyntaxHighlight(sty, strings.Join(displayLines, "\n"), path, bg)
590 highlightedLines := strings.Split(highlighted, "\n")
591
592 // Calculate line number width.
593 maxLineNumber := len(displayLines) + offset
594 maxDigits := getDigits(maxLineNumber)
595 numFmt := fmt.Sprintf("%%%dd", maxDigits)
596
597 bodyWidth := width - toolBodyLeftPaddingTotal
598 codeWidth := bodyWidth - maxDigits
599
600 var out []string
601 for i, ln := range highlightedLines {
602 lineNum := sty.Tool.ContentLineNumber.Render(fmt.Sprintf(numFmt, i+1+offset))
603
604 // Truncate accounting for padding that will be added.
605 ln = ansi.Truncate(ln, codeWidth-sty.Tool.ContentCodeLine.GetHorizontalPadding(), "…")
606
607 codeLine := sty.Tool.ContentCodeLine.
608 Width(codeWidth).
609 Render(ln)
610
611 out = append(out, lipgloss.JoinHorizontal(lipgloss.Left, lineNum, codeLine))
612 }
613
614 // Add truncation message if needed.
615 if len(lines) > maxLines && !expanded {
616 out = append(out, sty.Tool.ContentCodeTruncation.
617 Width(width).
618 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
619 )
620 }
621
622 return sty.Tool.Body.Render(strings.Join(out, "\n"))
623}
624
625// toolOutputImageContent renders image data with size info.
626func toolOutputImageContent(sty *styles.Styles, data, mediaType string) string {
627 dataSize := len(data) * 3 / 4
628 sizeStr := formatSize(dataSize)
629
630 return sty.Tool.Body.Render(fmt.Sprintf(
631 "%s %s %s %s",
632 sty.Tool.ResourceLoadedText.Render("Loaded Image"),
633 sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
634 sty.Tool.MediaType.Render(mediaType),
635 sty.Tool.ResourceSize.Render(sizeStr),
636 ))
637}
638
639// toolOutputSkillContent renders a skill loaded indicator.
640func toolOutputSkillContent(sty *styles.Styles, name, description string) string {
641 return sty.Tool.Body.Render(fmt.Sprintf(
642 "%s %s %s %s",
643 sty.Tool.ResourceLoadedText.Render("Loaded Skill"),
644 sty.Tool.ResourceLoadedIndicator.Render(styles.ArrowRightIcon),
645 sty.Tool.ResourceName.Render(name),
646 sty.Tool.ResourceSize.Render(description),
647 ))
648}
649
650// getDigits returns the number of digits in a number.
651func getDigits(n int) int {
652 if n == 0 {
653 return 1
654 }
655 if n < 0 {
656 n = -n
657 }
658 digits := 0
659 for n > 0 {
660 n /= 10
661 digits++
662 }
663 return digits
664}
665
666// formatSize formats byte size into human readable format.
667func formatSize(bytes int) string {
668 const (
669 kb = 1024
670 mb = kb * 1024
671 )
672 switch {
673 case bytes >= mb:
674 return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
675 case bytes >= kb:
676 return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
677 default:
678 return fmt.Sprintf("%d B", bytes)
679 }
680}
681
682// toolOutputDiffContent renders a diff between old and new content.
683func toolOutputDiffContent(sty *styles.Styles, file, oldContent, newContent string, width int, expanded bool) string {
684 bodyWidth := width - toolBodyLeftPaddingTotal
685
686 formatter := common.DiffFormatter(sty).
687 Before(file, oldContent).
688 After(file, newContent).
689 Width(bodyWidth)
690
691 // Use split view for wide terminals.
692 if width > maxTextWidth {
693 formatter = formatter.Split()
694 }
695
696 formatted := formatter.String()
697 lines := strings.Split(formatted, "\n")
698
699 // Truncate if needed.
700 maxLines := responseContextHeight
701 if expanded {
702 maxLines = len(lines)
703 }
704
705 if len(lines) > maxLines && !expanded {
706 truncMsg := sty.Tool.DiffTruncation.
707 Width(bodyWidth).
708 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
709 formatted = strings.Join(lines[:maxLines], "\n") + "\n" + truncMsg
710 }
711
712 return sty.Tool.Body.Render(formatted)
713}
714
715// formatTimeout converts timeout seconds to a duration string (e.g., "30s").
716// Returns empty string if timeout is 0.
717func formatTimeout(timeout int) string {
718 if timeout == 0 {
719 return ""
720 }
721 return fmt.Sprintf("%ds", timeout)
722}
723
724// formatNonZero returns string representation of non-zero integers, empty string for zero.
725func formatNonZero(value int) string {
726 if value == 0 {
727 return ""
728 }
729 return fmt.Sprintf("%d", value)
730}
731
732// toolOutputMultiEditDiffContent renders a diff with optional failed edits note.
733func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.MultiEditResponseMetadata, totalEdits, width int, expanded bool) string {
734 bodyWidth := width - toolBodyLeftPaddingTotal
735
736 formatter := common.DiffFormatter(sty).
737 Before(file, meta.OldContent).
738 After(file, meta.NewContent).
739 Width(bodyWidth)
740
741 // Use split view for wide terminals.
742 if width > maxTextWidth {
743 formatter = formatter.Split()
744 }
745
746 formatted := formatter.String()
747 lines := strings.Split(formatted, "\n")
748
749 // Truncate if needed.
750 maxLines := responseContextHeight
751 if expanded {
752 maxLines = len(lines)
753 }
754
755 if len(lines) > maxLines && !expanded {
756 truncMsg := sty.Tool.DiffTruncation.
757 Width(bodyWidth).
758 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines))
759 formatted = truncMsg + "\n" + strings.Join(lines[:maxLines], "\n")
760 }
761
762 // Add failed edits note if any exist.
763 if len(meta.EditsFailed) > 0 {
764 noteTag := sty.Tool.NoteTag.Render("Note")
765 noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, totalEdits)
766 note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
767 formatted = formatted + "\n\n" + note
768 }
769
770 return sty.Tool.Body.Render(formatted)
771}
772
773// roundedEnumerator creates a tree enumerator with rounded corners.
774func roundedEnumerator(lPadding, width int) tree.Enumerator {
775 if width == 0 {
776 width = 2
777 }
778 if lPadding == 0 {
779 lPadding = 1
780 }
781 return func(children tree.Children, index int) string {
782 line := strings.Repeat("─", width)
783 padding := strings.Repeat(" ", lPadding)
784 if children.Length()-1 == index {
785 return padding + "╰" + line
786 }
787 return padding + "├" + line
788 }
789}
790
791// toolOutputMarkdownContent renders markdown content with optional truncation.
792func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
793 content = stringext.NormalizeSpace(content)
794
795 // Cap width for readability.
796 if width > maxTextWidth {
797 width = maxTextWidth
798 }
799
800 renderer := common.PlainMarkdownRenderer(sty, width)
801 rendered, err := renderer.Render(content)
802 if err != nil {
803 return toolOutputPlainContent(sty, content, width, expanded)
804 }
805
806 lines := strings.Split(rendered, "\n")
807 maxLines := responseContextHeight
808 if expanded {
809 maxLines = len(lines)
810 }
811
812 var out []string
813 for i, ln := range lines {
814 if i >= maxLines {
815 break
816 }
817 out = append(out, ln)
818 }
819
820 if len(lines) > maxLines && !expanded {
821 out = append(out, sty.Tool.ContentTruncation.
822 Width(width).
823 Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
824 )
825 }
826
827 return sty.Tool.Body.Render(strings.Join(out, "\n"))
828}
829
830// formatToolForCopy formats the tool call for clipboard copying.
831func (t *baseToolMessageItem) formatToolForCopy() string {
832 var parts []string
833
834 toolName := prettifyToolName(t.toolCall.Name)
835 parts = append(parts, fmt.Sprintf("## %s Tool Call", toolName))
836
837 if t.toolCall.Input != "" {
838 params := t.formatParametersForCopy()
839 if params != "" {
840 parts = append(parts, "### Parameters:")
841 parts = append(parts, params)
842 }
843 }
844
845 if t.result != nil && t.result.ToolCallID != "" {
846 if t.result.IsError {
847 parts = append(parts, "### Error:")
848 parts = append(parts, t.result.Content)
849 } else {
850 parts = append(parts, "### Result:")
851 content := t.formatResultForCopy()
852 if content != "" {
853 parts = append(parts, content)
854 }
855 }
856 } else if t.status == ToolStatusCanceled {
857 parts = append(parts, "### Status:")
858 parts = append(parts, "Cancelled")
859 } else {
860 parts = append(parts, "### Status:")
861 parts = append(parts, "Pending...")
862 }
863
864 return strings.Join(parts, "\n\n")
865}
866
867// formatParametersForCopy formats tool parameters for clipboard copying.
868func (t *baseToolMessageItem) formatParametersForCopy() string {
869 switch t.toolCall.Name {
870 case tools.BashToolName:
871 var params tools.BashParams
872 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
873 cmd := strings.ReplaceAll(params.Command, "\n", " ")
874 cmd = strings.ReplaceAll(cmd, "\t", " ")
875 return fmt.Sprintf("**Command:** %s", cmd)
876 }
877 case tools.ViewToolName:
878 var params tools.ViewParams
879 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
880 var parts []string
881 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
882 if params.Limit > 0 {
883 parts = append(parts, fmt.Sprintf("**Limit:** %d", params.Limit))
884 }
885 if params.Offset > 0 {
886 parts = append(parts, fmt.Sprintf("**Offset:** %d", params.Offset))
887 }
888 return strings.Join(parts, "\n")
889 }
890 case tools.EditToolName:
891 var params tools.EditParams
892 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
893 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
894 }
895 case tools.MultiEditToolName:
896 var params tools.MultiEditParams
897 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
898 var parts []string
899 parts = append(parts, fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath)))
900 parts = append(parts, fmt.Sprintf("**Edits:** %d", len(params.Edits)))
901 return strings.Join(parts, "\n")
902 }
903 case tools.WriteToolName:
904 var params tools.WriteParams
905 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
906 return fmt.Sprintf("**File:** %s", fsext.PrettyPath(params.FilePath))
907 }
908 case tools.FetchToolName:
909 var params tools.FetchParams
910 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
911 var parts []string
912 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
913 if params.Format != "" {
914 parts = append(parts, fmt.Sprintf("**Format:** %s", params.Format))
915 }
916 if params.Timeout > 0 {
917 parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
918 }
919 return strings.Join(parts, "\n")
920 }
921 case tools.AgenticFetchToolName:
922 var params tools.AgenticFetchParams
923 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
924 var parts []string
925 if params.URL != "" {
926 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
927 }
928 if params.Prompt != "" {
929 parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
930 }
931 return strings.Join(parts, "\n")
932 }
933 case tools.WebFetchToolName:
934 var params tools.WebFetchParams
935 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
936 return fmt.Sprintf("**URL:** %s", params.URL)
937 }
938 case tools.GrepToolName:
939 var params tools.GrepParams
940 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
941 var parts []string
942 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
943 if params.Path != "" {
944 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
945 }
946 if params.Include != "" {
947 parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
948 }
949 if params.LiteralText {
950 parts = append(parts, "**Literal:** true")
951 }
952 return strings.Join(parts, "\n")
953 }
954 case tools.GlobToolName:
955 var params tools.GlobParams
956 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
957 var parts []string
958 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
959 if params.Path != "" {
960 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
961 }
962 return strings.Join(parts, "\n")
963 }
964 case tools.LSToolName:
965 var params tools.LSParams
966 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
967 path := params.Path
968 if path == "" {
969 path = "."
970 }
971 return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
972 }
973 case tools.DownloadToolName:
974 var params tools.DownloadParams
975 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
976 var parts []string
977 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
978 parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
979 if params.Timeout > 0 {
980 parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
981 }
982 return strings.Join(parts, "\n")
983 }
984 case tools.SourcegraphToolName:
985 var params tools.SourcegraphParams
986 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
987 var parts []string
988 parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
989 if params.Count > 0 {
990 parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
991 }
992 if params.ContextWindow > 0 {
993 parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
994 }
995 return strings.Join(parts, "\n")
996 }
997 case tools.DiagnosticsToolName:
998 return "**Project:** diagnostics"
999 case agent.AgentToolName:
1000 var params agent.AgentParams
1001 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1002 return fmt.Sprintf("**Task:**\n%s", params.Prompt)
1003 }
1004 }
1005
1006 var params map[string]any
1007 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1008 var parts []string
1009 for key, value := range params {
1010 displayKey := strings.ReplaceAll(key, "_", " ")
1011 if len(displayKey) > 0 {
1012 displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
1013 }
1014 parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
1015 }
1016 return strings.Join(parts, "\n")
1017 }
1018
1019 return ""
1020}
1021
1022// formatResultForCopy formats tool results for clipboard copying.
1023func (t *baseToolMessageItem) formatResultForCopy() string {
1024 if t.result == nil {
1025 return ""
1026 }
1027
1028 if t.result.Data != "" {
1029 if strings.HasPrefix(t.result.MIMEType, "image/") {
1030 return fmt.Sprintf("[Image: %s]", t.result.MIMEType)
1031 }
1032 return fmt.Sprintf("[Media: %s]", t.result.MIMEType)
1033 }
1034
1035 switch t.toolCall.Name {
1036 case tools.BashToolName:
1037 return t.formatBashResultForCopy()
1038 case tools.ViewToolName:
1039 return t.formatViewResultForCopy()
1040 case tools.EditToolName:
1041 return t.formatEditResultForCopy()
1042 case tools.MultiEditToolName:
1043 return t.formatMultiEditResultForCopy()
1044 case tools.WriteToolName:
1045 return t.formatWriteResultForCopy()
1046 case tools.FetchToolName:
1047 return t.formatFetchResultForCopy()
1048 case tools.AgenticFetchToolName:
1049 return t.formatAgenticFetchResultForCopy()
1050 case tools.WebFetchToolName:
1051 return t.formatWebFetchResultForCopy()
1052 case agent.AgentToolName:
1053 return t.formatAgentResultForCopy()
1054 case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName, tools.TodosToolName:
1055 return fmt.Sprintf("```\n%s\n```", t.result.Content)
1056 default:
1057 return t.result.Content
1058 }
1059}
1060
1061// formatBashResultForCopy formats bash tool results for clipboard.
1062func (t *baseToolMessageItem) formatBashResultForCopy() string {
1063 if t.result == nil {
1064 return ""
1065 }
1066
1067 var meta tools.BashResponseMetadata
1068 if t.result.Metadata != "" {
1069 json.Unmarshal([]byte(t.result.Metadata), &meta)
1070 }
1071
1072 output := meta.Output
1073 if output == "" && t.result.Content != tools.BashNoOutput {
1074 output = t.result.Content
1075 }
1076
1077 if output == "" {
1078 return ""
1079 }
1080
1081 return fmt.Sprintf("```bash\n%s\n```", output)
1082}
1083
1084// formatViewResultForCopy formats view tool results for clipboard.
1085func (t *baseToolMessageItem) formatViewResultForCopy() string {
1086 if t.result == nil {
1087 return ""
1088 }
1089
1090 var meta tools.ViewResponseMetadata
1091 if t.result.Metadata != "" {
1092 json.Unmarshal([]byte(t.result.Metadata), &meta)
1093 }
1094
1095 if meta.Content == "" {
1096 return t.result.Content
1097 }
1098
1099 lang := ""
1100 if meta.FilePath != "" {
1101 ext := strings.ToLower(filepath.Ext(meta.FilePath))
1102 switch ext {
1103 case ".go":
1104 lang = "go"
1105 case ".js", ".mjs":
1106 lang = "javascript"
1107 case ".ts":
1108 lang = "typescript"
1109 case ".py":
1110 lang = "python"
1111 case ".rs":
1112 lang = "rust"
1113 case ".java":
1114 lang = "java"
1115 case ".c":
1116 lang = "c"
1117 case ".cpp", ".cc", ".cxx":
1118 lang = "cpp"
1119 case ".sh", ".bash":
1120 lang = "bash"
1121 case ".json":
1122 lang = "json"
1123 case ".yaml", ".yml":
1124 lang = "yaml"
1125 case ".xml":
1126 lang = "xml"
1127 case ".html":
1128 lang = "html"
1129 case ".css":
1130 lang = "css"
1131 case ".md":
1132 lang = "markdown"
1133 }
1134 }
1135
1136 var result strings.Builder
1137 if lang != "" {
1138 fmt.Fprintf(&result, "```%s\n", lang)
1139 } else {
1140 result.WriteString("```\n")
1141 }
1142 result.WriteString(meta.Content)
1143 result.WriteString("\n```")
1144
1145 return result.String()
1146}
1147
1148// formatEditResultForCopy formats edit tool results for clipboard.
1149func (t *baseToolMessageItem) formatEditResultForCopy() string {
1150 if t.result == nil || t.result.Metadata == "" {
1151 if t.result != nil {
1152 return t.result.Content
1153 }
1154 return ""
1155 }
1156
1157 var meta tools.EditResponseMetadata
1158 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1159 return t.result.Content
1160 }
1161
1162 var params tools.EditParams
1163 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1164
1165 var result strings.Builder
1166
1167 if meta.OldContent != "" || meta.NewContent != "" {
1168 fileName := params.FilePath
1169 if fileName != "" {
1170 fileName = fsext.PrettyPath(fileName)
1171 }
1172 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1173
1174 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1175 result.WriteString("```diff\n")
1176 result.WriteString(diffContent)
1177 result.WriteString("\n```")
1178 }
1179
1180 return result.String()
1181}
1182
1183// formatMultiEditResultForCopy formats multi-edit tool results for clipboard.
1184func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1185 if t.result == nil || t.result.Metadata == "" {
1186 if t.result != nil {
1187 return t.result.Content
1188 }
1189 return ""
1190 }
1191
1192 var meta tools.MultiEditResponseMetadata
1193 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1194 return t.result.Content
1195 }
1196
1197 var params tools.MultiEditParams
1198 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1199
1200 var result strings.Builder
1201 if meta.OldContent != "" || meta.NewContent != "" {
1202 fileName := params.FilePath
1203 if fileName != "" {
1204 fileName = fsext.PrettyPath(fileName)
1205 }
1206 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1207
1208 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1209 result.WriteString("```diff\n")
1210 result.WriteString(diffContent)
1211 result.WriteString("\n```")
1212 }
1213
1214 return result.String()
1215}
1216
1217// formatWriteResultForCopy formats write tool results for clipboard.
1218func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1219 if t.result == nil {
1220 return ""
1221 }
1222
1223 var params tools.WriteParams
1224 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1225 return t.result.Content
1226 }
1227
1228 lang := ""
1229 if params.FilePath != "" {
1230 ext := strings.ToLower(filepath.Ext(params.FilePath))
1231 switch ext {
1232 case ".go":
1233 lang = "go"
1234 case ".js", ".mjs":
1235 lang = "javascript"
1236 case ".ts":
1237 lang = "typescript"
1238 case ".py":
1239 lang = "python"
1240 case ".rs":
1241 lang = "rust"
1242 case ".java":
1243 lang = "java"
1244 case ".c":
1245 lang = "c"
1246 case ".cpp", ".cc", ".cxx":
1247 lang = "cpp"
1248 case ".sh", ".bash":
1249 lang = "bash"
1250 case ".json":
1251 lang = "json"
1252 case ".yaml", ".yml":
1253 lang = "yaml"
1254 case ".xml":
1255 lang = "xml"
1256 case ".html":
1257 lang = "html"
1258 case ".css":
1259 lang = "css"
1260 case ".md":
1261 lang = "markdown"
1262 }
1263 }
1264
1265 var result strings.Builder
1266 fmt.Fprintf(&result, "File: %s\n", fsext.PrettyPath(params.FilePath))
1267 if lang != "" {
1268 fmt.Fprintf(&result, "```%s\n", lang)
1269 } else {
1270 result.WriteString("```\n")
1271 }
1272 result.WriteString(params.Content)
1273 result.WriteString("\n```")
1274
1275 return result.String()
1276}
1277
1278// formatFetchResultForCopy formats fetch tool results for clipboard.
1279func (t *baseToolMessageItem) formatFetchResultForCopy() string {
1280 if t.result == nil {
1281 return ""
1282 }
1283
1284 var params tools.FetchParams
1285 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1286 return t.result.Content
1287 }
1288
1289 var result strings.Builder
1290 if params.URL != "" {
1291 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1292 }
1293 if params.Format != "" {
1294 fmt.Fprintf(&result, "Format: %s\n", params.Format)
1295 }
1296 if params.Timeout > 0 {
1297 fmt.Fprintf(&result, "Timeout: %ds\n", params.Timeout)
1298 }
1299 result.WriteString("\n")
1300
1301 result.WriteString(t.result.Content)
1302
1303 return result.String()
1304}
1305
1306// formatAgenticFetchResultForCopy formats agentic fetch tool results for clipboard.
1307func (t *baseToolMessageItem) formatAgenticFetchResultForCopy() string {
1308 if t.result == nil {
1309 return ""
1310 }
1311
1312 var params tools.AgenticFetchParams
1313 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1314 return t.result.Content
1315 }
1316
1317 var result strings.Builder
1318 if params.URL != "" {
1319 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1320 }
1321 if params.Prompt != "" {
1322 fmt.Fprintf(&result, "Prompt: %s\n\n", params.Prompt)
1323 }
1324
1325 result.WriteString("```markdown\n")
1326 result.WriteString(t.result.Content)
1327 result.WriteString("\n```")
1328
1329 return result.String()
1330}
1331
1332// formatWebFetchResultForCopy formats web fetch tool results for clipboard.
1333func (t *baseToolMessageItem) formatWebFetchResultForCopy() string {
1334 if t.result == nil {
1335 return ""
1336 }
1337
1338 var params tools.WebFetchParams
1339 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1340 return t.result.Content
1341 }
1342
1343 var result strings.Builder
1344 fmt.Fprintf(&result, "URL: %s\n\n", params.URL)
1345 result.WriteString("```markdown\n")
1346 result.WriteString(t.result.Content)
1347 result.WriteString("\n```")
1348
1349 return result.String()
1350}
1351
1352// formatAgentResultForCopy formats agent tool results for clipboard.
1353func (t *baseToolMessageItem) formatAgentResultForCopy() string {
1354 if t.result == nil {
1355 return ""
1356 }
1357
1358 var result strings.Builder
1359
1360 if t.result.Content != "" {
1361 fmt.Fprintf(&result, "```markdown\n%s\n```", t.result.Content)
1362 }
1363
1364 return result.String()
1365}
1366
1367// prettifyToolName returns a human-readable name for tool names.
1368func prettifyToolName(name string) string {
1369 switch name {
1370 case agent.AgentToolName:
1371 return "Agent"
1372 case tools.BashToolName:
1373 return "Bash"
1374 case tools.JobOutputToolName:
1375 return "Job: Output"
1376 case tools.JobKillToolName:
1377 return "Job: Kill"
1378 case tools.DownloadToolName:
1379 return "Download"
1380 case tools.EditToolName:
1381 return "Edit"
1382 case tools.MultiEditToolName:
1383 return "Multi-Edit"
1384 case tools.FetchToolName:
1385 return "Fetch"
1386 case tools.AgenticFetchToolName:
1387 return "Agentic Fetch"
1388 case tools.WebFetchToolName:
1389 return "Fetch"
1390 case tools.WebSearchToolName:
1391 return "Search"
1392 case tools.GlobToolName:
1393 return "Glob"
1394 case tools.GrepToolName:
1395 return "Grep"
1396 case tools.LSToolName:
1397 return "List"
1398 case tools.SourcegraphToolName:
1399 return "Sourcegraph"
1400 case tools.TodosToolName:
1401 return "To-Do"
1402 case tools.ViewToolName:
1403 return "View"
1404 case tools.WriteToolName:
1405 return "Write"
1406 default:
1407 return genericPrettyName(name)
1408 }
1409}