Skip to content

fix(statusline): budget lines by the host's real reserve, not an inherited 85% - #13

Merged
cdeust merged 2 commits into
mainfrom
fix/statusline-width-budget
Jul 26, 2026
Merged

fix(statusline): budget lines by the host's real reserve, not an inherited 85%#13
cdeust merged 2 commits into
mainfrom
fix/statusline-width-budget

Conversation

@cdeust

@cdeust cdeust commented Jul 26, 2026

Copy link
Copy Markdown
Owner

What

FIT_RATIO=85 held every status line to 85% of the terminal. The number came from the pictet-tech claude-statusline plugin — uninstalled earlier today, so the citation pointed at a file that no longer exists — and its claim (a line near the full width is truncated and costs the block its second row) had never been reproduced in this repo.

This replaces the guess with the host's actual behaviour, read from Claude Code 2.1.220's own renderer.

The reserve, derived

Source in the 2.1.220 bundle Effect
<Box width={columns} paddingLeft={2} paddingRight={compact?1:2}> 4 columns
<Box paddingX={statusLine.padding ?? 0}> 2 × padding
<Text wrap="truncate"> per line (TRS) each line truncated independently

The wider paddingRight branch is used: one column pessimistic trims a segment early, one column optimistic is cut by the host.

That third row falsifies the inherited claim outright — an over-wide line costs no other row.

Two contradictions this removes

  • Preset vs budget. l was selected from 90 columns up but could not render untrimmed below 104, so between those widths it was selected and then trimmed on every refresh. Selection now runs on the budget, and SIZE_L_MIN_BUDGET=89 is exactly l's widest measured line. At 95 columns with padding 1: budget 89, widest emitted line 89.
  • Probe order. $COLUMNS now precedes the controlling tty. The host sets it to the width it renders into; capturing a live payload showed the hook environment has no controlling terminal, so the tty rung answers nothing there, and the statusLine JSON carries no width field at all.

Net effect on a 200-column terminal: a line may use 196 columns instead of 170.

Also fixed (§14)

test_perf_heat_rgb_20run_avg was flaky (~4 runs in 10) and asserted nothing — it compared one sample of heat_rgb against the mean of twenty more of the same call. Separate commit.

Completion Ledger

Item Evidence
A1 happy paths 53 + 22 shell tests, 18 python tests pass
A2 edge cases fit_budget: zero/negative/non-numeric width and padding, width below chrome, omitted padding — test_fit_budget_validates_both_inputs, test_fit_budget_floor_is_positive
A3 failure paths every read_statusline_padding fallback arm asserted: absent file, absent key, malformed JSON, string/float/negative/null/bool/array — test_read_statusline_padding_defaults_to_zero, ..._rejects_non_integers
A4 boundary validation both fit_budget inputs validated (probe fallback, user-edited JSON)
A5 invariants fit_budget postcondition "positive integer, always"; floor asserted usable by fit_line
C1 scalability no new loops; two extra jq reads per refresh, same as existing config readers
E1/E3 compatibility comment-level rename SIZE_L_MIN_COLSSIZE_L_MIN_BUDGET; no persisted format
E2 consumers measure_widths.sh (cap formula), statusline-command.sh, both READMEs updated
G1 path ledger every new branch mapped to a named test
G3 determinism 15 consecutive runs of each suite, 0 failures
G5 gates shellcheck 0.11.0 clean on renderer, modules and suites
H4 docs CHANGELOG 2.1.1, README.md + README.fr.md, versions bumped
H7 boy-scout flaky perf test fixed, not deferred
Overflow invariant no rendered line exceeds its budget at 40/60/80/95/120/200/241 columns

Not asserted, stated plainly: the tty rung's success path has no test. Stubbing stty cannot reach it — the < /dev/tty redirection fails first in a test process — and only a pty harness could. Its failure path is pinned. This is unchanged by this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4

cdeust and others added 2 commits July 26, 2026 16:28
…stop it flaking

test_perf_heat_rgb_20run_avg failed roughly 4 runs in 10 on an idle machine.
Two defects, one cause each.

It asserted nothing. The "baseline" was a single timing of heat_rgb and the
comparand was the mean of twenty more timings of the same call, so both sides
contained identical work and the delta was noise around zero. With n=1 on the
baseline, one slow first sample failed the assertion outright.

The instrument dwarfed the phenomenon. Timing an EMPTY pair of `date +%s%N`
calls on this host returned 4.2, 21.4, 21.7, 21.8 and 23.5 ms across five
consecutive attempts — a ~19 ms spread against a 5 ms budget. Per-sample
timestamps cannot support that assertion at any sample count the suite can
afford.

Now: one timestamp pair around a block of 2000 calls, and the same around an
equally-sized no-op control carrying the same redirection. The fork is
amortized (~0.01 ms per call) and the control subtracts everything that is not
heat_rgb. Measured on this host: heat_rgb 173 us/call, control 41 us/call — a
0.13 ms delta against the unchanged 5 ms budget (AC-015/NFR-002).

Verified: 15 consecutive runs, 0 failures (was 4/15 failing before, and 4/10
with an interim per-sample fix that corrected only the control).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
…rited 85%

FIT_RATIO=85 held every line to 85% of the terminal. The figure came from the
pictet-tech claude-statusline plugin, which is no longer installed, and its
claim — a line near the full width is truncated AND costs the block its second
row — was never reproduced here. The previous commit could only document that;
this one replaces it with the host's actual behaviour, read from Claude Code
2.1.220 itself.

The reserve is additive, and it is now taken from the renderer:
  <Box width={columns} paddingLeft={2} paddingRight={compact?1:2}>   -> 4 cols
  <Box paddingX={statusLine.padding ?? 0}>                           -> 2*padding
The wider of the paddingRight branches is used: one column pessimistic trims a
segment early, one column optimistic is cut by the host. statusLine.padding is
read from settings.json (read_statusline_padding), type-strict in jq because a
JSON string "2" is not a number to the host.

The same read falsifies the inherited claim: every line is rendered
<Text wrap="truncate"> individually, so an over-wide line is truncated on its
own and costs no other row. Line count is independent of line width.

Preset selection moves onto the budget rather than the raw width, which removes
a contradiction between two constants: l was selected from 90 columns up but
could not render untrimmed below 104, so between those widths it was selected
and then trimmed on every refresh. SIZE_L_MIN_COLS=90 becomes
SIZE_L_MIN_BUDGET=89 — exactly l's widest measured line, so "budget >= widest"
is the fit condition and the two cannot disagree. Verified: at 95 columns with
padding 1, budget 89 and widest emitted line 89.

probe_cols consults $COLUMNS before the controlling tty. The host sets COLUMNS
to the width it renders into (`let{columns:N}=process.stdout;if(N)L.COLUMNS=
String(N)`), which is the authority; the tty probe stays below it for hand-run
invocations. Capturing a live payload on 2026-07-26 showed the hook environment
has no controlling terminal at all, so that rung answers nothing under the
host, and that the statusLine JSON carries no width field of any kind.

Net effect on a 200-column terminal: a line may use 196 columns instead of 170.

Verified: shellcheck 0.11.0 clean on renderer, modules and suites; 53 + 22 shell
tests and 18 python tests pass; no rendered line exceeds its budget at 40, 60,
80, 95, 120, 200 or 241 columns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SQwMscnDJfTN7sz3Jwzg4
@cdeust
cdeust merged commit a9a9be5 into main Jul 26, 2026
1 check passed
@cdeust
cdeust deleted the fix/statusline-width-budget branch July 26, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant