Skip to content

Commit c79d69d

Browse files
e-kulikovJanDeDobbeleer
authored andcommitted
fix(runtime): honor COLUMNS fallback without a TTY
1 parent 1f2b3dd commit c79d69d

3 files changed

Lines changed: 86 additions & 10 deletions

File tree

.agents/skills/project-knowledge/references/terminal.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@
2828
- On Windows there is no SIGPIPE; child lifecycle management must rely on fd closure / stdin EOF.
2929
- On native Linux, process spawns cost 11-16ms - daemon architectures that pay off on Windows can
3030
be a wash there (see the bash serve revert in [bash](bash.md)).
31+
32+
## Statusline width detection
33+
34+
- On Unix, `terminal-dimensions` runs `stty size` against the renderer's stdin. Claude Code sends
35+
statusline JSON over that stdin, so it is not a TTY. A valid `COLUMNS` fallback must replace the
36+
resulting `stty` error; returning a non-zero width with the stale error makes
37+
`prompt.Engine.canWriteRightBlock` reject ordinary right-aligned blocks (verified 2026-08-03).

src/runtime/terminal_unix.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package runtime
44

55
import (
6+
"errors"
67
"strconv"
78
"strings"
89
"time"
@@ -62,18 +63,12 @@ func (term *Terminal) TerminalWidth() (int, error) {
6263
}
6364

6465
width, err := terminal.Width()
65-
if err != nil {
66-
log.Error(err)
66+
if width == 0 {
67+
width, err = resolveTerminalWidth(width, err, term.Getenv("COLUMNS"))
6768
}
6869

69-
// fetch width from the environment variable
70-
// in case the terminal width is not available
71-
if width == 0 {
72-
i, err := strconv.Atoi(term.Getenv("COLUMNS"))
73-
if err != nil {
74-
log.Error(err)
75-
}
76-
width = uint(i)
70+
if err != nil {
71+
log.Error(err)
7772
}
7873

7974
term.CmdFlags.TerminalWidth = int(width)
@@ -82,6 +77,22 @@ func (term *Terminal) TerminalWidth() (int, error) {
8277
return term.CmdFlags.TerminalWidth, err
8378
}
8479

80+
func resolveTerminalWidth(width uint, terminalErr error, columns string) (uint, error) {
81+
if width != 0 {
82+
return width, terminalErr
83+
}
84+
85+
columnWidth, err := strconv.Atoi(columns)
86+
if err != nil {
87+
return 0, errors.Join(terminalErr, err)
88+
}
89+
if columnWidth <= 0 {
90+
return 0, errors.Join(terminalErr, errors.New("terminal width must be greater than zero"))
91+
}
92+
93+
return uint(columnWidth), nil
94+
}
95+
8596
func (term *Terminal) Platform() string {
8697
const key = "environment_platform"
8798
if val, found := cache.Get[string](cache.Device, key); found {

src/runtime/terminal_unix_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package runtime
44

55
import (
6+
"errors"
67
"testing"
78

89
"github.com/stretchr/testify/assert"
@@ -64,3 +65,60 @@ func TestMemoryPercentageCalculation(t *testing.T) {
6465
})
6566
}
6667
}
68+
69+
func TestResolveTerminalWidth(t *testing.T) {
70+
terminalErr := errors.New("not a tty")
71+
cases := []struct {
72+
TerminalError error
73+
Name string
74+
Columns string
75+
TerminalWidth uint
76+
ExpectedWidth uint
77+
ExpectedToError bool
78+
}{
79+
{
80+
Name: "terminal width is available",
81+
Columns: "120",
82+
TerminalWidth: 80,
83+
ExpectedWidth: 80,
84+
},
85+
{
86+
Name: "COLUMNS replaces terminal error",
87+
Columns: "120",
88+
TerminalError: terminalErr,
89+
ExpectedWidth: 120,
90+
},
91+
{
92+
Name: "terminal and COLUMNS both fail",
93+
Columns: "invalid",
94+
TerminalError: terminalErr,
95+
ExpectedToError: true,
96+
},
97+
{
98+
Name: "zero COLUMNS is invalid",
99+
Columns: "0",
100+
TerminalError: terminalErr,
101+
ExpectedToError: true,
102+
},
103+
{
104+
Name: "negative COLUMNS is invalid",
105+
Columns: "-1",
106+
TerminalError: terminalErr,
107+
ExpectedToError: true,
108+
},
109+
}
110+
111+
for _, tc := range cases {
112+
t.Run(tc.Name, func(t *testing.T) {
113+
width, err := resolveTerminalWidth(tc.TerminalWidth, tc.TerminalError, tc.Columns)
114+
115+
assert.Equal(t, tc.ExpectedWidth, width)
116+
if tc.ExpectedToError {
117+
assert.Error(t, err)
118+
assert.ErrorIs(t, err, terminalErr)
119+
return
120+
}
121+
assert.NoError(t, err)
122+
})
123+
}
124+
}

0 commit comments

Comments
 (0)