@@ -111,8 +111,18 @@ func (br baseRenderer) unmarshalParams(input string, target any) error {
111 return json.Unmarshal([]byte(input), target)
111 return json.Unmarshal([]byte(input), target)
114+// makeHeader builds the tool call header with status icon and parameters for a nested tool call.
115+func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string {
116+ t := styles.CurrentTheme()
117+ tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " "
118+ return tool + renderParamList(true, width-lipgloss.Width(tool), params...)
114// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
121// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
115func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
122func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string {
124+ return br.makeNestedHeader(v, tool, width, params...)
116 t := styles.CurrentTheme()
126 t := styles.CurrentTheme()
117 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
127 icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending)
118 if v.result.ToolCallID != "" {
128 if v.result.ToolCallID != "" {
@@ -125,16 +135,17 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params
125 icon = t.S().Muted.Render(styles.ToolPending)
135 icon = t.S().Muted.Render(styles.ToolPending)
127 tool = t.S().Base.Foreground(t.Blue).Render(tool)
137 tool = t.S().Base.Foreground(t.Blue).Render(tool)
128− prefix := fmt.Sprintf("%s %s: ", icon, tool)
138+ prefix := fmt.Sprintf("%s %s ", icon, tool)
129− return prefix + renderParamList(width-lipgloss.Width(prefix), params...)
139+ return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...)
132// renderError provides consistent error rendering
142// renderError provides consistent error rendering
133func (br baseRenderer) renderError(v *toolCallCmp, message string) string {
143func (br baseRenderer) renderError(v *toolCallCmp, message string) string {
134 t := styles.CurrentTheme()
144 t := styles.CurrentTheme()
135 header := br.makeHeader(v, prettifyToolName(v.call.Name), v.textWidth(), "")
145 header := br.makeHeader(v, prettifyToolName(v.call.Name), v.textWidth(), "")
146+ errorTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
136− message = t.S().Error.Render(v.fit(message, v.textWidth()-2)) // -2 for padding
147+ message = t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(message, v.textWidth()-3-lipgloss.Width(errorTag))) // -2 for padding and space
137− return joinHeaderBody(header, message)
148+ return joinHeaderBody(header, errorTag+" "+message)
140// Register tool renderers
151// Register tool renderers
@@ -188,6 +199,9 @@ func (br bashRenderer) Render(v *toolCallCmp) string {
188 args := newParamBuilder().addMain(cmd).build()
199 args := newParamBuilder().addMain(cmd).build()
190 return br.renderWithParams(v, "Bash", args, func() string {
201 return br.renderWithParams(v, "Bash", args, func() string {
202+ if v.result.Content == tools.BashNoOutput {
191 return renderPlainContent(v, v.result.Content)
205 return renderPlainContent(v, v.result.Content)
@@ -244,13 +258,12 @@ type editRenderer struct {
244// Render displays the edited file with a formatted diff of changes
258// Render displays the edited file with a formatted diff of changes
245func (er editRenderer) Render(v *toolCallCmp) string {
259func (er editRenderer) Render(v *toolCallCmp) string {
246 var params tools.EditParams
260 var params tools.EditParams
247− if err := er.unmarshalParams(v.call.Input, ¶ms); err != nil {
262+ if err := er.unmarshalParams(v.call.Input, ¶ms); err == nil {
248− return er.renderError(v, "Invalid edit parameters")
263+ file := fsext.PrettyPath(params.FilePath)
264+ args = newParamBuilder().addMain(file).build()
251− file := fsext.PrettyPath(params.FilePath)
252− args := newParamBuilder().addMain(file).build()
254 return er.renderWithParams(v, "Edit", args, func() string {
267 return er.renderWithParams(v, "Edit", args, func() string {
255 var meta tools.EditResponseMetadata
268 var meta tools.EditResponseMetadata
256 if err := er.unmarshalParams(v.result.Metadata, &meta); err != nil {
269 if err := er.unmarshalParams(v.result.Metadata, &meta); err != nil {
@@ -260,8 +273,10 @@ func (er editRenderer) Render(v *toolCallCmp) string {
260 formatter := core.DiffFormatter().
273 formatter := core.DiffFormatter().
261 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
274 Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
262 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
275 After(fsext.PrettyPath(params.FilePath), meta.NewContent).
264 Width(v.textWidth() - 2) // -2 for padding
276 Width(v.textWidth() - 2) // -2 for padding
277+ if v.textWidth() > 120 {
278+ formatter = formatter.Split()
265 return formatter.String()
280 return formatter.String()
@@ -278,13 +293,13 @@ type writeRenderer struct {
278// Render displays the file being written with syntax highlighting
293// Render displays the file being written with syntax highlighting
279func (wr writeRenderer) Render(v *toolCallCmp) string {
294func (wr writeRenderer) Render(v *toolCallCmp) string {
280 var params tools.WriteParams
295 var params tools.WriteParams
281− if err := wr.unmarshalParams(v.call.Input, ¶ms); err != nil {
298+ if err := wr.unmarshalParams(v.call.Input, ¶ms); err == nil {
282− return wr.renderError(v, "Invalid write parameters")
299+ file = fsext.PrettyPath(params.FilePath)
300+ args = newParamBuilder().addMain(file).build()
285− file := fsext.PrettyPath(params.FilePath)
286− args := newParamBuilder().addMain(file).build()
288 return wr.renderWithParams(v, "Write", args, func() string {
303 return wr.renderWithParams(v, "Write", args, func() string {
289 return renderCodeContent(v, file, params.Content, 0)
304 return renderCodeContent(v, file, params.Content, 0)
@@ -302,16 +317,15 @@ type fetchRenderer struct {
302// Render displays the fetched URL with format and timeout parameters
317// Render displays the fetched URL with format and timeout parameters
303func (fr fetchRenderer) Render(v *toolCallCmp) string {
318func (fr fetchRenderer) Render(v *toolCallCmp) string {
304 var params tools.FetchParams
319 var params tools.FetchParams
305− if err := fr.unmarshalParams(v.call.Input, ¶ms); err != nil {
321+ if err := fr.unmarshalParams(v.call.Input, ¶ms); err == nil {
306− return fr.renderError(v, "Invalid fetch parameters")
322+ args = newParamBuilder().
323+ addMain(params.URL).
324+ addKeyValue("format", params.Format).
325+ addKeyValue("timeout", formatTimeout(params.Timeout)).
309− args := newParamBuilder().
310− addMain(params.URL).
311− addKeyValue("format", params.Format).
312− addKeyValue("timeout", formatTimeout(params.Timeout)).
315 return fr.renderWithParams(v, "Fetch", args, func() string {
329 return fr.renderWithParams(v, "Fetch", args, func() string {
316 file := fr.getFileExtension(params.Format)
330 file := fr.getFileExtension(params.Format)
317 return renderCodeContent(v, file, v.result.Content, 0)
331 return renderCodeContent(v, file, v.result.Content, 0)
@@ -350,15 +364,14 @@ type globRenderer struct {
350// Render displays the glob pattern with optional path parameter
364// Render displays the glob pattern with optional path parameter
351func (gr globRenderer) Render(v *toolCallCmp) string {
365func (gr globRenderer) Render(v *toolCallCmp) string {
352 var params tools.GlobParams
366 var params tools.GlobParams
353− if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil {
368+ if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil {
354− return gr.renderError(v, "Invalid glob parameters")
369+ args = newParamBuilder().
370+ addMain(params.Pattern).
371+ addKeyValue("path", params.Path).
357− args := newParamBuilder().
358− addMain(params.Pattern).
359− addKeyValue("path", params.Path).
362 return gr.renderWithParams(v, "Glob", args, func() string {
375 return gr.renderWithParams(v, "Glob", args, func() string {
363 return renderPlainContent(v, v.result.Content)
376 return renderPlainContent(v, v.result.Content)
@@ -376,17 +389,16 @@ type grepRenderer struct {
376// Render displays the search pattern with path, include, and literal text options
389// Render displays the search pattern with path, include, and literal text options
377func (gr grepRenderer) Render(v *toolCallCmp) string {
390func (gr grepRenderer) Render(v *toolCallCmp) string {
378 var params tools.GrepParams
391 var params tools.GrepParams
379− if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil {
393+ if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil {
380− return gr.renderError(v, "Invalid grep parameters")
394+ args = newParamBuilder().
395+ addMain(params.Pattern).
396+ addKeyValue("path", params.Path).
397+ addKeyValue("include", params.Include).
398+ addFlag("literal", params.LiteralText).
383− args := newParamBuilder().
384− addMain(params.Pattern).
385− addKeyValue("path", params.Path).
386− addKeyValue("include", params.Include).
387− addFlag("literal", params.LiteralText).
390 return gr.renderWithParams(v, "Grep", args, func() string {
402 return gr.renderWithParams(v, "Grep", args, func() string {
391 return renderPlainContent(v, v.result.Content)
403 return renderPlainContent(v, v.result.Content)
@@ -404,17 +416,16 @@ type lsRenderer struct {
404// Render displays the directory path, defaulting to current directory
416// Render displays the directory path, defaulting to current directory
405func (lr lsRenderer) Render(v *toolCallCmp) string {
417func (lr lsRenderer) Render(v *toolCallCmp) string {
406 var params tools.LSParams
418 var params tools.LSParams
407− if err := lr.unmarshalParams(v.call.Input, ¶ms); err != nil {
420+ if err := lr.unmarshalParams(v.call.Input, ¶ms); err == nil {
408− return lr.renderError(v, "Invalid ls parameters")
421+ path := params.Path
425+ path = fsext.PrettyPath(path)
411− path := params.Path
427+ args = newParamBuilder().addMain(path).build()
415− path = fsext.PrettyPath(path)
417− args := newParamBuilder().addMain(path).build()
419 return lr.renderWithParams(v, "List", args, func() string {
430 return lr.renderWithParams(v, "List", args, func() string {
420 return renderPlainContent(v, v.result.Content)
431 return renderPlainContent(v, v.result.Content)
@@ -433,16 +444,15 @@ type sourcegraphRenderer struct {
433// Render displays the search query with optional count and context window parameters
444// Render displays the search query with optional count and context window parameters
434func (sr sourcegraphRenderer) Render(v *toolCallCmp) string {
445func (sr sourcegraphRenderer) Render(v *toolCallCmp) string {
435 var params tools.SourcegraphParams
446 var params tools.SourcegraphParams
436− if err := sr.unmarshalParams(v.call.Input, ¶ms); err != nil {
448+ if err := sr.unmarshalParams(v.call.Input, ¶ms); err == nil {
437− return sr.renderError(v, "Invalid sourcegraph parameters")
449+ args = newParamBuilder().
450+ addMain(params.Query).
451+ addKeyValue("count", formatNonZero(params.Count)).
452+ addKeyValue("context", formatNonZero(params.ContextWindow)).
440− args := newParamBuilder().
441− addMain(params.Query).
442− addKeyValue("count", formatNonZero(params.Count)).
443− addKeyValue("context", formatNonZero(params.ContextWindow)).
446 return sr.renderWithParams(v, "Sourcegraph", args, func() string {
456 return sr.renderWithParams(v, "Sourcegraph", args, func() string {
447 return renderPlainContent(v, v.result.Content)
457 return renderPlainContent(v, v.result.Content)
@@ -475,25 +485,47 @@ type agentRenderer struct {
475 baseRenderer
485 baseRenderer
488+func RoundedEnumerator(children tree.Children, index int) string {
489+ if children.Length()-1 == index {
478// Render displays agent task parameters and result content
495// Render displays agent task parameters and result content
479func (tr agentRenderer) Render(v *toolCallCmp) string {
496func (tr agentRenderer) Render(v *toolCallCmp) string {
497+ t := styles.CurrentTheme()
480 var params agent.AgentParams
498 var params agent.AgentParams
481− if err := tr.unmarshalParams(v.call.Input, ¶ms); err != nil {
499+ tr.unmarshalParams(v.call.Input, ¶ms)
482− return tr.renderError(v, "Invalid task parameters")
500+
484 prompt := params.Prompt
501 prompt := params.Prompt
485 prompt = strings.ReplaceAll(prompt, "\n", " ")
502 prompt = strings.ReplaceAll(prompt, "\n", " ")
486− args := newParamBuilder().addMain(prompt).build()
488− header := tr.makeHeader(v, "Task", v.textWidth(), args...)
504+ header := tr.makeHeader(v, "Agent", v.textWidth())
505+ if res, done := earlyState(header, v); done {
508+ taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task")
509+ remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding
510+ prompt = t.S().Muted.Width(remainingWidth).Render(prompt)
511+ header = lipgloss.JoinVertical(
515+ lipgloss.JoinHorizontal(
489− t := tree.Root(header)
522+ childTools := tree.Root(header)
491 for _, call := range v.nestedToolCalls {
524 for _, call := range v.nestedToolCalls {
492− t.Child(call.View())
525+ childTools.Child(call.View())
495 parts := []string{
527 parts := []string{
496− t.Enumerator(tree.RoundedEnumerator).String(),
528+ childTools.Enumerator(RoundedEnumerator).String(),
498 if v.result.ToolCallID == "" {
530 if v.result.ToolCallID == "" {
499 v.spinning = true
531 v.spinning = true
@@ -516,7 +548,8 @@ func (tr agentRenderer) Render(v *toolCallCmp) string {
518// renderParamList renders params, params[0] (params[1]=params[2] ....)
550// renderParamList renders params, params[0] (params[1]=params[2] ....)
519−func renderParamList(paramsWidth int, params ...string) string {
551+func renderParamList(nested bool, paramsWidth int, params ...string) string {
552+ t := styles.CurrentTheme()
520 if len(params) == 0 {
553 if len(params) == 0 {
521 return ""
554 return ""
@@ -526,7 +559,10 @@ func renderParamList(paramsWidth int, params ...string) string {
528 if len(params) == 1 {
561 if len(params) == 1 {
529− return mainParam
563+ return t.S().Muted.Render(mainParam)
565+ return t.S().Subtle.Render(mainParam)
531 otherParams := params[1:]
567 otherParams := params[1:]
532 // create pairs of key/value
568 // create pairs of key/value
@@ -547,15 +583,21 @@ func renderParamList(paramsWidth int, params ...string) string {
547 partsRendered := strings.Join(parts, ", ")
583 partsRendered := strings.Join(parts, ", ")
548 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
584 remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
549 if remainingWidth < 30 {
585 if remainingWidth < 30 {
587+ return t.S().Muted.Render(mainParam)
550 // No space for the params, just show the main
589 // No space for the params, just show the main
551− return mainParam
590+ return t.S().Subtle.Render(mainParam)
554 if len(parts) > 0 {
593 if len(parts) > 0 {
555 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
594 mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
598+ return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
558− return ansi.Truncate(mainParam, paramsWidth, "...")
600+ return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "..."))
561// earlyState returns immediately‑rendered error/cancelled/ongoing states.
603// earlyState returns immediately‑rendered error/cancelled/ongoing states.
@@ -566,21 +608,21 @@ func earlyState(header string, v *toolCallCmp) (string, bool) {
566 case v.result.IsError:
608 case v.result.IsError:
567 message = v.renderToolError()
609 message = v.renderToolError()
568 case v.cancelled:
610 case v.cancelled:
569− message = "Cancelled"
611+ message = t.S().Base.Padding(0, 1).Background(t.Border).Render("Cancelled")
570 case v.result.ToolCallID == "":
612 case v.result.ToolCallID == "":
571− message = "Waiting for tool to start..."
613+ message = t.S().Base.Padding(0, 1).Background(t.Accent).Foreground(t.FgSubtle).Render("Waiting for tool to start...")
573 return "", false
615 return "", false
576 message = t.S().Base.PaddingLeft(2).Render(message)
618 message = t.S().Base.PaddingLeft(2).Render(message)
577− return lipgloss.JoinVertical(lipgloss.Left, header, message), true
619+ return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true
580func joinHeaderBody(header, body string) string {
622func joinHeaderBody(header, body string) string {
581 t := styles.CurrentTheme()
623 t := styles.CurrentTheme()
582 body = t.S().Base.PaddingLeft(2).Render(body)
624 body = t.S().Base.PaddingLeft(2).Render(body)
583− return lipgloss.JoinVertical(lipgloss.Left, header, body, "")
625+ return lipgloss.JoinVertical(lipgloss.Left, header, "", body, "")
586func renderPlainContent(v *toolCallCmp, content string) string {
628func renderPlainContent(v *toolCallCmp, content string) string {
@@ -600,46 +642,56 @@ func renderPlainContent(v *toolCallCmp, content string) string {
601 out = append(out, t.S().Muted.
643 out = append(out, t.S().Muted.
602 Width(width).
644 Width(width).
603− Background(t.BgSubtle).
645+ Background(t.BgBaseLighter).
604 Render(ln))
646 Render(ln))
607 if len(lines) > responseContextHeight {
649 if len(lines) > responseContextHeight {
608 out = append(out, t.S().Muted.
650 out = append(out, t.S().Muted.
609− Background(t.BgSubtle).
651+ Background(t.BgBaseLighter).
610 Width(width).
652 Width(width).
611 Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
653 Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
613 return strings.Join(out, "\n")
655 return strings.Join(out, "\n")
658+func pad(v any, width int) string {
659+ s := fmt.Sprintf("%v", v)
660+ w := ansi.StringWidth(s)
664+ return strings.Repeat(" ", width-w) + s
616func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
667func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
617 t := styles.CurrentTheme()
668 t := styles.CurrentTheme()
618 truncated := truncateHeight(content, responseContextHeight)
669 truncated := truncateHeight(content, responseContextHeight)
620− highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle)
671+ highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgBase)
621 lines := strings.Split(highlighted, "\n")
672 lines := strings.Split(highlighted, "\n")
623 if len(strings.Split(content, "\n")) > responseContextHeight {
674 if len(strings.Split(content, "\n")) > responseContextHeight {
624 lines = append(lines, t.S().Muted.
675 lines = append(lines, t.S().Muted.
625− Background(t.BgSubtle).
676+ Background(t.BgBase).
626− Width(v.textWidth()-2).
627− Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
677+ Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
680+ maxLineNumber := len(lines) + offset
681+ padding := lipgloss.Width(fmt.Sprintf("%d", maxLineNumber))
630 for i, ln := range lines {
682 for i, ln := range lines {
631− num := t.S().Muted.
683+ num := t.S().Base.
684+ Foreground(t.FgMuted).
632− Background(t.BgSubtle).
685+ Background(t.BgBase).
634− PaddingRight(2).
686+ PaddingRight(1).
635− Render(fmt.Sprintf("%d", i+1+offset))
688+ Render(pad(i+1+offset, padding))
636− w := v.textWidth() - 2 - lipgloss.Width(num) // -2 for left padding
689+ w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding
637 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
690 lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
639 t.S().Base.
692 t.S().Base.
640− Width(w).
693+ PaddingLeft(1).
641− Background(t.BgSubtle).
642− Render(v.fit(ln, w)))
694+ Render(v.fit(ln, w-1)))
644 return lipgloss.JoinVertical(lipgloss.Left, lines...)
696 return lipgloss.JoinVertical(lipgloss.Left, lines...)
@@ -647,8 +699,9 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string
647func (v *toolCallCmp) renderToolError() string {
699func (v *toolCallCmp) renderToolError() string {
648 t := styles.CurrentTheme()
700 t := styles.CurrentTheme()
649 err := strings.ReplaceAll(v.result.Content, "\n", " ")
701 err := strings.ReplaceAll(v.result.Content, "\n", " ")
650− err = fmt.Sprintf("Error: %s", err)
702+ errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR")
651− return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()))
703+ err = fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(err, v.textWidth()-2-lipgloss.Width(errTag))))
654func truncateHeight(s string, h int) string {
707func truncateHeight(s string, h int) string {
@@ -662,7 +715,7 @@ func truncateHeight(s string, h int) string {
662func prettifyToolName(name string) string {
715func prettifyToolName(name string) string {
663 switch name {
716 switch name {
664 case agent.AgentToolName:
717 case agent.AgentToolName:
665− return "Task"
718+ return "Agent"
666 case tools.BashToolName:
719 case tools.BashToolName:
667 return "Bash"
720 return "Bash"
668 case tools.EditToolName:
721 case tools.EditToolName: