@@ -1,889 +0,0 @@
1−package toolrender
2−
3−import (
4− "cmp"
5− "encoding/json"
6− "fmt"
7− "strings"
8−
9− "charm.land/lipgloss/v2"
10− "charm.land/lipgloss/v2/tree"
11− "github.com/charmbracelet/crush/internal/agent"
12− "github.com/charmbracelet/crush/internal/agent/tools"
13− "github.com/charmbracelet/crush/internal/ansiext"
14− "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/message"
16− "github.com/charmbracelet/crush/internal/ui/common"
17− "github.com/charmbracelet/crush/internal/ui/styles"
18− "github.com/charmbracelet/x/ansi"
19−)
20−
21−// responseContextHeight limits the number of lines displayed in tool output.
22−const responseContextHeight = 10
23−
24−// RenderContext provides the context needed for rendering a tool call.
25−type RenderContext struct {
26− Call message.ToolCall
27− Result message.ToolResult
28− Cancelled bool
29− IsNested bool
30− Width int
31− Styles *styles.Styles
32−}
33−
34−// TextWidth returns the available width for content accounting for borders.
35−func (rc *RenderContext) TextWidth() int {
36− if rc.IsNested {
37− return rc.Width - 6
38− }
39− return rc.Width - 5
40−}
41−
42−// Fit truncates content to fit within the specified width with ellipsis.
43−func (rc *RenderContext) Fit(content string, width int) string {
44− lineStyle := rc.Styles.Muted
45− dots := lineStyle.Render("…")
46− return ansi.Truncate(content, width, dots)
47−}
48−
49−// Render renders a tool call using the appropriate renderer based on tool name.
50−func Render(ctx *RenderContext) string {
51− switch ctx.Call.Name {
52− case tools.ViewToolName:
53− return renderView(ctx)
54− case tools.EditToolName:
55− return renderEdit(ctx)
56− case tools.MultiEditToolName:
57− return renderMultiEdit(ctx)
58− case tools.WriteToolName:
59− return renderWrite(ctx)
60− case tools.BashToolName:
61− return renderBash(ctx)
62− case tools.JobOutputToolName:
63− return renderJobOutput(ctx)
64− case tools.JobKillToolName:
65− return renderJobKill(ctx)
66− case tools.FetchToolName:
67− return renderSimpleFetch(ctx)
68− case tools.AgenticFetchToolName:
69− return renderAgenticFetch(ctx)
70− case tools.WebFetchToolName:
71− return renderWebFetch(ctx)
72− case tools.DownloadToolName:
73− return renderDownload(ctx)
74− case tools.GlobToolName:
75− return renderGlob(ctx)
76− case tools.GrepToolName:
77− return renderGrep(ctx)
78− case tools.LSToolName:
79− return renderLS(ctx)
80− case tools.SourcegraphToolName:
81− return renderSourcegraph(ctx)
82− case tools.DiagnosticsToolName:
83− return renderDiagnostics(ctx)
84− case agent.AgentToolName:
85− return renderAgent(ctx)
86− default:
87− return renderGeneric(ctx)
88− }
89−}
90−
91−// Helper functions
92−
93−func unmarshalParams(input string, target any) error {
94− return json.Unmarshal([]byte(input), target)
95−}
96−
97−type paramBuilder struct {
98− args []string
99−}
100−
101−func newParamBuilder() *paramBuilder {
102− return ¶mBuilder{args: make([]string, 0)}
103−}
104−
105−func (pb *paramBuilder) addMain(value string) *paramBuilder {
106− if value != "" {
107− pb.args = append(pb.args, value)
108− }
109− return pb
110−}
111−
112−func (pb *paramBuilder) addKeyValue(key, value string) *paramBuilder {
113− if value != "" {
114− pb.args = append(pb.args, key, value)
115− }
116− return pb
117−}
118−
119−func (pb *paramBuilder) addFlag(key string, value bool) *paramBuilder {
120− if value {
121− pb.args = append(pb.args, key, "true")
122− }
123− return pb
124−}
125−
126−func (pb *paramBuilder) build() []string {
127− return pb.args
128−}
129−
130−func formatNonZero[T comparable](value T) string {
131− var zero T
132− if value == zero {
133− return ""
134− }
135− return fmt.Sprintf("%v", value)
136−}
137−
138−func makeHeader(ctx *RenderContext, toolName string, args []string) string {
139− if ctx.IsNested {
140− return makeNestedHeader(ctx, toolName, args)
141− }
142− s := ctx.Styles
143− var icon string
144− if ctx.Result.ToolCallID != "" {
145− if ctx.Result.IsError {
146− icon = s.Tool.IconError.Render()
147− } else {
148− icon = s.Tool.IconSuccess.Render()
149− }
150− } else if ctx.Cancelled {
151− icon = s.Tool.IconCancelled.Render()
152− } else {
153− icon = s.Tool.IconPending.Render()
154− }
155− tool := s.Tool.NameNormal.Render(toolName)
156− prefix := fmt.Sprintf("%s %s ", icon, tool)
157− return prefix + renderParamList(ctx, false, ctx.TextWidth()-lipgloss.Width(prefix), args...)
158−}
159−
160−func makeNestedHeader(ctx *RenderContext, toolName string, args []string) string {
161− s := ctx.Styles
162− var icon string
163− if ctx.Result.ToolCallID != "" {
164− if ctx.Result.IsError {
165− icon = s.Tool.IconError.Render()
166− } else {
167− icon = s.Tool.IconSuccess.Render()
168− }
169− } else if ctx.Cancelled {
170− icon = s.Tool.IconCancelled.Render()
171− } else {
172− icon = s.Tool.IconPending.Render()
173− }
174− tool := s.Tool.NameNested.Render(toolName)
175− prefix := fmt.Sprintf("%s %s ", icon, tool)
176− return prefix + renderParamList(ctx, true, ctx.TextWidth()-lipgloss.Width(prefix), args...)
177−}
178−
179−func renderParamList(ctx *RenderContext, nested bool, paramsWidth int, params ...string) string {
180− s := ctx.Styles
181− if len(params) == 0 {
182− return ""
183− }
184− mainParam := params[0]
185− if paramsWidth >= 0 && lipgloss.Width(mainParam) > paramsWidth {
186− mainParam = ansi.Truncate(mainParam, paramsWidth, "…")
187− }
188−
189− if len(params) == 1 {
190− return s.Tool.ParamMain.Render(mainParam)
191− }
192− otherParams := params[1:]
193− if len(otherParams)%2 != 0 {
194− otherParams = append(otherParams, "")
195− }
196− parts := make([]string, 0, len(otherParams)/2)
197− for i := 0; i < len(otherParams); i += 2 {
198− key := otherParams[i]
199− value := otherParams[i+1]
200− if value == "" {
201− continue
202− }
203− parts = append(parts, fmt.Sprintf("%s=%s", key, value))
204− }
205−
206− partsRendered := strings.Join(parts, ", ")
207− remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3
208− if remainingWidth < 30 {
209− return s.Tool.ParamMain.Render(mainParam)
210− }
211−
212− if len(parts) > 0 {
213− mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
214− }
215−
216− return s.Tool.ParamMain.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
217−}
218−
219−func earlyState(ctx *RenderContext, header string) (string, bool) {
220− s := ctx.Styles
221− message := ""
222− switch {
223− case ctx.Result.IsError:
224− message = renderToolError(ctx)
225− case ctx.Cancelled:
226− message = s.Tool.StateCancelled.Render("Canceled.")
227− case ctx.Result.ToolCallID == "":
228− message = s.Tool.StateWaiting.Render("Waiting for tool response...")
229− default:
230− return "", false
231− }
232−
233− message = s.Tool.BodyPadding.Render(message)
234− return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
235−}
236−
237−func renderToolError(ctx *RenderContext) string {
238− s := ctx.Styles
239− errTag := s.Tool.ErrorTag.Render("ERROR")
240− msg := ctx.Result.Content
241− if msg == "" {
242− msg = "An error occurred"
243− }
244− truncated := ansi.Truncate(msg, ctx.TextWidth()-3-lipgloss.Width(errTag), "…")
245− return errTag + " " + s.Tool.ErrorMessage.Render(truncated)
246−}
247−
248−func joinHeaderBody(ctx *RenderContext, header, body string) string {
249− s := ctx.Styles
250− if body == "" {
251− return header
252− }
253− body = s.Tool.BodyPadding.Render(body)
254− return lipgloss.JoinVertical(lipgloss.Left, header, "", body)
255−}
256−
257−func renderWithParams(ctx *RenderContext, toolName string, args []string, contentRenderer func() string) string {
258− header := makeHeader(ctx, toolName, args)
259− if ctx.IsNested {
260− return header
261− }
262− if res, done := earlyState(ctx, header); done {
263− return res
264− }
265− body := contentRenderer()
266− return joinHeaderBody(ctx, header, body)
267−}
268−
269−func renderError(ctx *RenderContext, message string) string {
270− s := ctx.Styles
271− header := makeHeader(ctx, prettifyToolName(ctx.Call.Name), []string{})
272− errorTag := s.Tool.ErrorTag.Render("ERROR")
273− message = s.Tool.ErrorMessage.Render(ctx.Fit(message, ctx.TextWidth()-3-lipgloss.Width(errorTag)))
274− return joinHeaderBody(ctx, header, errorTag+" "+message)
275−}
276−
277−func renderPlainContent(ctx *RenderContext, content string) string {
278− s := ctx.Styles
279− content = strings.ReplaceAll(content, "\r\n", "\n")
280− content = strings.ReplaceAll(content, "\t", " ")
281− content = strings.TrimSpace(content)
282− lines := strings.Split(content, "\n")
283−
284− width := ctx.TextWidth() - 2
285− var out []string
286− for i, ln := range lines {
287− if i >= responseContextHeight {
288− break
289− }
290− ln = ansiext.Escape(ln)
291− ln = " " + ln
292− if len(ln) > width {
293− ln = ctx.Fit(ln, width)
294− }
295− out = append(out, s.Tool.ContentLine.Width(width).Render(ln))
296− }
297−
298− if len(lines) > responseContextHeight {
299− out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
300− }
301−
302− return strings.Join(out, "\n")
303−}
304−
305−func renderMarkdownContent(ctx *RenderContext, content string) string {
306− s := ctx.Styles
307− content = strings.ReplaceAll(content, "\r\n", "\n")
308− content = strings.ReplaceAll(content, "\t", " ")
309− content = strings.TrimSpace(content)
310−
311− width := ctx.TextWidth() - 2
312− width = min(width, 120)
313−
314− renderer := common.PlainMarkdownRenderer(width)
315− rendered, err := renderer.Render(content)
316− if err != nil {
317− return renderPlainContent(ctx, content)
318− }
319−
320− lines := strings.Split(rendered, "\n")
321−
322− var out []string
323− for i, ln := range lines {
324− if i >= responseContextHeight {
325− break
326− }
327− out = append(out, ln)
328− }
329−
330− style := s.Tool.ContentLine
331− if len(lines) > responseContextHeight {
332− out = append(out, s.Tool.ContentTruncation.
333− Width(width-2).
334− Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
335− }
336−
337− return style.Render(strings.Join(out, "\n"))
338−}
339−
340−func renderCodeContent(ctx *RenderContext, path, content string, offset int) string {
341− s := ctx.Styles
342− content = strings.ReplaceAll(content, "\r\n", "\n")
343− content = strings.ReplaceAll(content, "\t", " ")
344− truncated := truncateHeight(content, responseContextHeight)
345−
346− lines := strings.Split(truncated, "\n")
347− for i, ln := range lines {
348− lines[i] = ansiext.Escape(ln)
349− }
350−
351− bg := s.Tool.ContentCodeBg
352− highlighted, _ := common.SyntaxHighlight(ctx.Styles, strings.Join(lines, "\n"), path, bg)
353− lines = strings.Split(highlighted, "\n")
354−
355− width := ctx.TextWidth() - 2
356− gutterWidth := getDigits(offset+len(lines)) + 1
357−
358− var out []string
359− for i, ln := range lines {
360− lineNum := fmt.Sprintf("%*d", gutterWidth, offset+i+1)
361− gutter := s.Subtle.Render(lineNum + " ")
362− ln = " " + ln
363− if lipgloss.Width(gutter+ln) > width {
364− ln = ctx.Fit(ln, width-lipgloss.Width(gutter))
365− }
366− out = append(out, s.Tool.ContentCodeLine.Width(width).Render(gutter+ln))
367− }
368−
369− contentLines := strings.Split(content, "\n")
370− if len(contentLines) > responseContextHeight {
371− out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight)))
372− }
373−
374− return strings.Join(out, "\n")
375−}
376−
377−func getDigits(n int) int {
378− if n == 0 {
379− return 1
380− }
381− if n < 0 {
382− n = -n
383− }
384−
385− digits := 0
386− for n > 0 {
387− n /= 10
388− digits++
389− }
390−
391− return digits
392−}
393−
394−func truncateHeight(content string, maxLines int) string {
395− lines := strings.Split(content, "\n")
396− if len(lines) <= maxLines {
397− return content
398− }
399− return strings.Join(lines[:maxLines], "\n")
400−}
401−
402−func prettifyToolName(name string) string {
403− switch name {
404− case "agent":
405− return "Agent"
406− case "bash":
407− return "Bash"
408− case "job_output":
409− return "Job: Output"
410− case "job_kill":
411− return "Job: Kill"
412− case "download":
413− return "Download"
414− case "edit":
415− return "Edit"
416− case "multiedit":
417− return "Multi-Edit"
418− case "fetch":
419− return "Fetch"
420− case "agentic_fetch":
421− return "Agentic Fetch"
422− case "web_fetch":
423− return "Fetching"
424− case "glob":
425− return "Glob"
426− case "grep":
427− return "Grep"
428− case "ls":
429− return "List"
430− case "sourcegraph":
431− return "Sourcegraph"
432− case "view":
433− return "View"
434− case "write":
435− return "Write"
436− case "lsp_references":
437− return "Find References"
438− case "lsp_diagnostics":
439− return "Diagnostics"
440− default:
441− parts := strings.Split(name, "_")
442− for i := range parts {
443− if len(parts[i]) > 0 {
444− parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
445− }
446− }
447− return strings.Join(parts, " ")
448− }
449−}
450−
451−// Tool-specific renderers
452−
453−func renderGeneric(ctx *RenderContext) string {
454− return renderWithParams(ctx, prettifyToolName(ctx.Call.Name), []string{ctx.Call.Input}, func() string {
455− return renderPlainContent(ctx, ctx.Result.Content)
456− })
457−}
458−
459−func renderView(ctx *RenderContext) string {
460− var params tools.ViewParams
461− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
462− return renderError(ctx, "Invalid view parameters")
463− }
464−
465− file := fsext.PrettyPath(params.FilePath)
466− args := newParamBuilder().
467− addMain(file).
468− addKeyValue("limit", formatNonZero(params.Limit)).
469− addKeyValue("offset", formatNonZero(params.Offset)).
470− build()
471−
472− return renderWithParams(ctx, "View", args, func() string {
473− var meta tools.ViewResponseMetadata
474− if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
475− return renderPlainContent(ctx, ctx.Result.Content)
476− }
477− return renderCodeContent(ctx, meta.FilePath, meta.Content, params.Offset)
478− })
479−}
480−
481−func renderEdit(ctx *RenderContext) string {
482− s := ctx.Styles
483− var params tools.EditParams
484− var args []string
485− if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
486− file := fsext.PrettyPath(params.FilePath)
487− args = newParamBuilder().addMain(file).build()
488− }
489−
490− return renderWithParams(ctx, "Edit", args, func() string {
491− var meta tools.EditResponseMetadata
492− if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
493− return renderPlainContent(ctx, ctx.Result.Content)
494− }
495−
496− formatter := common.DiffFormatter(ctx.Styles).
497− Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
498− After(fsext.PrettyPath(params.FilePath), meta.NewContent).
499− Width(ctx.TextWidth() - 2)
500− if ctx.TextWidth() > 120 {
501− formatter = formatter.Split()
502− }
503− formatted := formatter.String()
504− if lipgloss.Height(formatted) > responseContextHeight {
505− contentLines := strings.Split(formatted, "\n")
506− truncateMessage := s.Tool.DiffTruncation.
507− Width(ctx.TextWidth() - 2).
508− Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
509− formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
510− }
511− return formatted
512− })
513−}
514−
515−func renderMultiEdit(ctx *RenderContext) string {
516− s := ctx.Styles
517− var params tools.MultiEditParams
518− var args []string
519− if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
520− file := fsext.PrettyPath(params.FilePath)
521− args = newParamBuilder().
522− addMain(file).
523− addKeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
524− build()
525− }
526−
527− return renderWithParams(ctx, "Multi-Edit", args, func() string {
528− var meta tools.MultiEditResponseMetadata
529− if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
530− return renderPlainContent(ctx, ctx.Result.Content)
531− }
532−
533− formatter := common.DiffFormatter(ctx.Styles).
534− Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
535− After(fsext.PrettyPath(params.FilePath), meta.NewContent).
536− Width(ctx.TextWidth() - 2)
537− if ctx.TextWidth() > 120 {
538− formatter = formatter.Split()
539− }
540− formatted := formatter.String()
541− if lipgloss.Height(formatted) > responseContextHeight {
542− contentLines := strings.Split(formatted, "\n")
543− truncateMessage := s.Tool.DiffTruncation.
544− Width(ctx.TextWidth() - 2).
545− Render(fmt.Sprintf("… (%d lines)", len(contentLines)-responseContextHeight))
546− formatted = strings.Join(contentLines[:responseContextHeight], "\n") + "\n" + truncateMessage
547− }
548−
549− // Add note about failed edits if any.
550− if len(meta.EditsFailed) > 0 {
551− noteTag := s.Tool.NoteTag.Render("NOTE")
552− noteMsg := s.Tool.NoteMessage.Render(
553− fmt.Sprintf("%d of %d edits failed", len(meta.EditsFailed), len(params.Edits)))
554− formatted = formatted + "\n\n" + noteTag + " " + noteMsg
555− }
556−
557− return formatted
558− })
559−}
560−
561−func renderWrite(ctx *RenderContext) string {
562− var params tools.WriteParams
563− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
564− return renderError(ctx, "Invalid write parameters")
565− }
566−
567− file := fsext.PrettyPath(params.FilePath)
568− args := newParamBuilder().addMain(file).build()
569−
570− return renderWithParams(ctx, "Write", args, func() string {
571− return renderCodeContent(ctx, params.FilePath, params.Content, 0)
572− })
573−}
574−
575−func renderBash(ctx *RenderContext) string {
576− var params tools.BashParams
577− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
578− return renderError(ctx, "Invalid bash parameters")
579− }
580−
581− cmd := strings.ReplaceAll(params.Command, "\n", " ")
582− cmd = strings.ReplaceAll(cmd, "\t", " ")
583− args := newParamBuilder().
584− addMain(cmd).
585− addFlag("background", params.RunInBackground).
586− build()
587−
588− if ctx.Call.Finished {
589− var meta tools.BashResponseMetadata
590− _ = unmarshalParams(ctx.Result.Metadata, &meta)
591− if meta.Background {
592− description := cmp.Or(meta.Description, params.Command)
593− width := ctx.TextWidth()
594− if ctx.IsNested {
595− width -= 4
596− }
597− header := makeJobHeader(ctx, "Start", fmt.Sprintf("PID %s", meta.ShellID), description, width)
598− if ctx.IsNested {
599− return header
600− }
601− if res, done := earlyState(ctx, header); done {
602− return res
603− }
604− content := "Command: " + params.Command + "\n" + ctx.Result.Content
605− body := renderPlainContent(ctx, content)
606− return joinHeaderBody(ctx, header, body)
607− }
608− }
609−
610− return renderWithParams(ctx, "Bash", args, func() string {
611− var meta tools.BashResponseMetadata
612− if err := unmarshalParams(ctx.Result.Metadata, &meta); err != nil {
613− return renderPlainContent(ctx, ctx.Result.Content)
614− }
615− if meta.Output == "" && ctx.Result.Content != tools.BashNoOutput {
616− meta.Output = ctx.Result.Content
617− }
618−
619− if meta.Output == "" {
620− return ""
621− }
622− return renderPlainContent(ctx, meta.Output)
623− })
624−}
625−
626−func makeJobHeader(ctx *RenderContext, action, pid, description string, width int) string {
627− s := ctx.Styles
628− icon := s.Tool.JobIconPending.Render(styles.ToolPending)
629− if ctx.Result.ToolCallID != "" {
630− if ctx.Result.IsError {
631− icon = s.Tool.JobIconError.Render(styles.ToolError)
632− } else {
633− icon = s.Tool.JobIconSuccess.Render(styles.ToolSuccess)
634− }
635− } else if ctx.Cancelled {
636− icon = s.Muted.Render(styles.ToolPending)
637− }
638−
639− toolName := s.Tool.JobToolName.Render("Bash")
640− actionPart := s.Tool.JobAction.Render(action)
641− pidPart := s.Tool.JobPID.Render(pid)
642−
643− prefix := fmt.Sprintf("%s %s %s %s ", icon, toolName, actionPart, pidPart)
644− remainingWidth := width - lipgloss.Width(prefix)
645−
646− descDisplay := ansi.Truncate(description, remainingWidth, "…")
647− descDisplay = s.Tool.JobDescription.Render(descDisplay)
648−
649− return prefix + descDisplay
650−}
651−
652−func renderJobOutput(ctx *RenderContext) string {
653− var params tools.JobOutputParams
654− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
655− return renderError(ctx, "Invalid job output parameters")
656− }
657−
658− width := ctx.TextWidth()
659− if ctx.IsNested {
660− width -= 4
661− }
662−
663− var meta tools.JobOutputResponseMetadata
664− _ = unmarshalParams(ctx.Result.Metadata, &meta)
665− description := cmp.Or(meta.Description, meta.Command)
666−
667− header := makeJobHeader(ctx, "Output", fmt.Sprintf("PID %s", params.ShellID), description, width)
668− if ctx.IsNested {
669− return header
670− }
671− if res, done := earlyState(ctx, header); done {
672− return res
673− }
674− body := renderPlainContent(ctx, ctx.Result.Content)
675− return joinHeaderBody(ctx, header, body)
676−}
677−
678−func renderJobKill(ctx *RenderContext) string {
679− var params tools.JobKillParams
680− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
681− return renderError(ctx, "Invalid job kill parameters")
682− }
683−
684− width := ctx.TextWidth()
685− if ctx.IsNested {
686− width -= 4
687− }
688−
689− var meta tools.JobKillResponseMetadata
690− _ = unmarshalParams(ctx.Result.Metadata, &meta)
691− description := cmp.Or(meta.Description, meta.Command)
692−
693− header := makeJobHeader(ctx, "Kill", fmt.Sprintf("PID %s", params.ShellID), description, width)
694− if ctx.IsNested {
695− return header
696− }
697− if res, done := earlyState(ctx, header); done {
698− return res
699− }
700− body := renderPlainContent(ctx, ctx.Result.Content)
701− return joinHeaderBody(ctx, header, body)
702−}
703−
704−func renderSimpleFetch(ctx *RenderContext) string {
705− var params tools.FetchParams
706− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
707− return renderError(ctx, "Invalid fetch parameters")
708− }
709−
710− args := newParamBuilder().
711− addMain(params.URL).
712− addKeyValue("format", params.Format).
713− addKeyValue("timeout", formatNonZero(params.Timeout)).
714− build()
715−
716− return renderWithParams(ctx, "Fetch", args, func() string {
717− path := "file." + params.Format
718− return renderCodeContent(ctx, path, ctx.Result.Content, 0)
719− })
720−}
721−
722−func renderAgenticFetch(ctx *RenderContext) string {
723− // TODO: Implement nested tool call rendering with tree.
724− return renderGeneric(ctx)
725−}
726−
727−func renderWebFetch(ctx *RenderContext) string {
728− var params tools.WebFetchParams
729− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
730− return renderError(ctx, "Invalid web fetch parameters")
731− }
732−
733− args := newParamBuilder().addMain(params.URL).build()
734−
735− return renderWithParams(ctx, "Fetching", args, func() string {
736− return renderMarkdownContent(ctx, ctx.Result.Content)
737− })
738−}
739−
740−func renderDownload(ctx *RenderContext) string {
741− var params tools.DownloadParams
742− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
743− return renderError(ctx, "Invalid download parameters")
744− }
745−
746− args := newParamBuilder().
747− addMain(params.URL).
748− addKeyValue("file", fsext.PrettyPath(params.FilePath)).
749− addKeyValue("timeout", formatNonZero(params.Timeout)).
750− build()
751−
752− return renderWithParams(ctx, "Download", args, func() string {
753− return renderPlainContent(ctx, ctx.Result.Content)
754− })
755−}
756−
757−func renderGlob(ctx *RenderContext) string {
758− var params tools.GlobParams
759− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
760− return renderError(ctx, "Invalid glob parameters")
761− }
762−
763− args := newParamBuilder().
764− addMain(params.Pattern).
765− addKeyValue("path", params.Path).
766− build()
767−
768− return renderWithParams(ctx, "Glob", args, func() string {
769− return renderPlainContent(ctx, ctx.Result.Content)
770− })
771−}
772−
773−func renderGrep(ctx *RenderContext) string {
774− var params tools.GrepParams
775− var args []string
776− if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
777− args = newParamBuilder().
778− addMain(params.Pattern).
779− addKeyValue("path", params.Path).
780− addKeyValue("include", params.Include).
781− addFlag("literal", params.LiteralText).
782− build()
783− }
784−
785− return renderWithParams(ctx, "Grep", args, func() string {
786− return renderPlainContent(ctx, ctx.Result.Content)
787− })
788−}
789−
790−func renderLS(ctx *RenderContext) string {
791− var params tools.LSParams
792− path := cmp.Or(params.Path, ".")
793− args := newParamBuilder().addMain(path).build()
794−
795− if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil && params.Path != "" {
796− args = newParamBuilder().addMain(params.Path).build()
797− }
798−
799− return renderWithParams(ctx, "List", args, func() string {
800− return renderPlainContent(ctx, ctx.Result.Content)
801− })
802−}
803−
804−func renderSourcegraph(ctx *RenderContext) string {
805− var params tools.SourcegraphParams
806− if err := unmarshalParams(ctx.Call.Input, ¶ms); err != nil {
807− return renderError(ctx, "Invalid sourcegraph parameters")
808− }
809−
810− args := newParamBuilder().
811− addMain(params.Query).
812− addKeyValue("count", formatNonZero(params.Count)).
813− addKeyValue("context", formatNonZero(params.ContextWindow)).
814− build()
815−
816− return renderWithParams(ctx, "Sourcegraph", args, func() string {
817− return renderPlainContent(ctx, ctx.Result.Content)
818− })
819−}
820−
821−func renderDiagnostics(ctx *RenderContext) string {
822− args := newParamBuilder().addMain("project").build()
823−
824− return renderWithParams(ctx, "Diagnostics", args, func() string {
825− return renderPlainContent(ctx, ctx.Result.Content)
826− })
827−}
828−
829−func renderAgent(ctx *RenderContext) string {
830− s := ctx.Styles
831− var params agent.AgentParams
832− unmarshalParams(ctx.Call.Input, ¶ms)
833−
834− prompt := params.Prompt
835− prompt = strings.ReplaceAll(prompt, "\n", " ")
836−
837− header := makeHeader(ctx, "Agent", []string{})
838− if res, done := earlyState(ctx, header); ctx.Cancelled && done {
839− return res
840− }
841− taskTag := s.Tool.AgentTaskTag.Render("Task")
842− remainingWidth := ctx.TextWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2
843− remainingWidth = min(remainingWidth, 120-lipgloss.Width(taskTag)-2)
844− prompt = s.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
845− header = lipgloss.JoinVertical(
846− lipgloss.Left,
847− header,
848− "",
849− lipgloss.JoinHorizontal(
850− lipgloss.Left,
851− taskTag,
852− " ",
853− prompt,
854− ),
855− )
856− childTools := tree.Root(header)
857−
858− // TODO: Render nested tool calls when available.
859−
860− parts := []string{
861− childTools.Enumerator(roundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
862− }
863−
864− if ctx.Result.ToolCallID == "" {
865− // Pending state - would show animation in TUI.
866− parts = append(parts, "", s.Subtle.Render("Working..."))
867− }
868−
869− header = lipgloss.JoinVertical(
870− lipgloss.Left,
871− parts...,
872− )
873−
874− if ctx.Result.ToolCallID == "" {
875− return header
876− }
877−
878− body := renderMarkdownContent(ctx, ctx.Result.Content)
879− return joinHeaderBody(ctx, header, body)
880−}
881−
882−func roundedEnumeratorWithWidth(width int, offset int) func(tree.Children, int) string {
883− return func(children tree.Children, i int) string {
884− if children.Length()-1 == i {
885− return strings.Repeat(" ", offset) + "└" + strings.Repeat("─", width-1) + " "
886− }
887− return strings.Repeat(" ", offset) + "├" + strings.Repeat("─", width-1) + " "
888− }
889−}