fix(runtime): honor COLUMNS fallback without a TTY

This commit is contained in:
Evgeny Kulikov
2026-08-04 08:41:23 +02:00
committed by Jan De Dobbeleer
parent 413f96fb12
commit 102edf0153
3 changed files with 86 additions and 10 deletions
@@ -28,3 +28,10 @@
- On Windows there is no SIGPIPE; child lifecycle management must rely on fd closure / stdin EOF.
- On native Linux, process spawns cost 11-16ms - daemon architectures that pay off on Windows can
be a wash there (see the bash serve revert in [bash](bash.md)).
## Statusline width detection
- On Unix, `terminal-dimensions` runs `stty size` against the renderer's stdin. Claude Code sends
statusline JSON over that stdin, so it is not a TTY. A valid `COLUMNS` fallback must replace the
resulting `stty` error; returning a non-zero width with the stale error makes
`prompt.Engine.canWriteRightBlock` reject ordinary right-aligned blocks (verified 2026-08-03).
+21 -10
View File
@@ -3,6 +3,7 @@
package runtime
import (
"errors"
"strconv"
"strings"
"time"
@@ -62,18 +63,12 @@ func (term *Terminal) TerminalWidth() (int, error) {
}
width, err := terminal.Width()
if err != nil {
log.Error(err)
if width == 0 {
width, err = resolveTerminalWidth(width, err, term.Getenv("COLUMNS"))
}
// fetch width from the environment variable
// in case the terminal width is not available
if width == 0 {
i, err := strconv.Atoi(term.Getenv("COLUMNS"))
if err != nil {
log.Error(err)
}
width = uint(i)
if err != nil {
log.Error(err)
}
term.CmdFlags.TerminalWidth = int(width)
@@ -82,6 +77,22 @@ func (term *Terminal) TerminalWidth() (int, error) {
return term.CmdFlags.TerminalWidth, err
}
func resolveTerminalWidth(width uint, terminalErr error, columns string) (uint, error) {
if width != 0 {
return width, terminalErr
}
columnWidth, err := strconv.Atoi(columns)
if err != nil {
return 0, errors.Join(terminalErr, err)
}
if columnWidth <= 0 {
return 0, errors.Join(terminalErr, errors.New("terminal width must be greater than zero"))
}
return uint(columnWidth), nil
}
func (term *Terminal) Platform() string {
const key = "environment_platform"
if val, found := cache.Get[string](cache.Device, key); found {
+58
View File
@@ -3,6 +3,7 @@
package runtime
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
@@ -64,3 +65,60 @@ func TestMemoryPercentageCalculation(t *testing.T) {
})
}
}
func TestResolveTerminalWidth(t *testing.T) {
terminalErr := errors.New("not a tty")
cases := []struct {
TerminalError error
Name string
Columns string
TerminalWidth uint
ExpectedWidth uint
ExpectedToError bool
}{
{
Name: "terminal width is available",
Columns: "120",
TerminalWidth: 80,
ExpectedWidth: 80,
},
{
Name: "COLUMNS replaces terminal error",
Columns: "120",
TerminalError: terminalErr,
ExpectedWidth: 120,
},
{
Name: "terminal and COLUMNS both fail",
Columns: "invalid",
TerminalError: terminalErr,
ExpectedToError: true,
},
{
Name: "zero COLUMNS is invalid",
Columns: "0",
TerminalError: terminalErr,
ExpectedToError: true,
},
{
Name: "negative COLUMNS is invalid",
Columns: "-1",
TerminalError: terminalErr,
ExpectedToError: true,
},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
width, err := resolveTerminalWidth(tc.TerminalWidth, tc.TerminalError, tc.Columns)
assert.Equal(t, tc.ExpectedWidth, width)
if tc.ExpectedToError {
assert.Error(t, err)
assert.ErrorIs(t, err, terminalErr)
return
}
assert.NoError(t, err)
})
}
}