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