Show the per-model usage limit, and size the status line by measurement
/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.
Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.
Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.
Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.
Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.
Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.
Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:35 +01:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Tests for claude/statusline.isaacaudet.sh — the Isaac Audet-inspired Claude
|
|
|
|
|
# Code status line (the variant symlinked from ~/.claude/statusline.sh).
|
|
|
|
|
#
|
|
|
|
|
# Sibling variants in claude/ (statusline.burnrate.sh, statusline.original.sh)
|
|
|
|
|
# are NOT covered by these tests.
|
|
|
|
|
#
|
|
|
|
|
# Width invariants.
|
|
|
|
|
#
|
|
|
|
|
# Expressed in USABLE width (what the renderer actually has: COLUMNS minus
|
|
|
|
|
# padding on both sides minus a margin), since that is what the script wraps on.
|
|
|
|
|
# Asserting invariants rather than fixed line counts keeps these from rotting
|
|
|
|
|
# every time a segment's content changes.
|
|
|
|
|
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh"
|
|
|
|
|
PAD=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null || echo 0)
|
|
|
|
|
OVERHEAD=$(( 2 * PAD + 1 ))
|
Wrap the status line rather than drop the per-model limit
The measured ladder tried every single-line variant before considering a
second line, so whenever line one plus the full group did not fit — from
around 92 usable columns upwards, depending on how long the branch, cwd and
model names are — it emitted rl_bare (5h and 7d only) and silently dropped
the per-model (Fable) bar that is the main reason the group is worth
rendering. On a 110-column laptop the limit was invisible.
Reorder so a second line beats losing that bar: one line rich/mid/lean, then
wrap, and rl_bare only when WRAP_NARROW is false. Reset times and
extra-usage credits are still given up rather than wrapped for, which makes
the rendered content non-monotone in width; the comment on the ladder spells
that out.
Also stop the tests writing fixtures to the caches the live status line
reads. ~/.claude/statusline.sh is a symlink to the script, so a test run was
visibly rendering fabricated usage — a Fable bar at 10%, extra-usage credits
of $1234.56/$2000.00 — in whatever session happened to be open, and a run
killed before its trap fired would have left that in place for an hour. The
cache directory is now $STATUSLINE_CACHE_DIR (default /tmp/claude) and each
suite points it at a temporary directory, which also removes the
backup/restore dance and the deletion of the live git-status cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:28:55 +01:00
|
|
|
# Throwaway cache directory, so fixtures never reach the live status line's
|
|
|
|
|
# cache (~/.claude/statusline.sh is a symlink to the script under test).
|
|
|
|
|
export STATUSLINE_CACHE_DIR=$(mktemp -d)
|
|
|
|
|
CACHE="$STATUSLINE_CACHE_DIR/statusline-usage-cache.json"
|
Show the per-model usage limit, and size the status line by measurement
/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.
Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.
Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.
Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.
Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.
Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.
Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:35 +01:00
|
|
|
REPO=$(mktemp -d)
|
Wrap the status line rather than drop the per-model limit
The measured ladder tried every single-line variant before considering a
second line, so whenever line one plus the full group did not fit — from
around 92 usable columns upwards, depending on how long the branch, cwd and
model names are — it emitted rl_bare (5h and 7d only) and silently dropped
the per-model (Fable) bar that is the main reason the group is worth
rendering. On a 110-column laptop the limit was invisible.
Reorder so a second line beats losing that bar: one line rich/mid/lean, then
wrap, and rl_bare only when WRAP_NARROW is false. Reset times and
extra-usage credits are still given up rather than wrapped for, which makes
the rendered content non-monotone in width; the comment on the ladder spells
that out.
Also stop the tests writing fixtures to the caches the live status line
reads. ~/.claude/statusline.sh is a symlink to the script, so a test run was
visibly rendering fabricated usage — a Fable bar at 10%, extra-usage credits
of $1234.56/$2000.00 — in whatever session happened to be open, and a run
killed before its trap fired would have left that in place for an hour. The
cache directory is now $STATUSLINE_CACHE_DIR (default /tmp/claude) and each
suite points it at a temporary directory, which also removes the
backup/restore dance and the deletion of the live git-status cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:28:55 +01:00
|
|
|
cleanup() { rm -rf "$STATUSLINE_CACHE_DIR" "$REPO"; }
|
Show the per-model usage limit, and size the status line by measurement
/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.
Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.
Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.
Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.
Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.
Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.
Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:35 +01:00
|
|
|
trap cleanup EXIT
|
|
|
|
|
|
|
|
|
|
cat > "$CACHE" <<'JSON'
|
|
|
|
|
{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},
|
|
|
|
|
"seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"},
|
|
|
|
|
"extra_usage":{"is_enabled":false},
|
|
|
|
|
"limits":[{"kind":"weekly_scoped","group":"weekly","percent":10,"resets_at":"2026-08-28T02:00:00+00:00","scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
|
|
|
|
|
JSON
|
|
|
|
|
git -C "$REPO" init -q 2>/dev/null
|
|
|
|
|
git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init 2>/dev/null
|
|
|
|
|
|
|
|
|
|
widths() { python3 -c "
|
|
|
|
|
import sys,re,unicodedata
|
|
|
|
|
for ln in sys.stdin.read().rstrip('\n').split('\n'):
|
|
|
|
|
p=re.sub(r'\033\[[0-9;]*m','',ln)
|
|
|
|
|
print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in p))"; }
|
|
|
|
|
|
|
|
|
|
pass=0; fail=0
|
|
|
|
|
check() { # usable, branch, model, cost, [xfail-reason]
|
|
|
|
|
local u="$1" branch="$2" model="$3" cost="$4" xfail="${5:-}"
|
|
|
|
|
local w=$(( u + OVERHEAD ))
|
Wrap the status line rather than drop the per-model limit
The measured ladder tried every single-line variant before considering a
second line, so whenever line one plus the full group did not fit — from
around 92 usable columns upwards, depending on how long the branch, cwd and
model names are — it emitted rl_bare (5h and 7d only) and silently dropped
the per-model (Fable) bar that is the main reason the group is worth
rendering. On a 110-column laptop the limit was invisible.
Reorder so a second line beats losing that bar: one line rich/mid/lean, then
wrap, and rl_bare only when WRAP_NARROW is false. Reset times and
extra-usage credits are still given up rather than wrapped for, which makes
the rendered content non-monotone in width; the comment on the ladder spells
that out.
Also stop the tests writing fixtures to the caches the live status line
reads. ~/.claude/statusline.sh is a symlink to the script, so a test run was
visibly rendering fabricated usage — a Fable bar at 10%, extra-usage credits
of $1234.56/$2000.00 — in whatever session happened to be open, and a run
killed before its trap fired would have left that in place for an hour. The
cache directory is now $STATUSLINE_CACHE_DIR (default /tmp/claude) and each
suite points it at a temporary directory, which also removes the
backup/restore dance and the deletion of the live git-status cache.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 17:28:55 +01:00
|
|
|
git -C "$REPO" checkout -q -B "$branch" 2>/dev/null; rm -f "$STATUSLINE_CACHE_DIR"/git-*
|
Show the per-model usage limit, and size the status line by measurement
/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.
Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.
Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.
Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.
Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.
Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.
Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:35 +01:00
|
|
|
local stdin="{\"model\":{\"display_name\":\"$model\"},\"cwd\":\"$REPO\",\"cost\":{\"total_cost_usd\":$cost},\"context_window\":{\"context_window_size\":200000,\"current_usage\":{\"input_tokens\":45000,\"cache_read_input_tokens\":30000}}}"
|
|
|
|
|
local out ws n mx=0 over=0
|
|
|
|
|
out=$(TERM_WIDTH=$w bash "$SCRIPT" <<<"$stdin")
|
|
|
|
|
ws=$(printf '%s' "$out" | widths)
|
|
|
|
|
n=$(printf '%s\n' "$ws" | wc -l | tr -d ' ')
|
|
|
|
|
while read -r c; do [ "$c" -gt "$u" ] && over=1; [ "$c" -gt "$mx" ] && mx=$c; done <<< "$ws"
|
|
|
|
|
|
|
|
|
|
local why=""
|
|
|
|
|
# INVARIANT 1: never exceed the usable width
|
|
|
|
|
[ "$over" = "1" ] && why="line exceeds usable width"
|
|
|
|
|
# INVARIANT 2: at most two lines
|
|
|
|
|
[ "$n" -gt 2 ] && why="${why:-more than two lines}"
|
|
|
|
|
# INVARIANT 3: only wrap when it was actually necessary
|
|
|
|
|
if [ -z "$why" ] && [ "$n" = "2" ]; then
|
|
|
|
|
local l1 l2; l1=$(printf '%s\n' "$ws" | sed -n 1p); l2=$(printf '%s\n' "$ws" | sed -n 2p)
|
|
|
|
|
[ $(( l1 + 3 + l2 )) -le "$u" ] && why="wrapped unnecessarily (would have fit on one line)"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
local label="usable=$u branch=${#branch}ch model='${model:0:12}' lines=$n max=$mx"
|
|
|
|
|
if [ -n "$xfail" ] && [ -n "$why" ]; then
|
|
|
|
|
echo " XFAIL $label -- $why (known, pre-existing: $xfail)"; pass=$((pass+1)); return
|
|
|
|
|
fi
|
|
|
|
|
if [ -z "$why" ]; then echo " PASS $label"; pass=$((pass+1))
|
|
|
|
|
else echo " FAIL $label -- $why"
|
|
|
|
|
printf '%s\n' "$out" | sed $'s/\033\[[0-9;]*m//g' | sed 's/^/ /'; fail=$((fail+1)); fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SHORT=main
|
|
|
|
|
LONG=feature/some-really-long-branch-name-here
|
|
|
|
|
M1="Opus 5"
|
|
|
|
|
M2="Opus 5 (1M context)"
|
|
|
|
|
|
|
|
|
|
echo "Sweep of usable widths, short branch:"
|
|
|
|
|
for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 40; do check "$u" "$SHORT" "$M1" 0.50; done
|
|
|
|
|
# Narrow tier has a ~35-col floor; nothing can fit below that. Verified byte-identical
|
|
|
|
|
# on the pre-change script, so not a regression.
|
|
|
|
|
check 25 "$SHORT" "$M1" 0.50 "narrow tier floor ~35 cols"
|
|
|
|
|
echo "Sweep with the real long model name:"
|
|
|
|
|
for u in 200 150 116 100 85 68 40; do check "$u" "$SHORT" "$M2" 4.61; done
|
|
|
|
|
echo "Sweep with a long branch name:"
|
|
|
|
|
for u in 200 150 116 100 85 68; do check "$u" "$LONG" "$M2" 4.61; done
|
|
|
|
|
check 40 "$LONG" "$M2" 4.61 "narrow tier floor ~43 cols with a long branch"
|
|
|
|
|
echo "Large cost figure:"
|
|
|
|
|
for u in 116 85 68; do check "$u" "$SHORT" "$M1" 1234.56; done
|
Measure line one's branch budget instead of assuming 56 columns
In wrap mode the branch is truncated to whatever the rest of line one leaves,
and "the rest" was a flat 56 columns. It is not flat: the token counts and the
cost figure vary with the session, and with a four-digit cost the real width is
57, so line one came out exactly one column over and the renderer clipped it.
Reachable at COLUMNS 73-81 with padding 2 — a narrow split pane.
Add up the segments that follow the branch instead, from the same values that
render them, and count the ahead/behind markers too, which the constant also
ignored. That needs the cost formatted and the token bar width chosen before
the git segment is built, so both move up; neither depends on anything in
between.
The budget is now exact rather than approximate, so the branch also gets the
columns the old constant was over-reserving when the cost was short.
Tests: the large-cost sweep only used the short branch 'main', where the
overflow cannot show. Repeat it with the long branch, which fails on the old
budget at usable 68, 69, 70 and 75 by exactly one column.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 19:01:24 +01:00
|
|
|
# ... and with a long branch, which is what makes the wrap-mode branch budget
|
|
|
|
|
# bite: a four-digit cost is three columns wider than the budget assumed.
|
|
|
|
|
echo "Large cost figure with a long branch:"
|
|
|
|
|
for u in 68 69 70 75 85; do check "$u" "$LONG" "$M1" 1234.56; done
|
Show the per-model usage limit, and size the status line by measurement
/usage reports a weekly limit scoped to a single model (Fable) that the
status line did not surface. It is absent from both the status line's stdin
JSON and the legacy seven_day_opus/seven_day_sonnet fields, which are always
null now; it lives in the usage API's .limits[] under kind "weekly_scoped".
Render it labelled by scope.model.display_name so it follows whichever model
the limit applies to, inside the 7d segment since both are weekly limits
sharing one reset time.
Fitting it exposed a problem with sizing by width tier. Tiers only know the
terminal width, so content that varies with session state — branch name, cwd,
model display name — could push a line past the edge and be clipped by the
renderer. Measure the assembled line instead (vis_len) and emit the richest of
four rate-limit variants that fits, on one line or two: with reset times, with
extra-usage credits, bars only, or bars without the per-model segment. Every
branch is fit-checked, including with wrapping disabled. Two lines render
correctly in the status line.
Cap the cwd basename as well: nothing else shortened line one, so a long
project directory could overflow it on its own.
Correct the usable width. Claude Code exports COLUMNS but applies the `padding`
setting on top of it rather than deducting it first, so a line sized to COLUMNS
is clipped; deduct padding on both sides plus a column of margin.
Sanitise payload data before rendering. printf %b interprets backslash escapes,
which is how the colour variables work, so a cwd or model name containing a
literal \n — or a real newline, which jq decodes from the JSON — would split
the line and break both the width measurement and the two-line guarantee.
Parse the usage payload in one jq pass rather than ten. The status line runs on
every redraw and per-field parsing had become the dominant cost; this brings a
render back to roughly what it was before (~155ms vs ~115ms parent), the
remainder being the reset times the tiered version did not show at this width.
Fields are read one per line rather than via @tsv: tab is IFS whitespace, so
`read` collapses runs of it and an empty field — no scoped limit, which is the
common case — silently shifted every later field along by one.
Tests cover the API payload shapes including malformed and hostile input, the
width invariants (never exceed the usable width, never more than two lines,
never wrap unnecessarily), and a sweep over widths, branch lengths, model
names, cwd lengths and extra-usage. They restore the live usage cache on exit,
and remove the fixture outright when there was no cache to restore — otherwise
a test run would leave fabricated usage figures live for an hour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:55:35 +01:00
|
|
|
|
|
|
|
|
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]
|