@@ -0,0 +1,889 @@
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"
21+// responseContextHeight limits the number of lines displayed in tool output.
22+const responseContextHeight = 10
24+// RenderContext provides the context needed for rendering a tool call.
25+type RenderContext struct {
26+ Call message.ToolCall
27+ Result message.ToolResult
31+ Styles *styles.Styles
34+// TextWidth returns the available width for content accounting for borders.
35+func (rc *RenderContext) TextWidth() int {
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)
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:
80+ case tools.SourcegraphToolName:
81+ return renderSourcegraph(ctx)
82+ case tools.DiagnosticsToolName:
83+ return renderDiagnostics(ctx)
84+ case agent.AgentToolName:
85+ return renderAgent(ctx)
87+ return renderGeneric(ctx)
93+func unmarshalParams(input string, target any) error {
94+ return json.Unmarshal([]byte(input), target)
97+type paramBuilder struct {
101+func newParamBuilder() *paramBuilder {
102+ return ¶mBuilder{args: make([]string, 0)}
105+func (pb *paramBuilder) addMain(value string) *paramBuilder {
107+ pb.args = append(pb.args, value)
112+func (pb *paramBuilder) addKeyValue(key, value string) *paramBuilder {
114+ pb.args = append(pb.args, key, value)
119+func (pb *paramBuilder) addFlag(key string, value bool) *paramBuilder {
121+ pb.args = append(pb.args, key, "true")
126+func (pb *paramBuilder) build() []string {
130+func formatNonZero[T comparable](value T) string {
135+ return fmt.Sprintf("%v", value)
138+func makeHeader(ctx *RenderContext, toolName string, args []string) string {
140+ return makeNestedHeader(ctx, toolName, args)
144+ if ctx.Result.ToolCallID != "" {
145+ if ctx.Result.IsError {
146+ icon = s.Tool.IconError.Render()
148+ icon = s.Tool.IconSuccess.Render()
150+ } else if ctx.Cancelled {
151+ icon = s.Tool.IconCancelled.Render()
153+ icon = s.Tool.IconPending.Render()
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...)
160+func makeNestedHeader(ctx *RenderContext, toolName string, args []string) string {
163+ if ctx.Result.ToolCallID != "" {
164+ if ctx.Result.IsError {
165+ icon = s.Tool.IconError.Render()
167+ icon = s.Tool.IconSuccess.Render()
169+ } else if ctx.Cancelled {
170+ icon = s.Tool.IconCancelled.Render()
172+ icon = s.Tool.IconPending.Render()
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...)
179+func renderParamList(ctx *RenderContext, nested bool, paramsWidth int, params ...string) string {
181+ if len(params) == 0 {
184+ mainParam := params[0]
185+ if paramsWidth >= 0 && lipgloss.Width(mainParam) > paramsWidth {
186+ mainParam = ansi.Truncate(mainParam, paramsWidth, "…")
189+ if len(params) == 1 {
190+ return s.Tool.ParamMain.Render(mainParam)
192+ otherParams := params[1:]
193+ if len(otherParams)%2 != 0 {
194+ otherParams = append(otherParams, "")
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]
203+ parts = append(parts, fmt.Sprintf("%s=%s", key, value))
206+ partsRendered := strings.Join(parts, ", ")
207+ remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3
208+ if remainingWidth < 30 {
209+ return s.Tool.ParamMain.Render(mainParam)
213+ mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
216+ return s.Tool.ParamMain.Render(ansi.Truncate(mainParam, paramsWidth, "…"))
219+func earlyState(ctx *RenderContext, header string) (string, bool) {
223+ case ctx.Result.IsError:
224+ message = renderToolError(ctx)
226+ message = s.Tool.StateCancelled.Render("Canceled.")
227+ case ctx.Result.ToolCallID == "":
228+ message = s.Tool.StateWaiting.Render("Waiting for tool response...")
233+ message = s.Tool.BodyPadding.Render(message)
234+ return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
237+func renderToolError(ctx *RenderContext) string {
239+ errTag := s.Tool.ErrorTag.Render("ERROR")
240+ msg := ctx.Result.Content
242+ msg = "An error occurred"
244+ truncated := ansi.Truncate(msg, ctx.TextWidth()-3-lipgloss.Width(errTag), "…")
245+ return errTag + " " + s.Tool.ErrorMessage.Render(truncated)
248+func joinHeaderBody(ctx *RenderContext, header, body string) string {
253+ body = s.Tool.BodyPadding.Render(body)
254+ return lipgloss.JoinVertical(lipgloss.Left, header, "", body)
257+func renderWithParams(ctx *RenderContext, toolName string, args []string, contentRenderer func() string) string {
258+ header := makeHeader(ctx, toolName, args)
262+ if res, done := earlyState(ctx, header); done {
265+ body := contentRenderer()
266+ return joinHeaderBody(ctx, header, body)
269+func renderError(ctx *RenderContext, message string) string {
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)
277+func renderPlainContent(ctx *RenderContext, content string) string {
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")
284+ width := ctx.TextWidth() - 2
286+ for i, ln := range lines {
287+ if i >= responseContextHeight {
290+ ln = ansiext.Escape(ln)
292+ if len(ln) > width {
293+ ln = ctx.Fit(ln, width)
295+ out = append(out, s.Tool.ContentLine.Width(width).Render(ln))
298+ if len(lines) > responseContextHeight {
299+ out = append(out, s.Tool.ContentTruncation.Width(width).Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
302+ return strings.Join(out, "\n")
305+func renderMarkdownContent(ctx *RenderContext, content string) string {
307+ content = strings.ReplaceAll(content, "\r\n", "\n")
308+ content = strings.ReplaceAll(content, "\t", " ")
309+ content = strings.TrimSpace(content)
311+ width := ctx.TextWidth() - 2
312+ width = min(width, 120)
314+ renderer := common.PlainMarkdownRenderer(width)
315+ rendered, err := renderer.Render(content)
317+ return renderPlainContent(ctx, content)
320+ lines := strings.Split(rendered, "\n")
323+ for i, ln := range lines {
324+ if i >= responseContextHeight {
327+ out = append(out, ln)
330+ style := s.Tool.ContentLine
331+ if len(lines) > responseContextHeight {
332+ out = append(out, s.Tool.ContentTruncation.
334+ Render(fmt.Sprintf("… (%d lines)", len(lines)-responseContextHeight)))
337+ return style.Render(strings.Join(out, "\n"))
340+func renderCodeContent(ctx *RenderContext, path, content string, offset int) string {
342+ content = strings.ReplaceAll(content, "\r\n", "\n")
343+ content = strings.ReplaceAll(content, "\t", " ")
344+ truncated := truncateHeight(content, responseContextHeight)
346+ lines := strings.Split(truncated, "\n")
347+ for i, ln := range lines {
348+ lines[i] = ansiext.Escape(ln)
351+ bg := s.Tool.ContentCodeBg
352+ highlighted, _ := common.SyntaxHighlight(ctx.Styles, strings.Join(lines, "\n"), path, bg)
353+ lines = strings.Split(highlighted, "\n")
355+ width := ctx.TextWidth() - 2
356+ gutterWidth := getDigits(offset+len(lines)) + 1
359+ for i, ln := range lines {
360+ lineNum := fmt.Sprintf("%*d", gutterWidth, offset+i+1)
361+ gutter := s.Subtle.Render(lineNum + " ")
363+ if lipgloss.Width(gutter+ln) > width {
364+ ln = ctx.Fit(ln, width-lipgloss.Width(gutter))
366+ out = append(out, s.Tool.ContentCodeLine.Width(width).Render(gutter+ln))
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)))
374+ return strings.Join(out, "\n")
377+func getDigits(n int) int {
394+func truncateHeight(content string, maxLines int) string {
395+ lines := strings.Split(content, "\n")
396+ if len(lines) <= maxLines {
399+ return strings.Join(lines[:maxLines], "\n")
402+func prettifyToolName(name string) string {
409+ return "Job: Output"
420+ case "agentic_fetch":
421+ return "Agentic Fetch"
431+ return "Sourcegraph"
436+ case "lsp_references":
437+ return "Find References"
438+ case "lsp_diagnostics":
439+ return "Diagnostics"
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:]
447+ return strings.Join(parts, " ")
451+// Tool-specific renderers
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)
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")
465+ file := fsext.PrettyPath(params.FilePath)
466+ args := newParamBuilder().
468+ addKeyValue("limit", formatNonZero(params.Limit)).
469+ addKeyValue("offset", formatNonZero(params.Offset)).
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)
477+ return renderCodeContent(ctx, meta.FilePath, meta.Content, params.Offset)
481+func renderEdit(ctx *RenderContext) string {
483+ var params tools.EditParams
485+ if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
486+ file := fsext.PrettyPath(params.FilePath)
487+ args = newParamBuilder().addMain(file).build()
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)
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()
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
515+func renderMultiEdit(ctx *RenderContext) string {
517+ var params tools.MultiEditParams
519+ if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil {
520+ file := fsext.PrettyPath(params.FilePath)
521+ args = newParamBuilder().
523+ addKeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
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)
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()
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
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
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")
567+ file := fsext.PrettyPath(params.FilePath)
568+ args := newParamBuilder().addMain(file).build()
570+ return renderWithParams(ctx, "Write", args, func() string {
571+ return renderCodeContent(ctx, params.FilePath, params.Content, 0)
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")
581+ cmd := strings.ReplaceAll(params.Command, "\n", " ")
582+ cmd = strings.ReplaceAll(cmd, "\t", " ")
583+ args := newParamBuilder().
585+ addFlag("background", params.RunInBackground).
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()
597+ header := makeJobHeader(ctx, "Start", fmt.Sprintf("PID %s", meta.ShellID), description, width)
601+ if res, done := earlyState(ctx, header); done {
604+ content := "Command: " + params.Command + "\n" + ctx.Result.Content
605+ body := renderPlainContent(ctx, content)
606+ return joinHeaderBody(ctx, header, body)
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)
615+ if meta.Output == "" && ctx.Result.Content != tools.BashNoOutput {
616+ meta.Output = ctx.Result.Content
619+ if meta.Output == "" {
622+ return renderPlainContent(ctx, meta.Output)
626+func makeJobHeader(ctx *RenderContext, action, pid, description string, width int) string {
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)
633+ icon = s.Tool.JobIconSuccess.Render(styles.ToolSuccess)
635+ } else if ctx.Cancelled {
636+ icon = s.Muted.Render(styles.ToolPending)
639+ toolName := s.Tool.JobToolName.Render("Bash")
640+ actionPart := s.Tool.JobAction.Render(action)
641+ pidPart := s.Tool.JobPID.Render(pid)
643+ prefix := fmt.Sprintf("%s %s %s %s ", icon, toolName, actionPart, pidPart)
644+ remainingWidth := width - lipgloss.Width(prefix)
646+ descDisplay := ansi.Truncate(description, remainingWidth, "…")
647+ descDisplay = s.Tool.JobDescription.Render(descDisplay)
649+ return prefix + descDisplay
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")
658+ width := ctx.TextWidth()
663+ var meta tools.JobOutputResponseMetadata
664+ _ = unmarshalParams(ctx.Result.Metadata, &meta)
665+ description := cmp.Or(meta.Description, meta.Command)
667+ header := makeJobHeader(ctx, "Output", fmt.Sprintf("PID %s", params.ShellID), description, width)
671+ if res, done := earlyState(ctx, header); done {
674+ body := renderPlainContent(ctx, ctx.Result.Content)
675+ return joinHeaderBody(ctx, header, body)
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")
684+ width := ctx.TextWidth()
689+ var meta tools.JobKillResponseMetadata
690+ _ = unmarshalParams(ctx.Result.Metadata, &meta)
691+ description := cmp.Or(meta.Description, meta.Command)
693+ header := makeJobHeader(ctx, "Kill", fmt.Sprintf("PID %s", params.ShellID), description, width)
697+ if res, done := earlyState(ctx, header); done {
700+ body := renderPlainContent(ctx, ctx.Result.Content)
701+ return joinHeaderBody(ctx, header, body)
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")
710+ args := newParamBuilder().
711+ addMain(params.URL).
712+ addKeyValue("format", params.Format).
713+ addKeyValue("timeout", formatNonZero(params.Timeout)).
716+ return renderWithParams(ctx, "Fetch", args, func() string {
717+ path := "file." + params.Format
718+ return renderCodeContent(ctx, path, ctx.Result.Content, 0)
722+func renderAgenticFetch(ctx *RenderContext) string {
723+ // TODO: Implement nested tool call rendering with tree.
724+ return renderGeneric(ctx)
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")
733+ args := newParamBuilder().addMain(params.URL).build()
735+ return renderWithParams(ctx, "Fetching", args, func() string {
736+ return renderMarkdownContent(ctx, ctx.Result.Content)
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")
746+ args := newParamBuilder().
747+ addMain(params.URL).
748+ addKeyValue("file", fsext.PrettyPath(params.FilePath)).
749+ addKeyValue("timeout", formatNonZero(params.Timeout)).
752+ return renderWithParams(ctx, "Download", args, func() string {
753+ return renderPlainContent(ctx, ctx.Result.Content)
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")
763+ args := newParamBuilder().
764+ addMain(params.Pattern).
765+ addKeyValue("path", params.Path).
768+ return renderWithParams(ctx, "Glob", args, func() string {
769+ return renderPlainContent(ctx, ctx.Result.Content)
773+func renderGrep(ctx *RenderContext) string {
774+ var params tools.GrepParams
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).
785+ return renderWithParams(ctx, "Grep", args, func() string {
786+ return renderPlainContent(ctx, ctx.Result.Content)
790+func renderLS(ctx *RenderContext) string {
791+ var params tools.LSParams
792+ path := cmp.Or(params.Path, ".")
793+ args := newParamBuilder().addMain(path).build()
795+ if err := unmarshalParams(ctx.Call.Input, ¶ms); err == nil && params.Path != "" {
796+ args = newParamBuilder().addMain(params.Path).build()
799+ return renderWithParams(ctx, "List", args, func() string {
800+ return renderPlainContent(ctx, ctx.Result.Content)
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")
810+ args := newParamBuilder().
811+ addMain(params.Query).
812+ addKeyValue("count", formatNonZero(params.Count)).
813+ addKeyValue("context", formatNonZero(params.ContextWindow)).
816+ return renderWithParams(ctx, "Sourcegraph", args, func() string {
817+ return renderPlainContent(ctx, ctx.Result.Content)
821+func renderDiagnostics(ctx *RenderContext) string {
822+ args := newParamBuilder().addMain("project").build()
824+ return renderWithParams(ctx, "Diagnostics", args, func() string {
825+ return renderPlainContent(ctx, ctx.Result.Content)
829+func renderAgent(ctx *RenderContext) string {
831+ var params agent.AgentParams
832+ unmarshalParams(ctx.Call.Input, ¶ms)
834+ prompt := params.Prompt
835+ prompt = strings.ReplaceAll(prompt, "\n", " ")
837+ header := makeHeader(ctx, "Agent", []string{})
838+ if res, done := earlyState(ctx, header); ctx.Cancelled && done {
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(
849+ lipgloss.JoinHorizontal(
856+ childTools := tree.Root(header)
858+ // TODO: Render nested tool calls when available.
861+ childTools.Enumerator(roundedEnumeratorWithWidth(2, lipgloss.Width(taskTag)-5)).String(),
864+ if ctx.Result.ToolCallID == "" {
865+ // Pending state - would show animation in TUI.
866+ parts = append(parts, "", s.Subtle.Render("Working..."))
869+ header = lipgloss.JoinVertical(
874+ if ctx.Result.ToolCallID == "" {
878+ body := renderMarkdownContent(ctx, ctx.Result.Content)
879+ return joinHeaderBody(ctx, header, body)
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) + " "
887+ return strings.Repeat(" ", offset) + "├" + strings.Repeat("─", width-1) + " "