on main help sign in
feat(ui): collapse nested tool calls by default with expansion hint
7ea54ebiko
internal/ui/chat/agent.go+51−20
@@ -2,6 +2,7 @@ package chat
2
2
3import (
3import (
4 "encoding/json"
4 "encoding/json"
5+ "fmt"
5 "strings"
6 "strings"
6
7
7 tea "charm.land/bubbletea/v2"
8 tea "charm.land/bubbletea/v2"
@@ -24,6 +25,18 @@ type NestedToolContainer interface {
24 AddNestedTool(tool ToolMessageItem)
25 AddNestedTool(tool ToolMessageItem)
25}
26}
26
27
28+// collapsedNestedHint is the affordance shown under a collapsed sub-agent
29+// card, indicating its nested tool calls can be revealed with space.
30+const collapsedNestedHint = " ▸ space to expand"
31+
32+// toolCountLabel renders a "N tools" summary for a sub-agent header line.
33+func toolCountLabel(n int) string {
34+ if n == 1 {
35+ return "1 tool"
36+ }
37+ return fmt.Sprintf("%d tools", n)
38+}
39+
27// AgentToolMessageItem is a message item that represents an agent tool call.
40// AgentToolMessageItem is a message item that represents an agent tool call.
28type AgentToolMessageItem struct {
41type AgentToolMessageItem struct {
29 *baseToolMessageItem
42 *baseToolMessageItem
@@ -139,6 +152,10 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
139 }
152 }
140
153
141 header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts)
154 header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts)
155+ nestedCount := len(r.agent.nestedTools)
156+ if nestedCount > 0 {
157+ header += " " + sty.Tool.ParamMain.Render(toolCountLabel(nestedCount))
158+ }
142 if opts.Compact {
159 if opts.Compact {
143 return header
160 return header
144 }
161 }
@@ -164,17 +181,22 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts
164 ),
181 ),
165 )
182 )
166
183
167− // Build tree with nested tool calls.
184+ // Nested tool calls are collapsed by default to keep the conversation
168− childTools := tree.Root(header)
185+ // readable; press space to expand the full call tree.
169−
170− for _, nestedTool := range r.agent.nestedTools {
171− childView := nestedTool.Render(remainingWidth)
172− childTools.Child(childView)
173− }
174−
175− // Build parts.
176 var parts []string
186 var parts []string
187+ if opts.ExpandedContent {
188+ childTools := tree.Root(header)
189+ for _, nestedTool := range r.agent.nestedTools {
190+ childView := nestedTool.Render(remainingWidth)
191+ childTools.Child(childView)
192+ }
177− parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
193+ parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
194+ } else {
195+ parts = append(parts, header)
196+ if nestedCount > 0 {
197+ parts = append(parts, sty.Tool.AgentPrompt.Render(collapsedNestedHint))
198+ }
199+ }
178
200
179 // Show animation if still running.
201 // Show animation if still running.
180 if !opts.HasResult() && !opts.IsCanceled() {
202 if !opts.HasResult() && !opts.IsCanceled() {
@@ -308,6 +330,10 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int
308 }
330 }
309
331
310 header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts, toolParams...)
332 header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts, toolParams...)
333+ nestedCount := len(r.fetch.nestedTools)
334+ if nestedCount > 0 {
335+ header += " " + sty.Tool.ParamMain.Render(toolCountLabel(nestedCount))
336+ }
311 if opts.Compact {
337 if opts.Compact {
312 return header
338 return header
313 }
339 }
@@ -333,17 +359,22 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int
333 ),
359 ),
334 )
360 )
335
361
336− // Build tree with nested tool calls.
362+ // Nested tool calls are collapsed by default to keep the conversation
337− childTools := tree.Root(header)
363+ // readable; press space to expand the full call tree.
338−
339− for _, nestedTool := range r.fetch.nestedTools {
340− childView := nestedTool.Render(remainingWidth)
341− childTools.Child(childView)
342− }
343−
344− // Build parts.
345 var parts []string
364 var parts []string
365+ if opts.ExpandedContent {
366+ childTools := tree.Root(header)
367+ for _, nestedTool := range r.fetch.nestedTools {
368+ childView := nestedTool.Render(remainingWidth)
369+ childTools.Child(childView)
370+ }
346− parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
371+ parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
372+ } else {
373+ parts = append(parts, header)
374+ if nestedCount > 0 {
375+ parts = append(parts, sty.Tool.AgentPrompt.Render(collapsedNestedHint))
376+ }
377+ }
347
378
348 // Show animation if still running.
379 // Show animation if still running.
349 if !opts.HasResult() && !opts.IsCanceled() {
380 if !opts.HasResult() && !opts.IsCanceled() {