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