detached 67b6b2e

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 {
12871287 RecursiveFileSuggest: initialCfg.RecursiveFileSuggest,
12881288 RespectGitignore: initialCfg.RespectGitignore,
12891289 CompactMode: initialCfg.CompactMode,
1290+ CollapseToolCall: initialCfg.CollapseToolCall,
12901291 TUIInputStyle: initialCfg.TUIInputStyle,
12911292 TUIStatusPosition: initialCfg.TUIStatusPosition,
12921293 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 {
9595 // default; nil/missing means disabled. Toggle from /settings.
9696 CompactMode *bool `json:"compact_mode,omitempty"`
9797
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+
98103 // ShowInstructionsAtStartup lists loaded context files, extensions,
99104 // and user-installed skills above the transcript. Off by default;
100105 // 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 {
111111 // When true, it enables both flat tool calls and compact user turns.
112112 CompactMode *bool
113113
114+ // CollapseToolCall hides call and result content while
115+ // retaining each tool-call header.
116+ CollapseToolCall *bool
117+
114118 // TUIInputStyle controls the main input rendering: plain or lines.
115119 TUIInputStyle string
116120
@@ -350,6 +354,7 @@ type SettingsStore interface {
350354 SetRecursiveFileSuggest(enabled bool) error
351355 SetRespectGitignore(enabled bool) error
352356 SetCompactMode(enabled bool) error
357+ SetCollapseToolCall(enabled bool) error
353358 SetTUIInputStyle(style string) error
354359 SetTUIStatusPosition(position string) error
355360 SetTUIWorkingPosition(position string) error
@@ -626,6 +631,7 @@ func NewInteractive(cfg InteractiveConfig) *Interactive {
626631 FlatTools: cfg.FlatTools,
627632 CompactUser: cfg.CompactUser,
628633 CompactMode: cfg.CompactMode != nil && *cfg.CompactMode,
634+ CollapseToolCall: cfg.CollapseToolCall != nil && *cfg.CollapseToolCall,
629635 StartupAgentName: startupAgentName,
630636 StartupContextPaths: startupContextPaths,
631637 StartupExtensionNames: startupExtensionNames,
@@ -3416,6 +3422,7 @@ func (i *Interactive) openSettingsDialog() {
34163422 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
34173423 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore
34183424 compactMode := i.compactModeEnabled()
3425+ collapseToolCall := i.cfg.CollapseToolCall != nil && *i.cfg.CollapseToolCall
34193426 showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup
34203427 inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle)
34213428 statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition)
@@ -3557,6 +3564,12 @@ func (i *Interactive) openSettingsDialog() {
35573564 desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles",
35583565 value: compactMode,
35593566 },
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+ },
35603573 {
35613574 key: "show_instructions_at_startup",
35623575 label: "show loaded resources at startup",
@@ -3966,6 +3979,23 @@ func (i *Interactive) applySettingToggle(key string, value bool) {
39663979 i.statusOK = "compact transcript rendering " + onOff(value)
39673980 i.statusErr = ""
39683981 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()
39693999 case "show_instructions_at_startup":
39704000 val := value
39714001 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 *bool112 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 string119 TUIInputStyle string
116 120
@@ -350,6 +354,7 @@ type SettingsStore interface {
350 SetRecursiveFileSuggest(enabled bool) error354 SetRecursiveFileSuggest(enabled bool) error
351 SetRespectGitignore(enabled bool) error355 SetRespectGitignore(enabled bool) error
352 SetCompactMode(enabled bool) error356 SetCompactMode(enabled bool) error
357+ SetCollapseToolCall(enabled bool) error
353 SetTUIInputStyle(style string) error358 SetTUIInputStyle(style string) error
354 SetTUIStatusPosition(position string) error359 SetTUIStatusPosition(position string) error
355 SetTUIWorkingPosition(position string) error360 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.RecursiveFileSuggest3422 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
3417 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore3423 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.ShowInstructionsAtStartup3426 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 := value4000 val := value
3971 i.cfg.ShowInstructionsAtStartup = &val4001 i.cfg.ShowInstructionsAtStartup = &val
M packages/agent/settings_store.go +9 -0
@@ -104,6 +104,15 @@ func (configSettingsStore) SetCompactMode(enabled bool) error {
104104 return SaveConfig(cfg)
105105 }
106106
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+
107116 func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
108117 cfg, err := LoadConfig()
109118 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 {
158158 // the current spacious rendering.
159159 CompactMode bool
160160
161+ // CollapseToolCall shows only each tool-call header, hiding
162+ // both call previews and result content.
163+ CollapseToolCall bool
164+
161165 // ExpandAll forces every long tool result to render in full.
162166 // Toggled from the tui by ctrl+o. When false, results longer than
163167 // ToolCollapseLines collapse to ToolCollapsePreview lines plus a
@@ -790,6 +794,11 @@ func (v *View) renderMessage(m provider.Message, width int, turnOpen bool) []str
790794 // from the matching ToolCallBlock so multiple calls
791795 // in one assistant message render as N adjacent boxes
792796 // 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+ }
793802 if v.FlatTools || v.CompactMode {
794803 if v.CompactMode {
795804 lines = append(lines, compactToolBlank(v.Theme, width))
@@ -858,6 +867,21 @@ func (v *View) renderToolCall(tc ToolCallView, width int) []string {
858867 }
859868 label := tc.Name + " " + arg
860869
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+
861885 // No leading blank: Build()'s inter-message separator already
862886 // places one blank row between the previous transcript content
863887 // and the live overlay, matching the spacing the same call has
@@ -1106,6 +1130,31 @@ const toolBoxInnerPad = 1
11061130 // running edge-to-edge while user/assistant rows sit indented.
11071131 const toolBoxOuterMargin = 2
11081132
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+
11091158 // toolBoxTop renders the labelled top edge of a tool block:
11101159 //
11111160 // ┌─ bash xcrun simctl list devices ─────────────────────────────┐
@@ -158,6 +158,10 @@ type View struct {
158 // the current spacious rendering.158 // the current spacious rendering.
159 CompactMode bool159 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 than166 // Toggled from the tui by ctrl+o. When false, results longer than
163 // ToolCollapseLines collapse to ToolCollapsePreview lines plus a167 // 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 calls794 // from the matching ToolCallBlock so multiple calls
791 // in one assistant message render as N adjacent boxes795 // 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 + " " + arg868 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 already885 // No leading blank: Build()'s inter-message separator already
862 // places one blank row between the previous transcript content886 // places one blank row between the previous transcript content
863 // and the live overlay, matching the spacing the same call has887 // 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 = 21131 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close