fix(ui): render nested tools consistently across states

Christian Rocha created

Prior to this, nested tool calls would start in one color and finish in
another.

Change summary

internal/ui/chat/agent.go       |  4 ++--
internal/ui/chat/bash.go        |  6 +++---
internal/ui/chat/diagnostics.go |  2 +-
internal/ui/chat/fetch.go       |  6 +++---
internal/ui/chat/file.go        | 10 +++++-----
internal/ui/chat/generic.go     |  2 +-
internal/ui/chat/lsp_restart.go |  2 +-
internal/ui/chat/mcp.go         |  2 +-
internal/ui/chat/references.go  |  2 +-
internal/ui/chat/search.go      |  8 ++++----
internal/ui/chat/todos.go       |  2 +-
internal/ui/chat/tools.go       |  8 ++++++--
internal/ui/styles/styles.go    | 18 +++++++++---------
13 files changed, 38 insertions(+), 34 deletions(-)

Detailed changes

internal/ui/chat/agent.go 🔗

@@ -101,7 +101,7 @@ type AgentToolRenderContext struct {
 func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
-		return pendingTool(sty, "Agent", opts.Anim)
+		return pendingTool(sty, "Agent", opts.Anim, opts.Compact)
 	}
 
 	var params agent.AgentParams
@@ -232,7 +232,7 @@ type agenticFetchParams struct {
 func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
-		return pendingTool(sty, "Agentic Fetch", opts.Anim)
+		return pendingTool(sty, "Agentic Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params agenticFetchParams

internal/ui/chat/bash.go 🔗

@@ -41,7 +41,7 @@ type BashToolRenderContext struct{}
 func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Bash", opts.Anim)
+		return pendingTool(sty, "Bash", opts.Anim, opts.Compact)
 	}
 
 	var params tools.BashParams
@@ -123,7 +123,7 @@ type JobOutputToolRenderContext struct{}
 func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Job", opts.Anim)
+		return pendingTool(sty, "Job", opts.Anim, opts.Compact)
 	}
 
 	var params tools.JobOutputParams
@@ -174,7 +174,7 @@ type JobKillToolRenderContext struct{}
 func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Job", opts.Anim)
+		return pendingTool(sty, "Job", opts.Anim, opts.Compact)
 	}
 
 	var params tools.JobKillParams

internal/ui/chat/diagnostics.go 🔗

@@ -37,7 +37,7 @@ type DiagnosticsToolRenderContext struct{}
 func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Diagnostics", opts.Anim)
+		return pendingTool(sty, "Diagnostics", opts.Anim, opts.Compact)
 	}
 
 	var params tools.DiagnosticsParams

internal/ui/chat/fetch.go 🔗

@@ -36,7 +36,7 @@ type FetchToolRenderContext struct{}
 func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Fetch", opts.Anim)
+		return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params tools.FetchParams
@@ -111,7 +111,7 @@ type WebFetchToolRenderContext struct{}
 func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Fetch", opts.Anim)
+		return pendingTool(sty, "Fetch", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WebFetchParams
@@ -165,7 +165,7 @@ type WebSearchToolRenderContext struct{}
 func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Search", opts.Anim)
+		return pendingTool(sty, "Search", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WebSearchParams

internal/ui/chat/file.go 🔗

@@ -39,7 +39,7 @@ type ViewToolRenderContext struct{}
 func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "View", opts.Anim)
+		return pendingTool(sty, "View", opts.Anim, opts.Compact)
 	}
 
 	var params tools.ViewParams
@@ -125,7 +125,7 @@ type WriteToolRenderContext struct{}
 func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Write", opts.Anim)
+		return pendingTool(sty, "Write", opts.Anim, opts.Compact)
 	}
 
 	var params tools.WriteParams
@@ -180,7 +180,7 @@ type EditToolRenderContext struct{}
 func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	// Edit tool uses full width for diffs.
 	if opts.IsPending() {
-		return pendingTool(sty, "Edit", opts.Anim)
+		return pendingTool(sty, "Edit", opts.Anim, opts.Compact)
 	}
 
 	var params tools.EditParams
@@ -243,7 +243,7 @@ type MultiEditToolRenderContext struct{}
 func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	// MultiEdit tool uses full width for diffs.
 	if opts.IsPending() {
-		return pendingTool(sty, "Multi-Edit", opts.Anim)
+		return pendingTool(sty, "Multi-Edit", opts.Anim, opts.Compact)
 	}
 
 	var params tools.MultiEditParams
@@ -311,7 +311,7 @@ type DownloadToolRenderContext struct{}
 func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Download", opts.Anim)
+		return pendingTool(sty, "Download", opts.Anim, opts.Compact)
 	}
 
 	var params tools.DownloadParams

internal/ui/chat/generic.go 🔗

@@ -35,7 +35,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt
 	name := genericPrettyName(opts.ToolCall.Name)
 
 	if opts.IsPending() {
-		return pendingTool(sty, name, opts.Anim)
+		return pendingTool(sty, name, opts.Anim, opts.Compact)
 	}
 
 	var params map[string]any

internal/ui/chat/lsp_restart.go 🔗

@@ -32,7 +32,7 @@ type LSPRestartToolRenderContext struct{}
 func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Restart LSP", opts.Anim)
+		return pendingTool(sty, "Restart LSP", opts.Anim, opts.Compact)
 	}
 
 	var params tools.LSPRestartParams

internal/ui/chat/mcp.go 🔗

@@ -46,7 +46,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T
 	name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
 
 	if opts.IsPending() {
-		return pendingTool(sty, name, opts.Anim)
+		return pendingTool(sty, name, opts.Anim, opts.Compact)
 	}
 
 	var params map[string]any

internal/ui/chat/references.go 🔗

@@ -33,7 +33,7 @@ type ReferencesToolRenderContext struct{}
 func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Find References", opts.Anim)
+		return pendingTool(sty, "Find References", opts.Anim, opts.Compact)
 	}
 
 	var params tools.ReferencesParams

internal/ui/chat/search.go 🔗

@@ -37,7 +37,7 @@ type GlobToolRenderContext struct{}
 func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Glob", opts.Anim)
+		return pendingTool(sty, "Glob", opts.Anim, opts.Compact)
 	}
 
 	var params tools.GlobParams
@@ -96,7 +96,7 @@ type GrepToolRenderContext struct{}
 func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Grep", opts.Anim)
+		return pendingTool(sty, "Grep", opts.Anim, opts.Compact)
 	}
 
 	var params tools.GrepParams
@@ -161,7 +161,7 @@ type LSToolRenderContext struct{}
 func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "List", opts.Anim)
+		return pendingTool(sty, "List", opts.Anim, opts.Compact)
 	}
 
 	var params tools.LSParams
@@ -221,7 +221,7 @@ type SourcegraphToolRenderContext struct{}
 func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "Sourcegraph", opts.Anim)
+		return pendingTool(sty, "Sourcegraph", opts.Anim, opts.Compact)
 	}
 
 	var params tools.SourcegraphParams

internal/ui/chat/todos.go 🔗

@@ -41,7 +41,7 @@ type TodosToolRenderContext struct{}
 func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 	cappedWidth := cappedMessageWidth(width)
 	if opts.IsPending() {
-		return pendingTool(sty, "To-Do", opts.Anim)
+		return pendingTool(sty, "To-Do", opts.Anim, opts.Compact)
 	}
 
 	var params tools.TodosParams

internal/ui/chat/tools.go 🔗

@@ -424,9 +424,13 @@ func (t *baseToolMessageItem) HandleKeyEvent(key tea.KeyMsg) (bool, tea.Cmd) {
 }
 
 // pendingTool renders a tool that is still in progress with an animation.
-func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
+func pendingTool(sty *styles.Styles, name string, anim *anim.Anim, nested bool) string {
 	icon := sty.Tool.IconPending.Render()
-	toolName := sty.Tool.NameNormal.Render(name)
+	nameStyle := sty.Tool.NameNormal
+	if nested {
+		nameStyle = sty.Tool.NameNested
+	}
+	toolName := nameStyle.Render(name)
 
 	var animView string
 	if anim != nil {

internal/ui/styles/styles.go 🔗

@@ -254,18 +254,18 @@ type Styles struct {
 	// Tool - styles for tool call rendering
 	Tool struct {
 		// Icon styles with tool status
-		IconPending   lipgloss.Style // Pending operation icon
-		IconSuccess   lipgloss.Style // Successful operation icon
-		IconError     lipgloss.Style // Error operation icon
-		IconCancelled lipgloss.Style // Cancelled operation icon
+		IconPending   lipgloss.Style
+		IconSuccess   lipgloss.Style
+		IconError     lipgloss.Style
+		IconCancelled lipgloss.Style
 
 		// Tool name styles
-		NameNormal lipgloss.Style // Normal tool name
-		NameNested lipgloss.Style // Nested tool name
+		NameNormal lipgloss.Style // Top-level tool name
+		NameNested lipgloss.Style // Nested child tool name (inside Agent/Agentic Fetch)
 
 		// Parameter list styles
-		ParamMain lipgloss.Style // Main parameter
-		ParamKey  lipgloss.Style // Parameter keys
+		ParamMain lipgloss.Style
+		ParamKey  lipgloss.Style
 
 		// Content rendering styles
 		ContentLine           lipgloss.Style // Individual content line with background and width
@@ -1119,7 +1119,7 @@ func DefaultStyles() Styles {
 	s.Tool.IconCancelled = s.Muted.SetString(ToolPending)
 
 	s.Tool.NameNormal = base.Foreground(blue)
-	s.Tool.NameNested = base.Foreground(fgHalfMuted)
+	s.Tool.NameNested = base.Foreground(blue)
 
 	s.Tool.ParamMain = s.Subtle
 	s.Tool.ParamKey = s.Subtle