@@ -1,889 +0,0 @@
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) + " "