1package messages
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7 "time"
8
9 "github.com/charmbracelet/crush/internal/fsext"
10 "github.com/charmbracelet/crush/internal/llm/agent"
11 "github.com/charmbracelet/crush/internal/llm/tools"
12 "github.com/charmbracelet/crush/internal/tui/components/core"
13 "github.com/charmbracelet/crush/internal/tui/highlight"
14 "github.com/charmbracelet/crush/internal/tui/styles"
15 "github.com/charmbracelet/lipgloss/v2"
16 "github.com/charmbracelet/lipgloss/v2/tree"
17 "github.com/charmbracelet/x/ansi"
18)
19
20// responseContextHeight limits the number of lines displayed in tool output
21const responseContextHeight = 10
22
23// renderer defines the interface for tool-specific rendering implementations
24type renderer interface {
25 // Render returns the complete (already styled) tool‑call view, not
26 // including the outer border.
27 Render(v *toolCallCmp) string
28}
29
30// rendererFactory creates new renderer instances
31type rendererFactory func() renderer
32
33// renderRegistry manages the mapping of tool names to their renderers
34type renderRegistry map[string]rendererFactory
35
36// register adds a new renderer factory to the registry
37func (rr renderRegistry) register(name string, f rendererFactory) { rr[name] = f }
38
39// lookup retrieves a renderer for the given tool name, falling back to generic renderer
40func (rr renderRegistry) lookup(name string) renderer {
41 if f, ok := rr[name]; ok {
42 return f()
43 }
44 return genericRenderer{} // sensible fallback
45}
46
47// registry holds all registered tool renderers
48var registry = renderRegistry{}
49
50// baseRenderer provides common functionality for all tool renderers
51type baseRenderer struct{}
52
53// paramBuilder helps construct parameter lists for tool headers
54type paramBuilder struct {
55 args []string
56}
57
58// newParamBuilder creates a new parameter builder
59func newParamBuilder() *paramBuilder {
60 return ¶mBuilder{args: make([]string, 0)}
61}
62
63// addMain adds the main parameter (first argument)
64func (pb *paramBuilder) addMain(value string) *paramBuilder {
65 if value != "" {
66 pb.args = append(pb.args, value)
67 }
68 return pb
69}
70
71// addKeyValue adds a key-value pair parameter
72func (pb *paramBuilder) addKeyValue(key, value string) *paramBuilder {
73 if value != "" {
74 pb.args = append(pb.args, key, value)
75 }
76 return pb
77}
78
79// addFlag adds a boolean flag parameter
80func (pb *paramBuilder) addFlag(key string, value bool) *paramBuilder {
81 if value {
82 pb.args = append(pb.args, key, "true")
83 }
84 return pb
85}
86
87// build returns the final parameter list
88func (pb *paramBuilder) build() []string {
89 return pb.args
90}
91
92// renderWithParams provides a common rendering pattern for tools with parameters
93func (br baseRenderer) renderWithParams(v *toolCallCmp, toolName string, args []string, contentRenderer func() string) string {
94 width := v.textWidth()
95 if v.isNested {
96 width -= 4 // Adjust for nested tool call indentation
97 }
98 header := br.makeHeader(v, toolName, width, args...)
99 if v.isNested {
100 return v.style().Render(header)
101 }
102 if res, done := earlyState(header, v); done {
103 return res
104 }
105 body := contentRenderer()
106 return joinHeaderBody(header, body)
107}
108
109// unmarshalParams safely unmarshal JSON parameters
110func (br baseRenderer) unmarshalParams(input string, target any) error {
111 return json.Unmarshal([]byte(input), target)
112}
113
114// makeHeader builds the tool call header with status icon and parameters for a nested tool call.
115func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string {
116 t := styles.CurrentTheme()
117 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
118 if v.result.ToolCallID != "" {
119 if v.result.IsError {
120 icon = t.S().Base.Foreground(t.RedDark).Render(styles.ToolError)
121 } else {
122 icon = t.S().Base.Foreground(t.Green).Render(styles.ToolSuccess)
123 }
124 } else if v.cancelled {
125 icon = t.S().Muted.Render(styles.ToolPending)
126 }
127 tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " "
128 prefix := fmt.Sprintf("%s %s ", icon, tool)
129 return prefix + renderParamList(true, width-lipgloss.Width(tool), params...)
130}
131
132// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
133func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
134 if v.isNested {
135 return br.makeNestedHeader(v, tool, width, params...)
136 }
137 t := styles.CurrentTheme()
138 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
139 if v.result.ToolCallID != "" {
140 if v.result.IsError {
141 icon = t.S().Base.Foreground(t.RedDark).Render(styles.ToolError)
142 } else {
143 icon = t.S().Base.Foreground(t.Green).Render(styles.ToolSuccess)
144 }
145 } else if v.cancelled {
146 icon = t.S().Muted.Render(styles.ToolPending)
147 }
148 tool = t.S().Base.Foreground(t.Blue).Render(tool)
149 prefix := fmt.Sprintf("%s %s ", icon, tool)
150 return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
151}
152
153// renderError provides consistent error rendering
154func (br baseRenderer) renderError(v *toolCallCmp, message string) string {
155 t := styles.CurrentTheme()
156 header := br.makeHeader(v, prettifyToolName(v.call.Name), v.textWidth(), "")
157 errorTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
158 message = t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(message, v.textWidth()-3-lipgloss.Width(errorTag))) // -2 for padding and space
159 return joinHeaderBody(header, errorTag+" "+message)
160}
161
162// Register tool renderers
163func init() {
164 registry.register(tools.BashToolName, func() renderer { return bashRenderer{} })
165 registry.register(tools.ViewToolName, func() renderer { return viewRenderer{} })
166 registry.register(tools.EditToolName, func() renderer { return editRenderer{} })
167 registry.register(tools.WriteToolName, func() renderer { return writeRenderer{} })
168 registry.register(tools.FetchToolName, func() renderer { return fetchRenderer{} })
169 registry.register(tools.GlobToolName, func() renderer { return globRenderer{} })
170 registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
171 registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
172 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
173 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
174 registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
175}
176
177// -----------------------------------------------------------------------------
178// Generic renderer
179// -----------------------------------------------------------------------------
180
181// genericRenderer handles unknown tool types with basic parameter display
182type genericRenderer struct {
183 baseRenderer
184}
185
186// Render displays the tool call with its raw input and plain content output
187func (gr genericRenderer) Render(v *toolCallCmp) string {
188 return gr.renderWithParams(v, prettifyToolName(v.call.Name), []string{v.call.Input}, func() string {
189 return renderPlainContent(v, v.result.Content)
190 })
191}
192
193// -----------------------------------------------------------------------------
194// Bash renderer
195// -----------------------------------------------------------------------------
196
197// bashRenderer handles bash command execution display
198type bashRenderer struct {
199 baseRenderer
200}
201
202// Render displays the bash command with sanitized newlines and plain output
203func (br bashRenderer) Render(v *toolCallCmp) string {
204 var params tools.BashParams
205 if err := br.unmarshalParams(v.call.Input, ¶ms); err != nil {
206 return br.renderError(v, "Invalid bash parameters")
207 }
208
209 cmd := strings.ReplaceAll(params.Command, "\n", " ")
210 args := newParamBuilder().addMain(cmd).build()
211
212 return br.renderWithParams(v, "Bash", args, func() string {
213 if v.result.Content == tools.BashNoOutput {
214 return ""
215 }
216 return renderPlainContent(v, v.result.Content)
217 })
218}
219
220// -----------------------------------------------------------------------------
221// View renderer
222// -----------------------------------------------------------------------------
223
224// viewRenderer handles file viewing with syntax highlighting and line numbers
225type viewRenderer struct {
226 baseRenderer
227}
228
229// Render displays file content with optional limit and offset parameters
230func (vr viewRenderer) Render(v *toolCallCmp) string {
231 var params tools.ViewParams
232 if err := vr.unmarshalParams(v.call.Input, ¶ms); err != nil {
233 return vr.renderError(v, "Invalid view parameters")
234 }
235
236 file := fsext.PrettyPath(params.FilePath)
237 args := newParamBuilder().
238 addMain(file).
239 addKeyValue("limit", formatNonZero(params.Limit)).
240 addKeyValue("offset", formatNonZero(params.Offset)).
241 build()
242
243 return vr.renderWithParams(v, "View", args, func() string {
244 var meta tools.ViewResponseMetadata
245 if err := vr.unmarshalParams(v.result.Metadata, &meta); err != nil {
246 return renderPlainContent(v, v.result.Content)
247 }
248 return renderCodeContent(v, meta.FilePath, meta.Content, params.Offset)
249 })
250}
251
252// formatNonZero returns string representation of non-zero integers, empty string for zero
253func formatNonZero(value int) string {
254 if value == 0 {
255 return ""
256 }
257 return fmt.Sprintf("%d", value)
258}
259
260// -----------------------------------------------------------------------------
261// Edit renderer
262// -----------------------------------------------------------------------------
263
264// editRenderer handles file editing with diff visualization
265type editRenderer struct {
266 baseRenderer
267}
268
269// Render displays the edited file with a formatted diff of changes
270func (er editRenderer) Render(v *toolCallCmp) string {
271 t := styles.CurrentTheme()
272 var params tools.EditParams
273 var args []string
274 if err := er.unmarshalParams(v.call.Input, ¶ms); err == nil {
275 file := fsext.PrettyPath(params.FilePath)
276 args = newParamBuilder().addMain(file).build()
277 }
278
279 return er.renderWithParams(v, "Edit", args, func() string {
280 var meta tools.EditResponseMetadata
281 if err := er.unmarshalParams(v.result.Metadata, &meta); err != nil {
282 return renderPlainContent(v, v.result.Content)
283 }
284
285 formatter := core.DiffFormatter().
286 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
287 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
288 Width(v.textWidth() - 2) // -2 for padding
289 if v.textWidth() > 120 {
290 formatter = formatter.Split()
291 }
292 // add a message to the bottom if the content was truncated
293 formatted := formatter.String()
294 if lipgloss.Height(formatted) > responseContextHeight {
295 contentLines := strings.Split(formatted, "\n")
296 truncateMessage := t.S().Muted.
297 Background(t.BgBaseLighter).
298 PaddingLeft(2).
299 Width(v.textWidth() - 4).
300 Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
301 formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
302 }
303 return formatted
304 })
305}
306
307// -----------------------------------------------------------------------------
308// Write renderer
309// -----------------------------------------------------------------------------
310
311// writeRenderer handles file writing with syntax-highlighted content preview
312type writeRenderer struct {
313 baseRenderer
314}
315
316// Render displays the file being written with syntax highlighting
317func (wr writeRenderer) Render(v *toolCallCmp) string {
318 var params tools.WriteParams
319 var args []string
320 var file string
321 if err := wr.unmarshalParams(v.call.Input, ¶ms); err == nil {
322 file = fsext.PrettyPath(params.FilePath)
323 args = newParamBuilder().addMain(file).build()
324 }
325
326 return wr.renderWithParams(v, "Write", args, func() string {
327 return renderCodeContent(v, file, params.Content, 0)
328 })
329}
330
331// -----------------------------------------------------------------------------
332// Fetch renderer
333// -----------------------------------------------------------------------------
334
335// fetchRenderer handles URL fetching with format-specific content display
336type fetchRenderer struct {
337 baseRenderer
338}
339
340// Render displays the fetched URL with format and timeout parameters
341func (fr fetchRenderer) Render(v *toolCallCmp) string {
342 var params tools.FetchParams
343 var args []string
344 if err := fr.unmarshalParams(v.call.Input, ¶ms); err == nil {
345 args = newParamBuilder().
346 addMain(params.URL).
347 addKeyValue("format", params.Format).
348 addKeyValue("timeout", formatTimeout(params.Timeout)).
349 build()
350 }
351
352 return fr.renderWithParams(v, "Fetch", args, func() string {
353 file := fr.getFileExtension(params.Format)
354 return renderCodeContent(v, file, v.result.Content, 0)
355 })
356}
357
358// getFileExtension returns appropriate file extension for syntax highlighting
359func (fr fetchRenderer) getFileExtension(format string) string {
360 switch format {
361 case "text":
362 return "fetch.txt"
363 case "html":
364 return "fetch.html"
365 default:
366 return "fetch.md"
367 }
368}
369
370// formatTimeout converts timeout seconds to duration string
371func formatTimeout(timeout int) string {
372 if timeout == 0 {
373 return ""
374 }
375 return (time.Duration(timeout) * time.Second).String()
376}
377
378// -----------------------------------------------------------------------------
379// Glob renderer
380// -----------------------------------------------------------------------------
381
382// globRenderer handles file pattern matching with path filtering
383type globRenderer struct {
384 baseRenderer
385}
386
387// Render displays the glob pattern with optional path parameter
388func (gr globRenderer) Render(v *toolCallCmp) string {
389 var params tools.GlobParams
390 var args []string
391 if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil {
392 args = newParamBuilder().
393 addMain(params.Pattern).
394 addKeyValue("path", params.Path).
395 build()
396 }
397
398 return gr.renderWithParams(v, "Glob", args, func() string {
399 return renderPlainContent(v, v.result.Content)
400 })
401}
402
403// -----------------------------------------------------------------------------
404// Grep renderer
405// -----------------------------------------------------------------------------
406
407// grepRenderer handles content searching with pattern matching options
408type grepRenderer struct {
409 baseRenderer
410}
411
412// Render displays the search pattern with path, include, and literal text options
413func (gr grepRenderer) Render(v *toolCallCmp) string {
414 var params tools.GrepParams
415 var args []string
416 if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil {
417 args = newParamBuilder().
418 addMain(params.Pattern).
419 addKeyValue("path", params.Path).
420 addKeyValue("include", params.Include).
421 addFlag("literal", params.LiteralText).
422 build()
423 }
424
425 return gr.renderWithParams(v, "Grep", args, func() string {
426 return renderPlainContent(v, v.result.Content)
427 })
428}
429
430// -----------------------------------------------------------------------------
431// LS renderer
432// -----------------------------------------------------------------------------
433
434// lsRenderer handles directory listing with default path handling
435type lsRenderer struct {
436 baseRenderer
437}
438
439// Render displays the directory path, defaulting to current directory
440func (lr lsRenderer) Render(v *toolCallCmp) string {
441 var params tools.LSParams
442 var args []string
443 if err := lr.unmarshalParams(v.call.Input, ¶ms); err == nil {
444 path := params.Path
445 if path == "" {
446 path = "."
447 }
448 path = fsext.PrettyPath(path)
449
450 args = newParamBuilder().addMain(path).build()
451 }
452
453 return lr.renderWithParams(v, "List", args, func() string {
454 return renderPlainContent(v, v.result.Content)
455 })
456}
457
458// -----------------------------------------------------------------------------
459// Sourcegraph renderer
460// -----------------------------------------------------------------------------
461
462// sourcegraphRenderer handles code search with count and context options
463type sourcegraphRenderer struct {
464 baseRenderer
465}
466
467// Render displays the search query with optional count and context window parameters
468func (sr sourcegraphRenderer) Render(v *toolCallCmp) string {
469 var params tools.SourcegraphParams
470 var args []string
471 if err := sr.unmarshalParams(v.call.Input, ¶ms); err == nil {
472 args = newParamBuilder().
473 addMain(params.Query).
474 addKeyValue("count", formatNonZero(params.Count)).
475 addKeyValue("context", formatNonZero(params.ContextWindow)).
476 build()
477 }
478
479 return sr.renderWithParams(v, "Sourcegraph", args, func() string {
480 return renderPlainContent(v, v.result.Content)
481 })
482}
483
484// -----------------------------------------------------------------------------
485// Diagnostics renderer
486// -----------------------------------------------------------------------------
487
488// diagnosticsRenderer handles project-wide diagnostic information
489type diagnosticsRenderer struct {
490 baseRenderer
491}
492
493// Render displays project diagnostics with plain content formatting
494func (dr diagnosticsRenderer) Render(v *toolCallCmp) string {
495 args := newParamBuilder().addMain("project").build()
496
497 return dr.renderWithParams(v, "Diagnostics", args, func() string {
498 return renderPlainContent(v, v.result.Content)
499 })
500}
501
502// -----------------------------------------------------------------------------
503// Task renderer
504// -----------------------------------------------------------------------------
505
506// agentRenderer handles project-wide diagnostic information
507type agentRenderer struct {
508 baseRenderer
509}
510
511func RoundedEnumerator(children tree.Children, index int) string {
512 if children.Length()-1 == index {
513 return " ╰──"
514 }
515 return " ├──"
516}
517
518// Render displays agent task parameters and result content
519func (tr agentRenderer) Render(v *toolCallCmp) string {
520 t := styles.CurrentTheme()
521 var params agent.AgentParams
522 tr.unmarshalParams(v.call.Input, ¶ms)
523
524 prompt := params.Prompt
525 prompt = strings.ReplaceAll(prompt, "\n", " ")
526
527 header := tr.makeHeader(v, "Agent", v.textWidth())
528 if res, done := earlyState(header, v); v.cancelled && done {
529 return res
530 }
531 taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task")
532 remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding
533 prompt = t.S().Muted.Width(remainingWidth).Render(prompt)
534 header = lipgloss.JoinVertical(
535 lipgloss.Left,
536 header,
537 "",
538 lipgloss.JoinHorizontal(
539 lipgloss.Left,
540 taskTag,
541 " ",
542 prompt,
543 ),
544 )
545 childTools := tree.Root(header)
546
547 for _, call := range v.nestedToolCalls {
548 childTools.Child(call.View())
549 }
550 parts := []string{
551 childTools.Enumerator(RoundedEnumerator).String(),
552 }
553
554 if v.result.ToolCallID == "" {
555 v.spinning = true
556 parts = append(parts, "", v.anim.View())
557 } else {
558 v.spinning = false
559 }
560
561 header = lipgloss.JoinVertical(
562 lipgloss.Left,
563 parts...,
564 )
565
566 if v.result.ToolCallID == "" {
567 return header
568 }
569
570 body := renderPlainContent(v, v.result.Content)
571 return joinHeaderBody(header, body)
572}
573
574// renderParamList renders params, params[0] (params[1]=params[2] ....)
575func renderParamList(nested bool, paramsWidth int, params ...string) string {
576 t := styles.CurrentTheme()
577 if len(params) == 0 {
578 return ""
579 }
580 mainParam := params[0]
581 if len(mainParam) > paramsWidth {
582 mainParam = mainParam[:paramsWidth-3] + "…"
583 }
584
585 if len(params) == 1 {
586 if nested {
587 return t.S().Muted.Render(mainParam)
588 }
589 return t.S().Subtle.Render(mainParam)
590 }
591 otherParams := params[1:]
592 // create pairs of key/value
593 // if odd number of params, the last one is a key without value
594 if len(otherParams)%2 != 0 {
595 otherParams = append(otherParams, "")
596 }
597 parts := make([]string, 0, len(otherParams)/2)
598 for i := 0; i < len(otherParams); i += 2 {
599 key := otherParams[i]
600 value := otherParams[i+1]
601 if value == "" {
602 continue
603 }
604 parts = append(parts, fmt.Sprintf("%s=%s", key, value))
605 }
606
607 partsRendered := strings.Join(parts, ", ")
608 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
609 if remainingWidth < 30 {
610 if nested {
611 return t.S().Muted.Render(mainParam)
612 }
613 // No space for the params, just show the main
614 return t.S().Subtle.Render(mainParam)
615 }
616
617 if len(parts) > 0 {
618 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
619 }
620
621 if nested {
622 return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
623 }
624 return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
625}
626
627// earlyState returns immediately‑rendered error/cancelled/ongoing states.
628func earlyState(header string, v *toolCallCmp) (string, bool) {
629 t := styles.CurrentTheme()
630 message := ""
631 switch {
632 case v.result.IsError:
633 message = v.renderToolError()
634 case v.cancelled:
635 message = t.S().Base.Foreground(t.FgSubtle).Render("Canceled.")
636 case v.result.ToolCallID == "":
637 message = t.S().Base.Foreground(t.FgSubtle).Render("Waiting for tool to start...")
638 default:
639 return "", false
640 }
641
642 message = t.S().Base.PaddingLeft(2).Render(message)
643 return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
644}
645
646func joinHeaderBody(header, body string) string {
647 t := styles.CurrentTheme()
648 if body == "" {
649 return header
650 }
651 body = t.S().Base.PaddingLeft(2).Render(body)
652 return lipgloss.JoinVertical(lipgloss.Left, header, "", body, "")
653}
654
655func renderPlainContent(v *toolCallCmp, content string) string {
656 t := styles.CurrentTheme()
657 content = strings.TrimSpace(content)
658 lines := strings.Split(content, "\n")
659
660 width := v.textWidth() - 2 // -2 for left padding
661 var out []string
662 for i, ln := range lines {
663 if i >= responseContextHeight {
664 break
665 }
666 ln = " " + ln // left padding
667 if len(ln) > width {
668 ln = v.fit(ln, width)
669 }
670 out = append(out, t.S().Muted.
671 Width(width).
672 Background(t.BgBaseLighter).
673 Render(ln))
674 }
675
676 if len(lines) > responseContextHeight {
677 out = append(out, t.S().Muted.
678 Background(t.BgBaseLighter).
679 Width(width).
680 Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
681 }
682 return strings.Join(out, "\n")
683}
684
685func pad(v any, width int) string {
686 s := fmt.Sprintf("%v", v)
687 w := ansi.StringWidth(s)
688 if w >= width {
689 return s
690 }
691 return strings.Repeat(" ", width-w) + s
692}
693
694func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
695 t := styles.CurrentTheme()
696 truncated := truncateHeight(content, responseContextHeight)
697
698 highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgBase)
699 lines := strings.Split(highlighted, "\n")
700
701 if len(strings.Split(content, "\n")) > responseContextHeight {
702 lines = append(lines, t.S().Muted.
703 Background(t.BgBase).
704 Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
705 }
706
707 maxLineNumber := len(lines) + offset
708 padding := lipgloss.Width(fmt.Sprintf("%d", maxLineNumber))
709 for i, ln := range lines {
710 num := t.S().Base.
711 Foreground(t.FgMuted).
712 Background(t.BgBase).
713 PaddingRight(1).
714 PaddingLeft(1).
715 Render(pad(i+1+offset, padding))
716 w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding
717 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
718 num,
719 t.S().Base.
720 PaddingLeft(1).
721 Render(v.fit(ln, w-1)))
722 }
723 return lipgloss.JoinVertical(lipgloss.Left, lines...)
724}
725
726func (v *toolCallCmp) renderToolError() string {
727 t := styles.CurrentTheme()
728 err := strings.ReplaceAll(v.result.Content, "\n", " ")
729 errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
730 err = fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(err, v.textWidth()-2-lipgloss.Width(errTag))))
731 return err
732}
733
734func truncateHeight(s string, h int) string {
735 lines := strings.Split(s, "\n")
736 if len(lines) > h {
737 return strings.Join(lines[:h], "\n")
738 }
739 return s
740}
741
742func prettifyToolName(name string) string {
743 switch name {
744 case agent.AgentToolName:
745 return "Agent"
746 case tools.BashToolName:
747 return "Bash"
748 case tools.EditToolName:
749 return "Edit"
750 case tools.FetchToolName:
751 return "Fetch"
752 case tools.GlobToolName:
753 return "Glob"
754 case tools.GrepToolName:
755 return "Grep"
756 case tools.LSToolName:
757 return "List"
758 case tools.SourcegraphToolName:
759 return "Sourcegraph"
760 case tools.ViewToolName:
761 return "View"
762 case tools.WriteToolName:
763 return "Write"
764 default:
765 return name
766 }
767}