detached 88a6f6e

tui: make tool-call arg width configurable via ZOT_TOOL_ARG_WIDTH (#70)

The tool-call header truncates the primary argument (path/command/query)
at a hardcoded 60 cells, which clips long queries on wide terminals.

Add a toolArgWidth() helper (mirroring CellAspectRatio) that reads the
ZOT_TOOL_ARG_WIDTH env var, defaulting to 60 and clamping to [20, 500]
so a stray value can never produce an unreadable header or an
out-of-range slice. Replace the three hardcoded 60s in ShortArgs.

Documented under Tool rendering in the README; covered by tests.
commit
88a6f6e481904a71b78983e198b8351ed8fd4790
author
Graham Primm · · 2026-07-08 16:50 UTC
committer
GitHub · · 2026-07-08 16:50 UTC
parent
30f9ca5
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.2.67
3 files · +112 -6
M README.md +14 -0
@@ -619,6 +619,20 @@ ZOT_FLAT_TOOLS=0 zot # boxes, even if config.json says "flat"
619619
620620 Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and `ctrl+o` still expands a truncated result.
621621
622+### Tool arg width
623+
624+The header line for a tool call shows the tool name plus a one-line summary of its primary argument — a `path`, a `command`, or a query. That summary is truncated to 60 cells by default (`web_answer What is the best architecture to implement resilience wit...`). On a wide terminal that can clip long queries more than you'd like, so set the `ZOT_TOOL_ARG_WIDTH` env var to raise (or lower) the limit:
625+
626+```sh
627+ZOT_TOOL_ARG_WIDTH=120 zot # allow up to 120 cells before truncating
628+```
629+
630+| Value | Effect |
631+|---|---|
632+| unset (default) | Truncate the arg summary at 60 cells. |
633+| integer in `[20, 500]` | Truncate at that many cells instead. |
634+| anything else | Ignored; falls back to the 60-cell default. |
635+
622636 ## Compact input
623637
624638 By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a `▌` accent bar down the left. So even a one-line prompt occupies three rows. Set `compact_input` to collapse it to a single quiet `▌ your text` gutter line per wrapped row — no padding rows, no background tint.
@@ -619,6 +619,20 @@ ZOT_FLAT_TOOLS=0 zot # boxes, even if config.json says "flat"
619 619
620 Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and `ctrl+o` still expands a truncated result.620 Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and `ctrl+o` still expands a truncated result.
621 621
622+### Tool arg width
623+
624+The header line for a tool call shows the tool name plus a one-line summary of its primary argument — a `path`, a `command`, or a query. That summary is truncated to 60 cells by default (`web_answer What is the best architecture to implement resilience wit...`). On a wide terminal that can clip long queries more than you'd like, so set the `ZOT_TOOL_ARG_WIDTH` env var to raise (or lower) the limit:
625+
626+```sh
627+ZOT_TOOL_ARG_WIDTH=120 zot # allow up to 120 cells before truncating
628+```
629+
630+| Value | Effect |
631+|---|---|
632+| unset (default) | Truncate the arg summary at 60 cells. |
633+| integer in `[20, 500]` | Truncate at that many cells instead. |
634+| anything else | Ignored; falls back to the 60-cell default. |
635+
622 ## Compact input636 ## Compact input
623 637
624 By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a `▌` accent bar down the left. So even a one-line prompt occupies three rows. Set `compact_input` to collapse it to a single quiet `▌ your text` gutter line per wrapped row — no padding rows, no background tint.638 By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a `▌` accent bar down the left. So even a one-line prompt occupies three rows. Set `compact_input` to collapse it to a single quiet `▌ your text` gutter line per wrapped row — no padding rows, no background tint.
M packages/tui/view.go +35 -6
@@ -2131,19 +2131,48 @@ func toolResultBlock(th Theme, text string, width int, color int) []string {
21312131 return out
21322132 }
21332133
2134+// defaultToolArgWidth is the number of cells the tool-call header
2135+// truncates the primary argument (path/command/query) to when the
2136+// ZOT_TOOL_ARG_WIDTH environment variable is unset or invalid.
2137+const defaultToolArgWidth = 60
2138+
2139+// toolArgWidth returns the maximum cell width for the truncated
2140+// primary argument shown in a tool-call header. It defaults to
2141+// defaultToolArgWidth but can be raised or lowered with the
2142+// ZOT_TOOL_ARG_WIDTH environment variable, which is useful for wide
2143+// terminals where the default clips long queries too aggressively.
2144+// Values outside the sane range [20, 500] are ignored so a stray or
2145+// malformed setting can never produce an unreadable header or an
2146+// out-of-range slice.
2147+func toolArgWidth() int {
2148+ v := strings.TrimSpace(os.Getenv("ZOT_TOOL_ARG_WIDTH"))
2149+ if v == "" {
2150+ return defaultToolArgWidth
2151+ }
2152+ n, err := strconv.Atoi(v)
2153+ if err != nil || n < 20 || n > 500 {
2154+ return defaultToolArgWidth
2155+ }
2156+ return n
2157+}
2158+
21342159 // ShortArgs renders a tool call's arguments into a one-line
21352160 // suffix for the "tool name <args>" header. tool is the tool
21362161 // name so we can add shape-specific decorations: for read we
21372162 // append the requested line range (e.g. "path:1-200") pulled
21382163 // from the offset/limit args, which is useful context at a
21392164 // glance without expanding the result body. Other tools keep
2140-// the legacy "path or command, truncated at 60 cells" shape.
2165+// the legacy "path or command, truncated" shape.
2166+//
2167+// The truncation width defaults to 60 cells but can be tuned via
2168+// the ZOT_TOOL_ARG_WIDTH environment variable (see toolArgWidth).
21412169 //
21422170 // Exported because the interactive mode pre-populates the
21432171 // ToolCallView.Args field with this value as soon as the tool
21442172 // call is announced, so the live overlay's header matches what
21452173 // the finalised transcript will later render.
21462174 func ShortArgs(tool string, raw json.RawMessage) string {
2175+ width := toolArgWidth()
21472176 var v any
21482177 if err := json.Unmarshal(raw, &v); err != nil {
21492178 return ""
@@ -2152,8 +2181,8 @@ func ShortArgs(tool string, raw json.RawMessage) string {
21522181 if !ok {
21532182 b, _ := json.Marshal(v)
21542183 s := oneLineToolLabel(string(b))
2155- if len(s) > 60 {
2156- s = s[:57] + "..."
2184+ if len(s) > width {
2185+ s = s[:width-3] + "..."
21572186 }
21582187 return s
21592188 }
@@ -2167,8 +2196,8 @@ func ShortArgs(tool string, raw json.RawMessage) string {
21672196 if primary == "" {
21682197 b, _ := json.Marshal(v)
21692198 s := oneLineToolLabel(string(b))
2170- if len(s) > 60 {
2171- s = s[:57] + "..."
2199+ if len(s) > width {
2200+ s = s[:width-3] + "..."
21722201 }
21732202 return s
21742203 }
@@ -2193,7 +2222,7 @@ func ShortArgs(tool string, raw json.RawMessage) string {
21932222
21942223 // Truncate the primary arg leaving room for the suffix so the
21952224 // range stays visible even on absurdly long paths.
2196- max := 60 - len(suffix)
2225+ max := width - len(suffix)
21972226 if max < 10 {
21982227 max = 10
21992228 }
@@ -2131,19 +2131,48 @@ func toolResultBlock(th Theme, text string, width int, color int) []string {
2131 return out2131 return out
2132 }2132 }
2133 2133
2134+// defaultToolArgWidth is the number of cells the tool-call header
2135+// truncates the primary argument (path/command/query) to when the
2136+// ZOT_TOOL_ARG_WIDTH environment variable is unset or invalid.
2137+const defaultToolArgWidth = 60
2138+
2139+// toolArgWidth returns the maximum cell width for the truncated
2140+// primary argument shown in a tool-call header. It defaults to
2141+// defaultToolArgWidth but can be raised or lowered with the
2142+// ZOT_TOOL_ARG_WIDTH environment variable, which is useful for wide
2143+// terminals where the default clips long queries too aggressively.
2144+// Values outside the sane range [20, 500] are ignored so a stray or
2145+// malformed setting can never produce an unreadable header or an
2146+// out-of-range slice.
2147+func toolArgWidth() int {
2148+ v := strings.TrimSpace(os.Getenv("ZOT_TOOL_ARG_WIDTH"))
2149+ if v == "" {
2150+ return defaultToolArgWidth
2151+ }
2152+ n, err := strconv.Atoi(v)
2153+ if err != nil || n < 20 || n > 500 {
2154+ return defaultToolArgWidth
2155+ }
2156+ return n
2157+}
2158+
2134 // ShortArgs renders a tool call's arguments into a one-line2159 // ShortArgs renders a tool call's arguments into a one-line
2135 // suffix for the "tool name <args>" header. tool is the tool2160 // suffix for the "tool name <args>" header. tool is the tool
2136 // name so we can add shape-specific decorations: for read we2161 // name so we can add shape-specific decorations: for read we
2137 // append the requested line range (e.g. "path:1-200") pulled2162 // append the requested line range (e.g. "path:1-200") pulled
2138 // from the offset/limit args, which is useful context at a2163 // from the offset/limit args, which is useful context at a
2139 // glance without expanding the result body. Other tools keep2164 // glance without expanding the result body. Other tools keep
2140-// the legacy "path or command, truncated at 60 cells" shape.2165+// the legacy "path or command, truncated" shape.
2166+//
2167+// The truncation width defaults to 60 cells but can be tuned via
2168+// the ZOT_TOOL_ARG_WIDTH environment variable (see toolArgWidth).
2141 //2169 //
2142 // Exported because the interactive mode pre-populates the2170 // Exported because the interactive mode pre-populates the
2143 // ToolCallView.Args field with this value as soon as the tool2171 // ToolCallView.Args field with this value as soon as the tool
2144 // call is announced, so the live overlay's header matches what2172 // call is announced, so the live overlay's header matches what
2145 // the finalised transcript will later render.2173 // the finalised transcript will later render.
2146 func ShortArgs(tool string, raw json.RawMessage) string {2174 func ShortArgs(tool string, raw json.RawMessage) string {
2175+ width := toolArgWidth()
2147 var v any2176 var v any
2148 if err := json.Unmarshal(raw, &v); err != nil {2177 if err := json.Unmarshal(raw, &v); err != nil {
2149 return ""2178 return ""
@@ -2152,8 +2181,8 @@ func ShortArgs(tool string, raw json.RawMessage) string {
2152 if !ok {2181 if !ok {
2153 b, _ := json.Marshal(v)2182 b, _ := json.Marshal(v)
2154 s := oneLineToolLabel(string(b))2183 s := oneLineToolLabel(string(b))
2155- if len(s) > 60 {2184+ if len(s) > width {
2156- s = s[:57] + "..."2185+ s = s[:width-3] + "..."
2157 }2186 }
2158 return s2187 return s
2159 }2188 }
@@ -2167,8 +2196,8 @@ func ShortArgs(tool string, raw json.RawMessage) string {
2167 if primary == "" {2196 if primary == "" {
2168 b, _ := json.Marshal(v)2197 b, _ := json.Marshal(v)
2169 s := oneLineToolLabel(string(b))2198 s := oneLineToolLabel(string(b))
2170- if len(s) > 60 {2199+ if len(s) > width {
2171- s = s[:57] + "..."2200+ s = s[:width-3] + "..."
2172 }2201 }
2173 return s2202 return s
2174 }2203 }
@@ -2193,7 +2222,7 @@ func ShortArgs(tool string, raw json.RawMessage) string {
2193 2222
2194 // Truncate the primary arg leaving room for the suffix so the2223 // Truncate the primary arg leaving room for the suffix so the
2195 // range stays visible even on absurdly long paths.2224 // range stays visible even on absurdly long paths.
2196- max := 60 - len(suffix)2225+ max := width - len(suffix)
2197 if max < 10 {2226 if max < 10 {
2198 max = 102227 max = 10
2199 }2228 }
A packages/tui/view_tool_arg_width_test.go +63 -0
@@ -0,0 +1,63 @@
1+package tui
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestToolArgWidthDefault(t *testing.T) {
10+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "")
11+ if got := toolArgWidth(); got != defaultToolArgWidth {
12+ t.Fatalf("toolArgWidth() with unset env = %d, want %d", got, defaultToolArgWidth)
13+ }
14+}
15+
16+func TestToolArgWidthEnvOverride(t *testing.T) {
17+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
18+ if got := toolArgWidth(); got != 120 {
19+ t.Fatalf("toolArgWidth() = %d, want 120", got)
20+ }
21+}
22+
23+func TestToolArgWidthIgnoresInvalid(t *testing.T) {
24+ cases := []string{"nope", "0", "10", "501", "-5", "12.5"}
25+ for _, c := range cases {
26+ t.Setenv("ZOT_TOOL_ARG_WIDTH", c)
27+ if got := toolArgWidth(); got != defaultToolArgWidth {
28+ t.Fatalf("toolArgWidth() with %q = %d, want default %d", c, got, defaultToolArgWidth)
29+ }
30+ }
31+}
32+
33+func TestShortArgsTruncatesAtDefaultWidth(t *testing.T) {
34+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "")
35+ long := strings.Repeat("a", 200)
36+ raw := json.RawMessage(`{"command":"` + long + `"}`)
37+ got := ShortArgs("web_answer", raw)
38+ if len(got) != defaultToolArgWidth {
39+ t.Fatalf("ShortArgs length = %d, want %d (%q)", len(got), defaultToolArgWidth, got)
40+ }
41+ if !strings.HasSuffix(got, "...") {
42+ t.Fatalf("ShortArgs should end with ellipsis, got %q", got)
43+ }
44+}
45+
46+func TestShortArgsRespectsWiderWidth(t *testing.T) {
47+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
48+ long := strings.Repeat("a", 200)
49+ raw := json.RawMessage(`{"command":"` + long + `"}`)
50+ got := ShortArgs("web_answer", raw)
51+ if len(got) != 120 {
52+ t.Fatalf("ShortArgs length = %d, want 120", len(got))
53+ }
54+}
55+
56+func TestShortArgsNoTruncationWhenShort(t *testing.T) {
57+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
58+ raw := json.RawMessage(`{"command":"short query"}`)
59+ got := ShortArgs("web_answer", raw)
60+ if got != "short query" {
61+ t.Fatalf("ShortArgs = %q, want %q", got, "short query")
62+ }
63+}
@@ -0,0 +1,63 @@
1+package tui
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestToolArgWidthDefault(t *testing.T) {
10+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "")
11+ if got := toolArgWidth(); got != defaultToolArgWidth {
12+ t.Fatalf("toolArgWidth() with unset env = %d, want %d", got, defaultToolArgWidth)
13+ }
14+}
15+
16+func TestToolArgWidthEnvOverride(t *testing.T) {
17+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
18+ if got := toolArgWidth(); got != 120 {
19+ t.Fatalf("toolArgWidth() = %d, want 120", got)
20+ }
21+}
22+
23+func TestToolArgWidthIgnoresInvalid(t *testing.T) {
24+ cases := []string{"nope", "0", "10", "501", "-5", "12.5"}
25+ for _, c := range cases {
26+ t.Setenv("ZOT_TOOL_ARG_WIDTH", c)
27+ if got := toolArgWidth(); got != defaultToolArgWidth {
28+ t.Fatalf("toolArgWidth() with %q = %d, want default %d", c, got, defaultToolArgWidth)
29+ }
30+ }
31+}
32+
33+func TestShortArgsTruncatesAtDefaultWidth(t *testing.T) {
34+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "")
35+ long := strings.Repeat("a", 200)
36+ raw := json.RawMessage(`{"command":"` + long + `"}`)
37+ got := ShortArgs("web_answer", raw)
38+ if len(got) != defaultToolArgWidth {
39+ t.Fatalf("ShortArgs length = %d, want %d (%q)", len(got), defaultToolArgWidth, got)
40+ }
41+ if !strings.HasSuffix(got, "...") {
42+ t.Fatalf("ShortArgs should end with ellipsis, got %q", got)
43+ }
44+}
45+
46+func TestShortArgsRespectsWiderWidth(t *testing.T) {
47+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
48+ long := strings.Repeat("a", 200)
49+ raw := json.RawMessage(`{"command":"` + long + `"}`)
50+ got := ShortArgs("web_answer", raw)
51+ if len(got) != 120 {
52+ t.Fatalf("ShortArgs length = %d, want 120", len(got))
53+ }
54+}
55+
56+func TestShortArgsNoTruncationWhenShort(t *testing.T) {
57+ t.Setenv("ZOT_TOOL_ARG_WIDTH", "120")
58+ raw := json.RawMessage(`{"command":"short query"}`)
59+ got := ShortArgs("web_answer", raw)
60+ if got != "short query" {
61+ t.Fatalf("ShortArgs = %q, want %q", got, "short query")
62+ }
63+}

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 476

↑↓ moveenter openesc close