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