Add setting for collapsed tool call rendering
- commit
- 67b6b2e730e04f30ad1599108a9a6785e8bf92d8
- author
- ggbalaazs · · 2026-09-05 12:48 UTC
- committer
- ggbalaazs · · 2026-09-05 15:26 UTC
- parent
- e18c6ae
- signature
- unsigned
6 files · +168 -0
M packages/agent/cli.go +1 -0
| @@ -1287,6 +1287,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||
| 1287 | 1287 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | |
| 1288 | 1288 | RespectGitignore: initialCfg.RespectGitignore, | |
| 1289 | 1289 | CompactMode: initialCfg.CompactMode, | |
| 1290 | + | CollapseToolCall: initialCfg.CollapseToolCall, | |
| 1290 | 1291 | TUIInputStyle: initialCfg.TUIInputStyle, | |
| 1291 | 1292 | TUIStatusPosition: initialCfg.TUIStatusPosition, | |
| 1292 | 1293 | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | |
| @@ -1287,6 +1287,7 @@ func runInteractive(ctx context.Context, args Args, version string) error { | |||||
| 1287 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | 1287 | RecursiveFileSuggest: initialCfg.RecursiveFileSuggest, | ||
| 1288 | RespectGitignore: initialCfg.RespectGitignore, | 1288 | RespectGitignore: initialCfg.RespectGitignore, | ||
| 1289 | CompactMode: initialCfg.CompactMode, | 1289 | CompactMode: initialCfg.CompactMode, | ||
| 1290 | + | CollapseToolCall: initialCfg.CollapseToolCall, | |||
| 1290 | TUIInputStyle: initialCfg.TUIInputStyle, | 1291 | TUIInputStyle: initialCfg.TUIInputStyle, | ||
| 1291 | TUIStatusPosition: initialCfg.TUIStatusPosition, | 1292 | TUIStatusPosition: initialCfg.TUIStatusPosition, | ||
| 1292 | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | 1293 | TUIWorkingPosition: initialCfg.TUIWorkingPosition, | ||
M packages/agent/config.go +5 -0
| @@ -95,6 +95,11 @@ type Config struct { | |||
| 95 | 95 | // default; nil/missing means disabled. Toggle from /settings. | |
| 96 | 96 | CompactMode *bool `json:"compact_mode,omitempty"` | |
| 97 | 97 | ||
| 98 | + | // CollapseToolCall shows only the tool-call header, hiding | |
| 99 | + | // call and result content. Off by default; nil/missing means disabled. | |
| 100 | + | // Toggle from /settings. | |
| 101 | + | CollapseToolCall *bool `json:"collapse_tool_call,omitempty"` | |
| 102 | + | ||
| 98 | 103 | // ShowInstructionsAtStartup lists loaded context files, extensions, | |
| 99 | 104 | // and user-installed skills above the transcript. Off by default; | |
| 100 | 105 | // nil/missing means disabled. Toggle from /settings. | |
| @@ -95,6 +95,11 @@ type Config struct { | |||||
| 95 | // default; nil/missing means disabled. Toggle from /settings. | 95 | // default; nil/missing means disabled. Toggle from /settings. | ||
| 96 | CompactMode *bool `json:"compact_mode,omitempty"` | 96 | CompactMode *bool `json:"compact_mode,omitempty"` | ||
| 97 | 97 | ||||
| 98 | + | // CollapseToolCall shows only the tool-call header, hiding | |||
| 99 | + | // call and result content. Off by default; nil/missing means disabled. | |||
| 100 | + | // Toggle from /settings. | |||
| 101 | + | CollapseToolCall *bool `json:"collapse_tool_call,omitempty"` | |||
| 102 | + | ||||
| 98 | // ShowInstructionsAtStartup lists loaded context files, extensions, | 103 | // ShowInstructionsAtStartup lists loaded context files, extensions, | ||
| 99 | // and user-installed skills above the transcript. Off by default; | 104 | // and user-installed skills above the transcript. Off by default; | ||
| 100 | // nil/missing means disabled. Toggle from /settings. | 105 | // nil/missing means disabled. Toggle from /settings. | ||
M packages/agent/modes/interactive.go +30 -0
| @@ -111,6 +111,10 @@ type InteractiveConfig struct { | |||
| 111 | 111 | // When true, it enables both flat tool calls and compact user turns. | |
| 112 | 112 | CompactMode *bool | |
| 113 | 113 | ||
| 114 | + | // CollapseToolCall hides call and result content while | |
| 115 | + | // retaining each tool-call header. | |
| 116 | + | CollapseToolCall *bool | |
| 117 | + | ||
| 114 | 118 | // TUIInputStyle controls the main input rendering: plain or lines. | |
| 115 | 119 | TUIInputStyle string | |
| 116 | 120 | ||
| @@ -350,6 +354,7 @@ type SettingsStore interface { | |||
| 350 | 354 | SetRecursiveFileSuggest(enabled bool) error | |
| 351 | 355 | SetRespectGitignore(enabled bool) error | |
| 352 | 356 | SetCompactMode(enabled bool) error | |
| 357 | + | SetCollapseToolCall(enabled bool) error | |
| 353 | 358 | SetTUIInputStyle(style string) error | |
| 354 | 359 | SetTUIStatusPosition(position string) error | |
| 355 | 360 | SetTUIWorkingPosition(position string) error | |
| @@ -626,6 +631,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||
| 626 | 631 | FlatTools: cfg.FlatTools, | |
| 627 | 632 | CompactUser: cfg.CompactUser, | |
| 628 | 633 | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | |
| 634 | + | CollapseToolCall: cfg.CollapseToolCall != nil && *cfg.CollapseToolCall, | |
| 629 | 635 | StartupAgentName: startupAgentName, | |
| 630 | 636 | StartupContextPaths: startupContextPaths, | |
| 631 | 637 | StartupExtensionNames: startupExtensionNames, | |
| @@ -3416,6 +3422,7 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3416 | 3422 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | |
| 3417 | 3423 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | |
| 3418 | 3424 | compactMode := i.compactModeEnabled() | |
| 3425 | + | collapseToolCall := i.cfg.CollapseToolCall != nil && *i.cfg.CollapseToolCall | |
| 3419 | 3426 | showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup | |
| 3420 | 3427 | inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) | |
| 3421 | 3428 | statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition) | |
| @@ -3557,6 +3564,12 @@ func (i *Interactive) openSettingsDialog() { | |||
| 3557 | 3564 | desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles", | |
| 3558 | 3565 | value: compactMode, | |
| 3559 | 3566 | }, | |
| 3567 | + | { | |
| 3568 | + | key: "collapse_tool_call", | |
| 3569 | + | label: "collapse tool call rendering", | |
| 3570 | + | desc: "show only the tool-call header, hiding call arguments and result content", | |
| 3571 | + | value: collapseToolCall, | |
| 3572 | + | }, | |
| 3560 | 3573 | { | |
| 3561 | 3574 | key: "show_instructions_at_startup", | |
| 3562 | 3575 | label: "show loaded resources at startup", | |
| @@ -3966,6 +3979,23 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||
| 3966 | 3979 | i.statusOK = "compact transcript rendering " + onOff(value) | |
| 3967 | 3980 | i.statusErr = "" | |
| 3968 | 3981 | i.mu.Unlock() | |
| 3982 | + | case "collapse_tool_call": | |
| 3983 | + | val := value | |
| 3984 | + | i.cfg.CollapseToolCall = &val | |
| 3985 | + | if i.cfg.SettingsStore != nil { | |
| 3986 | + | if err := i.cfg.SettingsStore.SetCollapseToolCall(value); err != nil { | |
| 3987 | + | i.mu.Lock() | |
| 3988 | + | i.statusErr = "settings: " + err.Error() | |
| 3989 | + | i.mu.Unlock() | |
| 3990 | + | return | |
| 3991 | + | } | |
| 3992 | + | } | |
| 3993 | + | i.mu.Lock() | |
| 3994 | + | i.view.CollapseToolCall = value | |
| 3995 | + | i.view.InvalidateRenderCache() | |
| 3996 | + | i.statusOK = "collapse tool call rendering " + onOff(value) | |
| 3997 | + | i.statusErr = "" | |
| 3998 | + | i.mu.Unlock() | |
| 3969 | 3999 | case "show_instructions_at_startup": | |
| 3970 | 4000 | val := value | |
| 3971 | 4001 | i.cfg.ShowInstructionsAtStartup = &val | |
| @@ -111,6 +111,10 @@ type InteractiveConfig struct { | |||||
| 111 | // When true, it enables both flat tool calls and compact user turns. | 111 | // When true, it enables both flat tool calls and compact user turns. | ||
| 112 | CompactMode *bool | 112 | CompactMode *bool | ||
| 113 | 113 | ||||
| 114 | + | // CollapseToolCall hides call and result content while | |||
| 115 | + | // retaining each tool-call header. | |||
| 116 | + | CollapseToolCall *bool | |||
| 117 | + | ||||
| 114 | // TUIInputStyle controls the main input rendering: plain or lines. | 118 | // TUIInputStyle controls the main input rendering: plain or lines. | ||
| 115 | TUIInputStyle string | 119 | TUIInputStyle string | ||
| 116 | 120 | ||||
| @@ -350,6 +354,7 @@ type SettingsStore interface { | |||||
| 350 | SetRecursiveFileSuggest(enabled bool) error | 354 | SetRecursiveFileSuggest(enabled bool) error | ||
| 351 | SetRespectGitignore(enabled bool) error | 355 | SetRespectGitignore(enabled bool) error | ||
| 352 | SetCompactMode(enabled bool) error | 356 | SetCompactMode(enabled bool) error | ||
| 357 | + | SetCollapseToolCall(enabled bool) error | |||
| 353 | SetTUIInputStyle(style string) error | 358 | SetTUIInputStyle(style string) error | ||
| 354 | SetTUIStatusPosition(position string) error | 359 | SetTUIStatusPosition(position string) error | ||
| 355 | SetTUIWorkingPosition(position string) error | 360 | SetTUIWorkingPosition(position string) error | ||
| @@ -626,6 +631,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive { | |||||
| 626 | FlatTools: cfg.FlatTools, | 631 | FlatTools: cfg.FlatTools, | ||
| 627 | CompactUser: cfg.CompactUser, | 632 | CompactUser: cfg.CompactUser, | ||
| 628 | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | 633 | CompactMode: cfg.CompactMode != nil && *cfg.CompactMode, | ||
| 634 | + | CollapseToolCall: cfg.CollapseToolCall != nil && *cfg.CollapseToolCall, | |||
| 629 | StartupAgentName: startupAgentName, | 635 | StartupAgentName: startupAgentName, | ||
| 630 | StartupContextPaths: startupContextPaths, | 636 | StartupContextPaths: startupContextPaths, | ||
| 631 | StartupExtensionNames: startupExtensionNames, | 637 | StartupExtensionNames: startupExtensionNames, | ||
| @@ -3416,6 +3422,7 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3416 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | 3422 | recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest | ||
| 3417 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | 3423 | respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore | ||
| 3418 | compactMode := i.compactModeEnabled() | 3424 | compactMode := i.compactModeEnabled() | ||
| 3425 | + | collapseToolCall := i.cfg.CollapseToolCall != nil && *i.cfg.CollapseToolCall | |||
| 3419 | showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup | 3426 | showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup | ||
| 3420 | inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) | 3427 | inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle) | ||
| 3421 | statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition) | 3428 | statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition) | ||
| @@ -3557,6 +3564,12 @@ func (i *Interactive) openSettingsDialog() { | |||||
| 3557 | desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles", | 3564 | desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles", | ||
| 3558 | value: compactMode, | 3565 | value: compactMode, | ||
| 3559 | }, | 3566 | }, | ||
| 3567 | + | { | |||
| 3568 | + | key: "collapse_tool_call", | |||
| 3569 | + | label: "collapse tool call rendering", | |||
| 3570 | + | desc: "show only the tool-call header, hiding call arguments and result content", | |||
| 3571 | + | value: collapseToolCall, | |||
| 3572 | + | }, | |||
| 3560 | { | 3573 | { | ||
| 3561 | key: "show_instructions_at_startup", | 3574 | key: "show_instructions_at_startup", | ||
| 3562 | label: "show loaded resources at startup", | 3575 | label: "show loaded resources at startup", | ||
| @@ -3966,6 +3979,23 @@ func (i *Interactive) applySettingToggle(key string, value bool) { | |||||
| 3966 | i.statusOK = "compact transcript rendering " + onOff(value) | 3979 | i.statusOK = "compact transcript rendering " + onOff(value) | ||
| 3967 | i.statusErr = "" | 3980 | i.statusErr = "" | ||
| 3968 | i.mu.Unlock() | 3981 | i.mu.Unlock() | ||
| 3982 | + | case "collapse_tool_call": | |||
| 3983 | + | val := value | |||
| 3984 | + | i.cfg.CollapseToolCall = &val | |||
| 3985 | + | if i.cfg.SettingsStore != nil { | |||
| 3986 | + | if err := i.cfg.SettingsStore.SetCollapseToolCall(value); err != nil { | |||
| 3987 | + | i.mu.Lock() | |||
| 3988 | + | i.statusErr = "settings: " + err.Error() | |||
| 3989 | + | i.mu.Unlock() | |||
| 3990 | + | return | |||
| 3991 | + | } | |||
| 3992 | + | } | |||
| 3993 | + | i.mu.Lock() | |||
| 3994 | + | i.view.CollapseToolCall = value | |||
| 3995 | + | i.view.InvalidateRenderCache() | |||
| 3996 | + | i.statusOK = "collapse tool call rendering " + onOff(value) | |||
| 3997 | + | i.statusErr = "" | |||
| 3998 | + | i.mu.Unlock() | |||
| 3969 | case "show_instructions_at_startup": | 3999 | case "show_instructions_at_startup": | ||
| 3970 | val := value | 4000 | val := value | ||
| 3971 | i.cfg.ShowInstructionsAtStartup = &val | 4001 | i.cfg.ShowInstructionsAtStartup = &val | ||
M packages/agent/settings_store.go +9 -0
| @@ -104,6 +104,15 @@ func (configSettingsStore) SetCompactMode(enabled bool) error { | |||
| 104 | 104 | return SaveConfig(cfg) | |
| 105 | 105 | } | |
| 106 | 106 | ||
| 107 | + | func (configSettingsStore) SetCollapseToolCall(enabled bool) error { | |
| 108 | + | cfg, err := LoadConfig() | |
| 109 | + | if err != nil { | |
| 110 | + | return err | |
| 111 | + | } | |
| 112 | + | cfg.CollapseToolCall = &enabled | |
| 113 | + | return SaveConfig(cfg) | |
| 114 | + | } | |
| 115 | + | ||
| 107 | 116 | func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error { | |
| 108 | 117 | cfg, err := LoadConfig() | |
| 109 | 118 | if err != nil { | |
| @@ -104,6 +104,15 @@ func (configSettingsStore) SetCompactMode(enabled bool) error { | |||||
| 104 | return SaveConfig(cfg) | 104 | return SaveConfig(cfg) | ||
| 105 | } | 105 | } | ||
| 106 | 106 | ||||
| 107 | + | func (configSettingsStore) SetCollapseToolCall(enabled bool) error { | |||
| 108 | + | cfg, err := LoadConfig() | |||
| 109 | + | if err != nil { | |||
| 110 | + | return err | |||
| 111 | + | } | |||
| 112 | + | cfg.CollapseToolCall = &enabled | |||
| 113 | + | return SaveConfig(cfg) | |||
| 114 | + | } | |||
| 115 | + | ||||
| 107 | func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error { | 116 | func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error { | ||
| 108 | cfg, err := LoadConfig() | 117 | cfg, err := LoadConfig() | ||
| 109 | if err != nil { | 118 | if err != nil { | ||
M packages/tui/view.go +49 -0
| @@ -158,6 +158,10 @@ type View struct { | |||
| 158 | 158 | // the current spacious rendering. | |
| 159 | 159 | CompactMode bool | |
| 160 | 160 | ||
| 161 | + | // CollapseToolCall shows only each tool-call header, hiding | |
| 162 | + | // both call previews and result content. | |
| 163 | + | CollapseToolCall bool | |
| 164 | + | ||
| 161 | 165 | // ExpandAll forces every long tool result to render in full. | |
| 162 | 166 | // Toggled from the tui by ctrl+o. When false, results longer than | |
| 163 | 167 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | |
| @@ -790,6 +794,11 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||
| 790 | 794 | // from the matching ToolCallBlock so multiple calls | |
| 791 | 795 | // in one assistant message render as N adjacent boxes | |
| 792 | 796 | // instead of stacking unclosed top edges. | |
| 797 | + | if v.CollapseToolCall && !v.ExpandAll { | |
| 798 | + | body := v.renderToolResultContent(tr.Content, width, color, path, startLine) | |
| 799 | + | lines = append(lines, collapsedToolBoxWithBody(v.Theme, label, body, width, v.CompactMode)...) | |
| 800 | + | continue | |
| 801 | + | } | |
| 793 | 802 | if v.FlatTools || v.CompactMode { | |
| 794 | 803 | if v.CompactMode { | |
| 795 | 804 | lines = append(lines, compactToolBlank(v.Theme, width)) | |
| @@ -858,6 +867,21 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||
| 858 | 867 | } | |
| 859 | 868 | label := tc.Name + " " + arg | |
| 860 | 869 | ||
| 870 | + | if v.CollapseToolCall && !v.ExpandAll { | |
| 871 | + | bodyText := tc.Result | |
| 872 | + | if bodyText == "" { | |
| 873 | + | bodyText = tc.Preview | |
| 874 | + | } | |
| 875 | + | var body []string | |
| 876 | + | if bodyText != "" { | |
| 877 | + | body = v.renderLiveToolResult(bodyText, flatToolBodyRenderWidth(width), v.Theme.ToolOut, tc.LivePath) | |
| 878 | + | } else { | |
| 879 | + | body = v.renderLiveToolBody(tc, width) | |
| 880 | + | } | |
| 881 | + | body = v.collapseToolBody(body, false) | |
| 882 | + | return collapsedToolBoxWithBody(v.Theme, label, body, width, v.CompactMode) | |
| 883 | + | } | |
| 884 | + | ||
| 861 | 885 | // No leading blank: Build()'s inter-message separator already | |
| 862 | 886 | // places one blank row between the previous transcript content | |
| 863 | 887 | // and the live overlay, matching the spacing the same call has | |
| @@ -1106,6 +1130,31 @@ const toolBoxInnerPad = 1 | |||
| 1106 | 1130 | // running edge-to-edge while user/assistant rows sit indented. | |
| 1107 | 1131 | const toolBoxOuterMargin = 2 | |
| 1108 | 1132 | ||
| 1133 | + | // collapsedToolBoxWithBody renders a tool frame with at most the final | |
| 1134 | + | // meaningful preview line, preserving the normal spacing and borders. | |
| 1135 | + | func collapsedToolBoxWithBody(th Theme, label string, body []string, width int, compact bool) []string { | |
| 1136 | + | last := "" | |
| 1137 | + | for i := len(body) - 1; i >= 0; i-- { | |
| 1138 | + | if _, line := parseImageFootprint(body[i]); visibleWidth(strings.TrimSpace(line)) > 0 { | |
| 1139 | + | last = line | |
| 1140 | + | break | |
| 1141 | + | } | |
| 1142 | + | } | |
| 1143 | + | if compact { | |
| 1144 | + | out := []string{compactToolBlank(th, width), toolHeaderLine(th, label, width, true)} | |
| 1145 | + | if last != "" { | |
| 1146 | + | out = append(out, compactToolBlank(th, width), toolBodyLine(th, last, width, true)) | |
| 1147 | + | } | |
| 1148 | + | return append(out, compactToolBlank(th, width)) | |
| 1149 | + | } | |
| 1150 | + | out := []string{toolBoxTop(th, label, width), toolBoxSide(th, "", width)} | |
| 1151 | + | if last != "" { | |
| 1152 | + | out = append(out, toolBoxSide(th, last, width)) | |
| 1153 | + | } | |
| 1154 | + | out = append(out, toolBoxSide(th, "", width), toolBoxBottom(th, width)) | |
| 1155 | + | return out | |
| 1156 | + | } | |
| 1157 | + | ||
| 1109 | 1158 | // toolBoxTop renders the labelled top edge of a tool block: | |
| 1110 | 1159 | // | |
| 1111 | 1160 | // ┌─ bash xcrun simctl list devices ─────────────────────────────┐ | |
| @@ -158,6 +158,10 @@ type View struct { | |||||
| 158 | // the current spacious rendering. | 158 | // the current spacious rendering. | ||
| 159 | CompactMode bool | 159 | CompactMode bool | ||
| 160 | 160 | ||||
| 161 | + | // CollapseToolCall shows only each tool-call header, hiding | |||
| 162 | + | // both call previews and result content. | |||
| 163 | + | CollapseToolCall bool | |||
| 164 | + | ||||
| 161 | // ExpandAll forces every long tool result to render in full. | 165 | // ExpandAll forces every long tool result to render in full. | ||
| 162 | // Toggled from the tui by ctrl+o. When false, results longer than | 166 | // Toggled from the tui by ctrl+o. When false, results longer than | ||
| 163 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | 167 | // ToolCollapseLines collapse to ToolCollapsePreview lines plus a | ||
| @@ -790,6 +794,11 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str | |||||
| 790 | // from the matching ToolCallBlock so multiple calls | 794 | // from the matching ToolCallBlock so multiple calls | ||
| 791 | // in one assistant message render as N adjacent boxes | 795 | // in one assistant message render as N adjacent boxes | ||
| 792 | // instead of stacking unclosed top edges. | 796 | // instead of stacking unclosed top edges. | ||
| 797 | + | if v.CollapseToolCall && !v.ExpandAll { | |||
| 798 | + | body := v.renderToolResultContent(tr.Content, width, color, path, startLine) | |||
| 799 | + | lines = append(lines, collapsedToolBoxWithBody(v.Theme, label, body, width, v.CompactMode)...) | |||
| 800 | + | continue | |||
| 801 | + | } | |||
| 793 | if v.FlatTools || v.CompactMode { | 802 | if v.FlatTools || v.CompactMode { | ||
| 794 | if v.CompactMode { | 803 | if v.CompactMode { | ||
| 795 | lines = append(lines, compactToolBlank(v.Theme, width)) | 804 | lines = append(lines, compactToolBlank(v.Theme, width)) | ||
| @@ -858,6 +867,21 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string { | |||||
| 858 | } | 867 | } | ||
| 859 | label := tc.Name + " " + arg | 868 | label := tc.Name + " " + arg | ||
| 860 | 869 | ||||
| 870 | + | if v.CollapseToolCall && !v.ExpandAll { | |||
| 871 | + | bodyText := tc.Result | |||
| 872 | + | if bodyText == "" { | |||
| 873 | + | bodyText = tc.Preview | |||
| 874 | + | } | |||
| 875 | + | var body []string | |||
| 876 | + | if bodyText != "" { | |||
| 877 | + | body = v.renderLiveToolResult(bodyText, flatToolBodyRenderWidth(width), v.Theme.ToolOut, tc.LivePath) | |||
| 878 | + | } else { | |||
| 879 | + | body = v.renderLiveToolBody(tc, width) | |||
| 880 | + | } | |||
| 881 | + | body = v.collapseToolBody(body, false) | |||
| 882 | + | return collapsedToolBoxWithBody(v.Theme, label, body, width, v.CompactMode) | |||
| 883 | + | } | |||
| 884 | + | ||||
| 861 | // No leading blank: Build()'s inter-message separator already | 885 | // No leading blank: Build()'s inter-message separator already | ||
| 862 | // places one blank row between the previous transcript content | 886 | // places one blank row between the previous transcript content | ||
| 863 | // and the live overlay, matching the spacing the same call has | 887 | // and the live overlay, matching the spacing the same call has | ||
| @@ -1106,6 +1130,31 @@ const toolBoxInnerPad = 1 | |||||
| 1106 | // running edge-to-edge while user/assistant rows sit indented. | 1130 | // running edge-to-edge while user/assistant rows sit indented. | ||
| 1107 | const toolBoxOuterMargin = 2 | 1131 | const toolBoxOuterMargin = 2 | ||
| 1108 | 1132 | ||||
| 1133 | + | // collapsedToolBoxWithBody renders a tool frame with at most the final | |||
| 1134 | + | // meaningful preview line, preserving the normal spacing and borders. | |||
| 1135 | + | func collapsedToolBoxWithBody(th Theme, label string, body []string, width int, compact bool) []string { | |||
| 1136 | + | last := "" | |||
| 1137 | + | for i := len(body) - 1; i >= 0; i-- { | |||
| 1138 | + | if _, line := parseImageFootprint(body[i]); visibleWidth(strings.TrimSpace(line)) > 0 { | |||
| 1139 | + | last = line | |||
| 1140 | + | break | |||
| 1141 | + | } | |||
| 1142 | + | } | |||
| 1143 | + | if compact { | |||
| 1144 | + | out := []string{compactToolBlank(th, width), toolHeaderLine(th, label, width, true)} | |||
| 1145 | + | if last != "" { | |||
| 1146 | + | out = append(out, compactToolBlank(th, width), toolBodyLine(th, last, width, true)) | |||
| 1147 | + | } | |||
| 1148 | + | return append(out, compactToolBlank(th, width)) | |||
| 1149 | + | } | |||
| 1150 | + | out := []string{toolBoxTop(th, label, width), toolBoxSide(th, "", width)} | |||
| 1151 | + | if last != "" { | |||
| 1152 | + | out = append(out, toolBoxSide(th, last, width)) | |||
| 1153 | + | } | |||
| 1154 | + | out = append(out, toolBoxSide(th, "", width), toolBoxBottom(th, width)) | |||
| 1155 | + | return out | |||
| 1156 | + | } | |||
| 1157 | + | ||||
| 1109 | // toolBoxTop renders the labelled top edge of a tool block: | 1158 | // toolBoxTop renders the labelled top edge of a tool block: | ||
| 1110 | // | 1159 | // | ||
| 1111 | // ┌─ bash xcrun simctl list devices ─────────────────────────────┐ | 1160 | // ┌─ bash xcrun simctl list devices ─────────────────────────────┐ | ||
A packages/tui/view_collapse_tools_test.go +74 -0
| @@ -0,0 +1,74 @@ | |||
| 1 | + | package tui | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/json" | |
| 5 | + | "strings" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/patriceckhart/zot/packages/provider" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestCollapseToolCallShowsLastBoxPreviewLine(t *testing.T) { | |
| 12 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |
| 13 | + | v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{ | |
| 14 | + | ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "first-output\nlast-output\n", | |
| 15 | + | }}} | |
| 16 | + | plain := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |
| 17 | + | if !strings.Contains(plain, "bash") { | |
| 18 | + | t.Fatalf("collapsed render lost tool header:\n%s", plain) | |
| 19 | + | } | |
| 20 | + | if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") { | |
| 21 | + | t.Fatalf("collapsed render did not show only the last result line:\n%s", plain) | |
| 22 | + | } | |
| 23 | + | if !strings.ContainsAny(plain, "┌┐") || !strings.ContainsAny(plain, "└┘") || !strings.Contains(plain, "│") { | |
| 24 | + | t.Fatalf("collapsed box render did not retain the complete empty frame:\n%s", plain) | |
| 25 | + | } | |
| 26 | + | v.ExpandAll = true | |
| 27 | + | expanded := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |
| 28 | + | if !strings.Contains(expanded, "first-output") { | |
| 29 | + | t.Fatalf("ctrl-o expansion did not reveal non-compact collapsed tool result:\n%s", expanded) | |
| 30 | + | } | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func TestCollapseToolCallShowsLastCompactPreviewLine(t *testing.T) { | |
| 34 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |
| 35 | + | v := View{ | |
| 36 | + | Theme: Dark, | |
| 37 | + | CompactMode: true, | |
| 38 | + | CollapseToolCall: true, | |
| 39 | + | Messages: []provider.Message{ | |
| 40 | + | {Role: provider.RoleAssistant, Content: []provider.Content{ | |
| 41 | + | provider.ToolCallBlock{ID: "toolu_1", Name: "bash", Arguments: args}, | |
| 42 | + | }}, | |
| 43 | + | {Role: provider.RoleTool, Content: []provider.Content{ | |
| 44 | + | provider.ToolResultBlock{CallID: "toolu_1", Content: []provider.Content{provider.TextBlock{Text: "first-output\nlast-output"}}}, | |
| 45 | + | }}, | |
| 46 | + | }, | |
| 47 | + | } | |
| 48 | + | plain := stripANSI(strings.Join(v.Build(80), "\n")) | |
| 49 | + | if !strings.Contains(plain, "bash") { | |
| 50 | + | t.Fatalf("collapsed compact render lost tool header:\n%s", plain) | |
| 51 | + | } | |
| 52 | + | if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") { | |
| 53 | + | t.Fatalf("collapsed compact render did not show only the last result line:\n%s", plain) | |
| 54 | + | } | |
| 55 | + | v.ExpandAll = true | |
| 56 | + | expanded := stripANSI(strings.Join(v.Build(80), "\n")) | |
| 57 | + | if !strings.Contains(expanded, "first-output") { | |
| 58 | + | t.Fatalf("ctrl-o expansion did not reveal collapsed tool result:\n%s", expanded) | |
| 59 | + | } | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | func TestCollapseToolCallShowsLastLivePreviewLine(t *testing.T) { | |
| 63 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |
| 64 | + | v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{ | |
| 65 | + | ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), RawJSONBuf: string(args), | |
| 66 | + | }}} | |
| 67 | + | plain := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |
| 68 | + | if !strings.Contains(plain, "$ echo") { | |
| 69 | + | t.Fatalf("collapsed live render lost its one-line preview:\n%s", plain) | |
| 70 | + | } | |
| 71 | + | if !strings.Contains(plain, "bash") { | |
| 72 | + | t.Fatalf("collapsed live render lost tool header:\n%s", plain) | |
| 73 | + | } | |
| 74 | + | } | |
| @@ -0,0 +1,74 @@ | |||||
| 1 | + | package tui | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/json" | |||
| 5 | + | "strings" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "github.com/patriceckhart/zot/packages/provider" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestCollapseToolCallShowsLastBoxPreviewLine(t *testing.T) { | |||
| 12 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |||
| 13 | + | v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{ | |||
| 14 | + | ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), Result: "first-output\nlast-output\n", | |||
| 15 | + | }}} | |||
| 16 | + | plain := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |||
| 17 | + | if !strings.Contains(plain, "bash") { | |||
| 18 | + | t.Fatalf("collapsed render lost tool header:\n%s", plain) | |||
| 19 | + | } | |||
| 20 | + | if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") { | |||
| 21 | + | t.Fatalf("collapsed render did not show only the last result line:\n%s", plain) | |||
| 22 | + | } | |||
| 23 | + | if !strings.ContainsAny(plain, "┌┐") || !strings.ContainsAny(plain, "└┘") || !strings.Contains(plain, "│") { | |||
| 24 | + | t.Fatalf("collapsed box render did not retain the complete empty frame:\n%s", plain) | |||
| 25 | + | } | |||
| 26 | + | v.ExpandAll = true | |||
| 27 | + | expanded := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |||
| 28 | + | if !strings.Contains(expanded, "first-output") { | |||
| 29 | + | t.Fatalf("ctrl-o expansion did not reveal non-compact collapsed tool result:\n%s", expanded) | |||
| 30 | + | } | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func TestCollapseToolCallShowsLastCompactPreviewLine(t *testing.T) { | |||
| 34 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |||
| 35 | + | v := View{ | |||
| 36 | + | Theme: Dark, | |||
| 37 | + | CompactMode: true, | |||
| 38 | + | CollapseToolCall: true, | |||
| 39 | + | Messages: []provider.Message{ | |||
| 40 | + | {Role: provider.RoleAssistant, Content: []provider.Content{ | |||
| 41 | + | provider.ToolCallBlock{ID: "toolu_1", Name: "bash", Arguments: args}, | |||
| 42 | + | }}, | |||
| 43 | + | {Role: provider.RoleTool, Content: []provider.Content{ | |||
| 44 | + | provider.ToolResultBlock{CallID: "toolu_1", Content: []provider.Content{provider.TextBlock{Text: "first-output\nlast-output"}}}, | |||
| 45 | + | }}, | |||
| 46 | + | }, | |||
| 47 | + | } | |||
| 48 | + | plain := stripANSI(strings.Join(v.Build(80), "\n")) | |||
| 49 | + | if !strings.Contains(plain, "bash") { | |||
| 50 | + | t.Fatalf("collapsed compact render lost tool header:\n%s", plain) | |||
| 51 | + | } | |||
| 52 | + | if !strings.Contains(plain, "last-output") || strings.Contains(plain, "first-output") { | |||
| 53 | + | t.Fatalf("collapsed compact render did not show only the last result line:\n%s", plain) | |||
| 54 | + | } | |||
| 55 | + | v.ExpandAll = true | |||
| 56 | + | expanded := stripANSI(strings.Join(v.Build(80), "\n")) | |||
| 57 | + | if !strings.Contains(expanded, "first-output") { | |||
| 58 | + | t.Fatalf("ctrl-o expansion did not reveal collapsed tool result:\n%s", expanded) | |||
| 59 | + | } | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | func TestCollapseToolCallShowsLastLivePreviewLine(t *testing.T) { | |||
| 63 | + | args := json.RawMessage(`{"command":"echo preview"}`) | |||
| 64 | + | v := View{Theme: Dark, CollapseToolCall: true, ToolCalls: []ToolCallView{{ | |||
| 65 | + | ID: "toolu_1", Name: "bash", Args: ShortArgs("bash", args), RawJSONBuf: string(args), | |||
| 66 | + | }}} | |||
| 67 | + | plain := stripANSI(strings.Join(v.BuildLive(80), "\n")) | |||
| 68 | + | if !strings.Contains(plain, "$ echo") { | |||
| 69 | + | t.Fatalf("collapsed live render lost its one-line preview:\n%s", plain) | |||
| 70 | + | } | |||
| 71 | + | if !strings.Contains(plain, "bash") { | |||
| 72 | + | t.Fatalf("collapsed live render lost tool header:\n%s", plain) | |||
| 73 | + | } | |||
| 74 | + | } | |||