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.JQ != "" {
919 parts = append(parts, fmt.Sprintf("**jq:** %s", params.JQ))
920 }
921 if params.Timeout > 0 {
922 parts = append(parts, fmt.Sprintf("**Timeout:** %ds", params.Timeout))
923 }
924 return strings.Join(parts, "\n")
925 }
926 case tools.AgenticFetchToolName:
927 var params tools.AgenticFetchParams
928 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
929 var parts []string
930 if params.URL != "" {
931 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
932 }
933 if params.Prompt != "" {
934 parts = append(parts, fmt.Sprintf("**Prompt:** %s", params.Prompt))
935 }
936 return strings.Join(parts, "\n")
937 }
938 case tools.WebFetchToolName:
939 var params tools.WebFetchParams
940 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
941 return fmt.Sprintf("**URL:** %s", params.URL)
942 }
943 case tools.GrepToolName:
944 var params tools.GrepParams
945 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
946 var parts []string
947 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
948 if params.Path != "" {
949 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
950 }
951 if params.Include != "" {
952 parts = append(parts, fmt.Sprintf("**Include:** %s", params.Include))
953 }
954 if params.LiteralText {
955 parts = append(parts, "**Literal:** true")
956 }
957 return strings.Join(parts, "\n")
958 }
959 case tools.GlobToolName:
960 var params tools.GlobParams
961 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
962 var parts []string
963 parts = append(parts, fmt.Sprintf("**Pattern:** %s", params.Pattern))
964 if params.Path != "" {
965 parts = append(parts, fmt.Sprintf("**Path:** %s", params.Path))
966 }
967 return strings.Join(parts, "\n")
968 }
969 case tools.LSToolName:
970 var params tools.LSParams
971 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
972 path := params.Path
973 if path == "" {
974 path = "."
975 }
976 return fmt.Sprintf("**Path:** %s", fsext.PrettyPath(path))
977 }
978 case tools.DownloadToolName:
979 var params tools.DownloadParams
980 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
981 var parts []string
982 parts = append(parts, fmt.Sprintf("**URL:** %s", params.URL))
983 parts = append(parts, fmt.Sprintf("**File Path:** %s", fsext.PrettyPath(params.FilePath)))
984 if params.Timeout > 0 {
985 parts = append(parts, fmt.Sprintf("**Timeout:** %s", (time.Duration(params.Timeout)*time.Second).String()))
986 }
987 return strings.Join(parts, "\n")
988 }
989 case tools.SourcegraphToolName:
990 var params tools.SourcegraphParams
991 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
992 var parts []string
993 parts = append(parts, fmt.Sprintf("**Query:** %s", params.Query))
994 if params.Count > 0 {
995 parts = append(parts, fmt.Sprintf("**Count:** %d", params.Count))
996 }
997 if params.ContextWindow > 0 {
998 parts = append(parts, fmt.Sprintf("**Context:** %d", params.ContextWindow))
999 }
1000 return strings.Join(parts, "\n")
1001 }
1002 case tools.DiagnosticsToolName:
1003 return "**Project:** diagnostics"
1004 case agent.AgentToolName:
1005 var params agent.AgentParams
1006 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1007 return fmt.Sprintf("**Task:**\n%s", params.Prompt)
1008 }
1009 }
1010
1011 var params map[string]any
1012 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) == nil {
1013 var parts []string
1014 for key, value := range params {
1015 displayKey := strings.ReplaceAll(key, "_", " ")
1016 if len(displayKey) > 0 {
1017 displayKey = strings.ToUpper(displayKey[:1]) + displayKey[1:]
1018 }
1019 parts = append(parts, fmt.Sprintf("**%s:** %v", displayKey, value))
1020 }
1021 return strings.Join(parts, "\n")
1022 }
1023
1024 return ""
1025}
1026
1027// formatResultForCopy formats tool results for clipboard copying.
1028func (t *baseToolMessageItem) formatResultForCopy() string {
1029 if t.result == nil {
1030 return ""
1031 }
1032
1033 if t.result.Data != "" {
1034 if strings.HasPrefix(t.result.MIMEType, "image/") {
1035 return fmt.Sprintf("[Image: %s]", t.result.MIMEType)
1036 }
1037 return fmt.Sprintf("[Media: %s]", t.result.MIMEType)
1038 }
1039
1040 switch t.toolCall.Name {
1041 case tools.BashToolName:
1042 return t.formatBashResultForCopy()
1043 case tools.ViewToolName:
1044 return t.formatViewResultForCopy()
1045 case tools.EditToolName:
1046 return t.formatEditResultForCopy()
1047 case tools.MultiEditToolName:
1048 return t.formatMultiEditResultForCopy()
1049 case tools.WriteToolName:
1050 return t.formatWriteResultForCopy()
1051 case tools.FetchToolName:
1052 return t.formatFetchResultForCopy()
1053 case tools.AgenticFetchToolName:
1054 return t.formatAgenticFetchResultForCopy()
1055 case tools.WebFetchToolName:
1056 return t.formatWebFetchResultForCopy()
1057 case agent.AgentToolName:
1058 return t.formatAgentResultForCopy()
1059 case tools.DownloadToolName, tools.GrepToolName, tools.GlobToolName, tools.LSToolName, tools.SourcegraphToolName, tools.DiagnosticsToolName, tools.TodosToolName:
1060 return fmt.Sprintf("```\n%s\n```", t.result.Content)
1061 default:
1062 return t.result.Content
1063 }
1064}
1065
1066// formatBashResultForCopy formats bash tool results for clipboard.
1067func (t *baseToolMessageItem) formatBashResultForCopy() string {
1068 if t.result == nil {
1069 return ""
1070 }
1071
1072 var meta tools.BashResponseMetadata
1073 if t.result.Metadata != "" {
1074 json.Unmarshal([]byte(t.result.Metadata), &meta)
1075 }
1076
1077 output := meta.Output
1078 if output == "" && t.result.Content != tools.BashNoOutput {
1079 output = t.result.Content
1080 }
1081
1082 if output == "" {
1083 return ""
1084 }
1085
1086 return fmt.Sprintf("```bash\n%s\n```", output)
1087}
1088
1089// formatViewResultForCopy formats view tool results for clipboard.
1090func (t *baseToolMessageItem) formatViewResultForCopy() string {
1091 if t.result == nil {
1092 return ""
1093 }
1094
1095 var meta tools.ViewResponseMetadata
1096 if t.result.Metadata != "" {
1097 json.Unmarshal([]byte(t.result.Metadata), &meta)
1098 }
1099
1100 if meta.Content == "" {
1101 return t.result.Content
1102 }
1103
1104 lang := ""
1105 if meta.FilePath != "" {
1106 ext := strings.ToLower(filepath.Ext(meta.FilePath))
1107 switch ext {
1108 case ".go":
1109 lang = "go"
1110 case ".js", ".mjs":
1111 lang = "javascript"
1112 case ".ts":
1113 lang = "typescript"
1114 case ".py":
1115 lang = "python"
1116 case ".rs":
1117 lang = "rust"
1118 case ".java":
1119 lang = "java"
1120 case ".c":
1121 lang = "c"
1122 case ".cpp", ".cc", ".cxx":
1123 lang = "cpp"
1124 case ".sh", ".bash":
1125 lang = "bash"
1126 case ".json":
1127 lang = "json"
1128 case ".yaml", ".yml":
1129 lang = "yaml"
1130 case ".xml":
1131 lang = "xml"
1132 case ".html":
1133 lang = "html"
1134 case ".css":
1135 lang = "css"
1136 case ".md":
1137 lang = "markdown"
1138 }
1139 }
1140
1141 var result strings.Builder
1142 if lang != "" {
1143 fmt.Fprintf(&result, "```%s\n", lang)
1144 } else {
1145 result.WriteString("```\n")
1146 }
1147 result.WriteString(meta.Content)
1148 result.WriteString("\n```")
1149
1150 return result.String()
1151}
1152
1153// formatEditResultForCopy formats edit tool results for clipboard.
1154func (t *baseToolMessageItem) formatEditResultForCopy() string {
1155 if t.result == nil || t.result.Metadata == "" {
1156 if t.result != nil {
1157 return t.result.Content
1158 }
1159 return ""
1160 }
1161
1162 var meta tools.EditResponseMetadata
1163 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1164 return t.result.Content
1165 }
1166
1167 var params tools.EditParams
1168 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1169
1170 var result strings.Builder
1171
1172 if meta.OldContent != "" || meta.NewContent != "" {
1173 fileName := params.FilePath
1174 if fileName != "" {
1175 fileName = fsext.PrettyPath(fileName)
1176 }
1177 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1178
1179 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1180 result.WriteString("```diff\n")
1181 result.WriteString(diffContent)
1182 result.WriteString("\n```")
1183 }
1184
1185 return result.String()
1186}
1187
1188// formatMultiEditResultForCopy formats multi-edit tool results for clipboard.
1189func (t *baseToolMessageItem) formatMultiEditResultForCopy() string {
1190 if t.result == nil || t.result.Metadata == "" {
1191 if t.result != nil {
1192 return t.result.Content
1193 }
1194 return ""
1195 }
1196
1197 var meta tools.MultiEditResponseMetadata
1198 if json.Unmarshal([]byte(t.result.Metadata), &meta) != nil {
1199 return t.result.Content
1200 }
1201
1202 var params tools.MultiEditParams
1203 json.Unmarshal([]byte(t.toolCall.Input), ¶ms)
1204
1205 var result strings.Builder
1206 if meta.OldContent != "" || meta.NewContent != "" {
1207 fileName := params.FilePath
1208 if fileName != "" {
1209 fileName = fsext.PrettyPath(fileName)
1210 }
1211 diffContent, additions, removals := diff.GenerateDiff(meta.OldContent, meta.NewContent, fileName)
1212
1213 fmt.Fprintf(&result, "Changes: +%d -%d\n", additions, removals)
1214 result.WriteString("```diff\n")
1215 result.WriteString(diffContent)
1216 result.WriteString("\n```")
1217 }
1218
1219 return result.String()
1220}
1221
1222// formatWriteResultForCopy formats write tool results for clipboard.
1223func (t *baseToolMessageItem) formatWriteResultForCopy() string {
1224 if t.result == nil {
1225 return ""
1226 }
1227
1228 var params tools.WriteParams
1229 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1230 return t.result.Content
1231 }
1232
1233 lang := ""
1234 if params.FilePath != "" {
1235 ext := strings.ToLower(filepath.Ext(params.FilePath))
1236 switch ext {
1237 case ".go":
1238 lang = "go"
1239 case ".js", ".mjs":
1240 lang = "javascript"
1241 case ".ts":
1242 lang = "typescript"
1243 case ".py":
1244 lang = "python"
1245 case ".rs":
1246 lang = "rust"
1247 case ".java":
1248 lang = "java"
1249 case ".c":
1250 lang = "c"
1251 case ".cpp", ".cc", ".cxx":
1252 lang = "cpp"
1253 case ".sh", ".bash":
1254 lang = "bash"
1255 case ".json":
1256 lang = "json"
1257 case ".yaml", ".yml":
1258 lang = "yaml"
1259 case ".xml":
1260 lang = "xml"
1261 case ".html":
1262 lang = "html"
1263 case ".css":
1264 lang = "css"
1265 case ".md":
1266 lang = "markdown"
1267 }
1268 }
1269
1270 var result strings.Builder
1271 fmt.Fprintf(&result, "File: %s\n", fsext.PrettyPath(params.FilePath))
1272 if lang != "" {
1273 fmt.Fprintf(&result, "```%s\n", lang)
1274 } else {
1275 result.WriteString("```\n")
1276 }
1277 result.WriteString(params.Content)
1278 result.WriteString("\n```")
1279
1280 return result.String()
1281}
1282
1283// formatFetchResultForCopy formats fetch tool results for clipboard.
1284func (t *baseToolMessageItem) formatFetchResultForCopy() string {
1285 if t.result == nil {
1286 return ""
1287 }
1288
1289 var params tools.FetchParams
1290 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1291 return t.result.Content
1292 }
1293
1294 var result strings.Builder
1295 if params.URL != "" {
1296 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1297 }
1298 if params.Format != "" {
1299 fmt.Fprintf(&result, "Format: %s\n", params.Format)
1300 }
1301 if params.JQ != "" {
1302 fmt.Fprintf(&result, "jq: %s\n", params.JQ)
1303 }
1304 if params.Timeout > 0 {
1305 fmt.Fprintf(&result, "Timeout: %ds\n", params.Timeout)
1306 }
1307 result.WriteString("\n")
1308
1309 result.WriteString(t.result.Content)
1310
1311 return result.String()
1312}
1313
1314// formatAgenticFetchResultForCopy formats agentic fetch tool results for clipboard.
1315func (t *baseToolMessageItem) formatAgenticFetchResultForCopy() string {
1316 if t.result == nil {
1317 return ""
1318 }
1319
1320 var params tools.AgenticFetchParams
1321 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1322 return t.result.Content
1323 }
1324
1325 var result strings.Builder
1326 if params.URL != "" {
1327 fmt.Fprintf(&result, "URL: %s\n", params.URL)
1328 }
1329 if params.Prompt != "" {
1330 fmt.Fprintf(&result, "Prompt: %s\n\n", params.Prompt)
1331 }
1332
1333 result.WriteString("```markdown\n")
1334 result.WriteString(t.result.Content)
1335 result.WriteString("\n```")
1336
1337 return result.String()
1338}
1339
1340// formatWebFetchResultForCopy formats web fetch tool results for clipboard.
1341func (t *baseToolMessageItem) formatWebFetchResultForCopy() string {
1342 if t.result == nil {
1343 return ""
1344 }
1345
1346 var params tools.WebFetchParams
1347 if json.Unmarshal([]byte(t.toolCall.Input), ¶ms) != nil {
1348 return t.result.Content
1349 }
1350
1351 var result strings.Builder
1352 fmt.Fprintf(&result, "URL: %s\n\n", params.URL)
1353 result.WriteString("```markdown\n")
1354 result.WriteString(t.result.Content)
1355 result.WriteString("\n```")
1356
1357 return result.String()
1358}
1359
1360// formatAgentResultForCopy formats agent tool results for clipboard.
1361func (t *baseToolMessageItem) formatAgentResultForCopy() string {
1362 if t.result == nil {
1363 return ""
1364 }
1365
1366 var result strings.Builder
1367
1368 if t.result.Content != "" {
1369 fmt.Fprintf(&result, "```markdown\n%s\n```", t.result.Content)
1370 }
1371
1372 return result.String()
1373}
1374
1375// prettifyToolName returns a human-readable name for tool names.
1376func prettifyToolName(name string) string {
1377 switch name {
1378 case agent.AgentToolName:
1379 return "Agent"
1380 case tools.BashToolName:
1381 return "Bash"
1382 case tools.JobOutputToolName:
1383 return "Job: Output"
1384 case tools.JobKillToolName:
1385 return "Job: Kill"
1386 case tools.DownloadToolName:
1387 return "Download"
1388 case tools.EditToolName:
1389 return "Edit"
1390 case tools.MultiEditToolName:
1391 return "Multi-Edit"
1392 case tools.FetchToolName:
1393 return "Fetch"
1394 case tools.AgenticFetchToolName:
1395 return "Agentic Fetch"
1396 case tools.WebFetchToolName:
1397 return "Fetch"
1398 case tools.WebSearchToolName:
1399 return "Search"
1400 case tools.GlobToolName:
1401 return "Glob"
1402 case tools.GrepToolName:
1403 return "Grep"
1404 case tools.LSToolName:
1405 return "List"
1406 case tools.SourcegraphToolName:
1407 return "Sourcegraph"
1408 case tools.TodosToolName:
1409 return "To-Do"
1410 case tools.ViewToolName:
1411 return "View"
1412 case tools.WriteToolName:
1413 return "Write"
1414 default:
1415 return genericPrettyName(name)
1416 }
1417}