2026-08-08 11:54:05 +01:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
# Claude Code Status Line
|
|
|
|
|
|
# Based on https://github.com/daniel3303/ClaudeCodeStatusLine
|
|
|
|
|
|
# Enhanced: git branch/ahead-behind, token bar, caching, improved visuals
|
|
|
|
|
|
|
|
|
|
|
|
set -f # disable globbing
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Config =====
|
|
|
|
|
|
SHOW_GIT=true # git branch, dirty status, ahead/behind
|
|
|
|
|
|
SHOW_TOKENS=true # token usage bar
|
|
|
|
|
|
SHOW_THINKING=true # extended thinking indicator
|
|
|
|
|
|
SHOW_RATE_LIMITS=true # 5h / 7d rate limit bars
|
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
|
|
|
|
SHOW_MODEL_LIMIT=true # per-model weekly limit bar (e.g. Fable), when the API reports one
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
RL_MIN_WIDTH=35 # narrowest usable width the 5h + 7d bars fit on their own
|
|
|
|
|
|
# line: two of "5h ██████ 100%" plus a separator, +4
|
|
|
|
|
|
COMPACT_WIDTH=100 # below this, shorten line one to what it has room for
|
2026-08-08 11:54:05 +01:00
|
|
|
|
BRANCH_MAX_LEN=28 # truncate branch names longer than this
|
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
|
|
|
|
CWD_MAX_LEN=20 # truncate the cwd basename longer than this
|
2026-08-08 11:54:05 +01:00
|
|
|
|
GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large repos)
|
2026-09-02 13:01:12 +01:00
|
|
|
|
USAGE_CACHE_SECS=300 # seconds to cache the usage API response (the 5h/7d bars)
|
Back off after a failed usage fetch instead of retrying every redraw
A failed fetch left the cache mtime untouched, so with no network every
redraw — and a redraw happens on every keystroke — tried curl again and
could wait --max-time 10 for it. Cutting the TTL from an hour to five
minutes made that start 55 minutes sooner, so it is worth fixing now.
The marker is a separate file rather than a touch of the cached
response: a cold /tmp has no response to touch, and that is exactly the
case with no stale data to fall back on, where the timeout is paid in
full with nothing to show for it.
It goes down before the request and is cleared when that lands, not
written afterwards. A fetch is in flight for as long as curl takes to
give up, and this cache dir is shared by every session, so redraws
starting inside that window are real rather than hypothetical: they now
serve stale data instead of each launching their own doomed request.
Every attempt writes it, so a repeated failure restarts the backoff, and
only an attempt does, so the redraws it suppresses cannot keep
re-stamping it and it always lapses.
Missing credentials stays outside the backoff — it costs no timeout, and
the redraw after a login should show the bars rather than wait out a
retry window it had no part in earning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:12:45 +01:00
|
|
|
|
USAGE_RETRY_SECS=60 # seconds to wait before retrying a failed usage API fetch
|
2026-08-08 11:54:05 +01:00
|
|
|
|
TOKEN_BAR_WIDTH=8 # width of token progress bar
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
SL_WORK_DAYS="${SL_WORK_DAYS:-12345}" # ISO weekdays you work: Mon=1 ... Sun=7
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
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
|
|
|
|
# Where the git and usage-API caches live. Overridable via the environment so a
|
|
|
|
|
|
# test run can render into its own directory: ~/.claude/statusline.sh is a
|
|
|
|
|
|
# symlink to this script, so a fixture written to the live cache is picked up by
|
2026-09-02 13:01:12 +01:00
|
|
|
|
# the next redraw and shown as real usage until USAGE_CACHE_SECS is up.
|
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
|
|
|
|
CACHE_DIR="${STATUSLINE_CACHE_DIR:-/tmp/claude}"
|
|
|
|
|
|
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
# "Now", overridable so a test can pin the day of the week: the pace figure
|
|
|
|
|
|
# counts the working days left before the reset, so it moves with today's
|
|
|
|
|
|
# weekday and would otherwise be unassertable. Only the pace code reads it --
|
|
|
|
|
|
# the cache-age checks below stay on the real clock deliberately, so pinning a
|
|
|
|
|
|
# time cannot make a fresh fixture look stale and send a test to the network.
|
|
|
|
|
|
SL_NOW="${SL_NOW:-}"
|
|
|
|
|
|
now_ts() { [ -n "$SL_NOW" ] && printf '%s' "$SL_NOW" || date +%s; }
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# Terminal width detection.
|
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
|
|
|
|
# Claude Code exports COLUMNS for this subprocess. There is no controlling
|
|
|
|
|
|
# terminal, so the stty fallback fails; when both fail we default to 80
|
|
|
|
|
|
# (safe/compact) rather than wide.
|
2026-08-08 11:54:05 +01:00
|
|
|
|
#
|
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
|
|
|
|
# The `padding` setting indents the status line on both sides, and Claude Code
|
|
|
|
|
|
# applies it on top of COLUMNS rather than deducting it first — measured: with
|
|
|
|
|
|
# COLUMNS=121 and padding 2, a 121-column line is clipped with an ellipsis. So
|
|
|
|
|
|
# deduct it here, plus a column of margin.
|
|
|
|
|
|
#
|
|
|
|
|
|
# To override detection entirely, set TERM_WIDTH in settings.json:
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# "command": "TERM_WIDTH=160 ~/.claude/statusline.sh"
|
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
|
|
|
|
# Only the user-level file: Claude Code also merges project .claude/settings.json,
|
|
|
|
|
|
# .claude/settings.local.json and managed policy, so a statusLine.padding set at
|
|
|
|
|
|
# project level would be applied by the renderer but missed here.
|
|
|
|
|
|
sl_padding=0
|
|
|
|
|
|
if [ -f "$HOME/.claude/settings.json" ]; then
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
sl_padding=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null)
|
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
|
|
|
|
[ "${sl_padding:-0}" -ge 0 ] 2>/dev/null || sl_padding=0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
if [ "${TERM_WIDTH:-0}" -le 0 ] 2>/dev/null; then
|
|
|
|
|
|
if [ "${COLUMNS:-0}" -gt 0 ] 2>/dev/null; then
|
|
|
|
|
|
TERM_WIDTH=$COLUMNS
|
|
|
|
|
|
else
|
|
|
|
|
|
_w=$(stty size </dev/tty 2>/dev/null | awk '{print $2}')
|
|
|
|
|
|
[ "${_w:-0}" -gt 0 ] 2>/dev/null && TERM_WIDTH=$_w || TERM_WIDTH=80
|
|
|
|
|
|
unset _w
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
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
|
|
|
|
# Usable width: minus padding on both sides, minus a column of margin.
|
|
|
|
|
|
USABLE_WIDTH=$(( TERM_WIDTH - 2 * sl_padding - 1 ))
|
|
|
|
|
|
[ "$USABLE_WIDTH" -lt 20 ] && USABLE_WIDTH=20
|
|
|
|
|
|
|
|
|
|
|
|
# ${#str} counts characters only under a UTF-8 locale; under LC_ALL=C it counts
|
|
|
|
|
|
# bytes, which would mis-measure the box/block glyphs and break line wrapping.
|
|
|
|
|
|
case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in
|
|
|
|
|
|
*UTF-8*|*utf8*|*UTF8*) ;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
# C.UTF-8 on glibc, en_US.UTF-8 on macOS (which has no C.UTF-8).
|
|
|
|
|
|
# Picking one that does not exist makes bash warn on every render and
|
|
|
|
|
|
# silently fall back to byte counting, so verify before committing.
|
|
|
|
|
|
for _loc in C.UTF-8 en_US.UTF-8; do
|
|
|
|
|
|
if LC_ALL="$_loc" locale charmap 2>/dev/null | grep -qi utf; then
|
|
|
|
|
|
export LC_ALL="$_loc"; break
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
unset _loc
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
input=$(cat)
|
|
|
|
|
|
[ -z "$input" ] && printf "Claude" && exit 0
|
|
|
|
|
|
|
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
|
|
|
|
mkdir -p "$CACHE_DIR"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
# ===== Colors =====
|
Make the Claude Code statusline follow the terminal palette
The nine colours were hardcoded One Dark RGB via 38;2;R;G;B, so they did not
follow the light/dark switch. Measured against the tangere backgrounds they are
fine on dark (4.25-10.80:1) but collapse on light: orange 1.78, amber 1.70,
yellow 1.63, and 'white' — used for the token total — at 1.35:1, effectively
invisible.
Switched to palette indices, which resolve through the terminal's own palette and
so follow the ghostty tangere-light/tangere-dark swap with no detection and no
cost per render. 'white' becomes 39, default foreground, since it means primary
text. All seven colours actually used get distinct indices; blue and yellow are
unreferenced but converted too, so no RGB is left behind.
Contrast now clears WCAG AA in both modes: 5.77-14.26:1 on dark, 6.32-20.61:1 on
light.
Trade-off: tangere has no orange and its palette 2 is teal rather than green, so
'green' reads teal and the warm colours sit closer together than the One Dark
originals did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:02:01 +01:00
|
|
|
|
# Palette indices rather than RGB, so these resolve through the terminal's own
|
|
|
|
|
|
# palette and follow the ghostty tangere-light/tangere-dark swap for free. No
|
|
|
|
|
|
# appearance detection, so no cost per statusline render.
|
|
|
|
|
|
#
|
|
|
|
|
|
# The previous values were One Dark RGB: fine on dark (4.3-10.8:1) but 1.35-3.4:1
|
|
|
|
|
|
# on a light background, with 'white' effectively invisible. These clear WCAG AA
|
|
|
|
|
|
# in both modes (5.8-14.3:1 on dark, 6.3-20.6:1 on light).
|
|
|
|
|
|
blue='\033[38;5;4m'
|
|
|
|
|
|
orange='\033[38;5;3m'
|
|
|
|
|
|
amber='\033[38;5;11m'
|
|
|
|
|
|
green='\033[38;5;2m'
|
|
|
|
|
|
cyan='\033[38;5;14m'
|
|
|
|
|
|
red='\033[38;5;1m'
|
|
|
|
|
|
yellow='\033[38;5;3m'
|
|
|
|
|
|
white='\033[39m'
|
|
|
|
|
|
magenta='\033[38;5;5m'
|
2026-08-08 11:54:05 +01:00
|
|
|
|
dim='\033[2m'
|
|
|
|
|
|
reset='\033[0m'
|
|
|
|
|
|
|
|
|
|
|
|
sep=" ${dim}│${reset} "
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Helpers =====
|
|
|
|
|
|
|
|
|
|
|
|
format_tokens() {
|
|
|
|
|
|
local num=$1
|
|
|
|
|
|
if [ "$num" -ge 1000000 ]; then
|
|
|
|
|
|
awk "BEGIN {printf \"%.1fm\", $num / 1000000}"
|
|
|
|
|
|
elif [ "$num" -ge 1000 ]; then
|
|
|
|
|
|
awk "BEGIN {printf \"%.0fk\", $num / 1000}"
|
|
|
|
|
|
else
|
|
|
|
|
|
printf "%d" "$num"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
# Display width of a string, ignoring ANSI colour escapes. Every glyph the
|
|
|
|
|
|
# statusline uses is single-width, so a character count is the display width.
|
|
|
|
|
|
#
|
|
|
|
|
|
# The colour vars hold the escapes as literal backslash-033 text (they are
|
|
|
|
|
|
# single-quoted, and printf %b only interprets them when the line is finally
|
|
|
|
|
|
# emitted), so strip that literal form as well as a real ESC byte.
|
|
|
|
|
|
vis_len() {
|
|
|
|
|
|
local plain
|
|
|
|
|
|
plain=$(printf '%s' "$1" | sed -e 's/\\033\[[0-9;]*m//g' -e $'s/\033\[[0-9;]*m//g' -e 's/\\\\/\\/g')
|
|
|
|
|
|
printf '%d' "${#plain}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# printf %b interprets backslash escapes -- that is how the colour variables
|
|
|
|
|
|
# above are applied. Data from the API or the filesystem must NOT be
|
|
|
|
|
|
# interpreted: a directory or model name containing a literal \n would split
|
|
|
|
|
|
# the line, breaking both the width measurement and the two-line guarantee.
|
|
|
|
|
|
# Double the backslashes so %b renders them literally.
|
|
|
|
|
|
esc_data() {
|
|
|
|
|
|
local v="$1"
|
|
|
|
|
|
# Real control characters (jq decodes \n in the JSON payload into an actual
|
|
|
|
|
|
# newline) would split the line regardless of escaping, so drop them first.
|
|
|
|
|
|
v="${v//$'\n'/ }"; v="${v//$'\r'/ }"; v="${v//$'\t'/ }"; v="${v//$'\033'/}"
|
|
|
|
|
|
printf '%s' "${v//\\/\\\\}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
truncate_str() {
|
|
|
|
|
|
local str="$1" max="$2"
|
|
|
|
|
|
[ "${#str}" -gt "$max" ] \
|
|
|
|
|
|
&& printf "%s…" "${str:0:$((max-1))}" \
|
|
|
|
|
|
|| printf "%s" "$str"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Colored progress bar using block chars
|
|
|
|
|
|
build_bar() {
|
|
|
|
|
|
local pct=$1 width=$2
|
|
|
|
|
|
[ "$pct" -lt 0 ] 2>/dev/null && pct=0
|
|
|
|
|
|
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
|
|
|
|
|
|
local filled=$(( pct * width / 100 ))
|
|
|
|
|
|
local empty=$(( width - filled ))
|
|
|
|
|
|
local bar_color
|
|
|
|
|
|
if [ "$pct" -ge 90 ]; then bar_color="$red"
|
|
|
|
|
|
elif [ "$pct" -ge 70 ]; then bar_color="$yellow"
|
|
|
|
|
|
elif [ "$pct" -ge 50 ]; then bar_color="$orange"
|
|
|
|
|
|
else bar_color="$green"
|
|
|
|
|
|
fi
|
|
|
|
|
|
local f="" e=""
|
|
|
|
|
|
for ((i=0; i<filled; i++)); do f+="█"; done
|
|
|
|
|
|
for ((i=0; i<empty; i++)); do e+="░"; done
|
|
|
|
|
|
printf "${bar_color}${f}${dim}${e}${reset}"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
# Colour for the sustainable %/day, measured against the window's OWN even-burn
|
|
|
|
|
|
# baseline (100 / workdays in the window) rather than a fixed number of points
|
|
|
|
|
|
# per day: on a five-day week healthy is 20%/day, so a threshold tuned to the
|
|
|
|
|
|
# 14.3%/day calendar baseline would still call 12%/day green -- by then two
|
|
|
|
|
|
# fifths of the budget has been overspent. The ratios reproduce the 12/8/5
|
|
|
|
|
|
# thresholds at a 14.3 baseline, whatever SL_WORK_DAYS is set to.
|
|
|
|
|
|
#
|
|
|
|
|
|
# A high pace means plenty of runway per working day, so it reads cool; a low
|
|
|
|
|
|
# one means the rest of the week has to be rationed. Both arguments are in
|
|
|
|
|
|
# tenths, which keeps the comparison integer.
|
|
|
|
|
|
pacecol() {
|
|
|
|
|
|
local p=$1 base=$2
|
|
|
|
|
|
if [ "$p" -ge $(( base * 84 / 100 )) ]; then printf '%s' "$green"
|
|
|
|
|
|
elif [ "$p" -ge $(( base * 56 / 100 )) ]; then printf '%s' "$yellow"
|
|
|
|
|
|
elif [ "$p" -ge $(( base * 35 / 100 )) ]; then printf '%s' "$orange"
|
|
|
|
|
|
else printf '%s' "$red"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# ===== Git info with per-directory caching =====
|
|
|
|
|
|
get_git_info() {
|
|
|
|
|
|
local dir="$1"
|
|
|
|
|
|
[ -z "$dir" ] && return
|
|
|
|
|
|
|
|
|
|
|
|
# Stable cache key per directory path
|
|
|
|
|
|
local dir_hash
|
|
|
|
|
|
dir_hash=$(printf '%s' "$dir" | cksum | awk '{print $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
|
|
|
|
local cache_file="$CACHE_DIR/git-${dir_hash}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
local needs_refresh=true
|
|
|
|
|
|
if [ -f "$cache_file" ]; then
|
|
|
|
|
|
local mtime now age
|
|
|
|
|
|
mtime=$(stat -f %m "$cache_file" 2>/dev/null || stat -c %Y "$cache_file" 2>/dev/null)
|
|
|
|
|
|
now=$(date +%s)
|
|
|
|
|
|
age=$(( now - mtime ))
|
|
|
|
|
|
[ "$age" -lt "$GIT_CACHE_SECS" ] && needs_refresh=false
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if $needs_refresh; then
|
|
|
|
|
|
# Branch name (or short hash for detached HEAD)
|
|
|
|
|
|
local branch
|
|
|
|
|
|
branch=$(git -C "$dir" symbolic-ref --short HEAD 2>/dev/null)
|
|
|
|
|
|
if [ -z "$branch" ]; then
|
|
|
|
|
|
branch=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null)
|
|
|
|
|
|
if [ -z "$branch" ]; then
|
|
|
|
|
|
: > "$cache_file" # not a git repo — write empty cache
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
branch="(${branch})"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Dirty: any staged or unstaged changes
|
|
|
|
|
|
local dirty=""
|
|
|
|
|
|
[ -n "$(git -C "$dir" status --porcelain 2>/dev/null)" ] && dirty="dirty"
|
|
|
|
|
|
|
|
|
|
|
|
# Ahead / behind upstream (skip if no upstream set)
|
|
|
|
|
|
local ahead=0 behind=0
|
|
|
|
|
|
local upstream
|
|
|
|
|
|
upstream=$(git -C "$dir" rev-parse --abbrev-ref "@{upstream}" 2>/dev/null)
|
|
|
|
|
|
if [ -n "$upstream" ]; then
|
|
|
|
|
|
local ab
|
|
|
|
|
|
ab=$(git -C "$dir" rev-list --left-right --count "HEAD...${upstream}" 2>/dev/null)
|
|
|
|
|
|
ahead=$(echo "$ab" | awk '{print $1}')
|
|
|
|
|
|
behind=$(echo "$ab" | awk '{print $2}')
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Tab-delimited cache (branch names can't contain tabs per git spec)
|
|
|
|
|
|
printf '%s\t%s\t%s\t%s' "$branch" "$dirty" "${ahead:-0}" "${behind:-0}" > "$cache_file"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
cat "$cache_file" 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ===== OAuth token resolution =====
|
|
|
|
|
|
get_oauth_token() {
|
|
|
|
|
|
[ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && echo "$CLAUDE_CODE_OAUTH_TOKEN" && return 0
|
|
|
|
|
|
|
|
|
|
|
|
if command -v security >/dev/null 2>&1; then
|
|
|
|
|
|
local blob token
|
|
|
|
|
|
blob=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
|
|
|
|
|
|
if [ -n "$blob" ]; then
|
|
|
|
|
|
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
|
|
|
|
|
[ -n "$token" ] && [ "$token" != "null" ] && echo "$token" && return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
local creds_file="${HOME}/.claude/.credentials.json"
|
|
|
|
|
|
if [ -f "$creds_file" ]; then
|
|
|
|
|
|
local token
|
|
|
|
|
|
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
|
|
|
|
|
|
[ -n "$token" ] && [ "$token" != "null" ] && echo "$token" && return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if command -v secret-tool >/dev/null 2>&1; then
|
|
|
|
|
|
local blob token
|
|
|
|
|
|
blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
|
|
|
|
|
|
if [ -n "$blob" ]; then
|
|
|
|
|
|
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
|
|
|
|
|
[ -n "$token" ] && [ "$token" != "null" ] && echo "$token" && return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ===== ISO 8601 → epoch (cross-platform) =====
|
|
|
|
|
|
iso_to_epoch() {
|
|
|
|
|
|
local iso_str="$1"
|
|
|
|
|
|
local epoch
|
|
|
|
|
|
|
|
|
|
|
|
epoch=$(date -d "${iso_str}" +%s 2>/dev/null)
|
|
|
|
|
|
[ -n "$epoch" ] && echo "$epoch" && return 0
|
|
|
|
|
|
|
|
|
|
|
|
local stripped="${iso_str%%.*}"
|
|
|
|
|
|
stripped="${stripped%%Z}"
|
|
|
|
|
|
stripped="${stripped%%+*}"
|
|
|
|
|
|
stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}"
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then
|
|
|
|
|
|
epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
|
|
|
|
|
else
|
|
|
|
|
|
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
[ -n "$epoch" ] && echo "$epoch" && return 0
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
# Local midnight of the day an epoch falls in, plus that day's ISO weekday
|
|
|
|
|
|
# (1 = Monday), as "<epoch> <weekday>". BSD date first -- it is the hot path
|
|
|
|
|
|
# here -- then GNU date, which has no -v.
|
|
|
|
|
|
day_start_dow() {
|
|
|
|
|
|
date -j -r "$1" -v0H -v0M -v0S +'%s %u' 2>/dev/null && return 0
|
|
|
|
|
|
local d
|
|
|
|
|
|
d=$(date -d "@$1" +%F 2>/dev/null) || return 1
|
|
|
|
|
|
date -d "$d 00:00:00" +'%s %u' 2>/dev/null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
format_reset_time() {
|
|
|
|
|
|
local iso_str="$1" style="$2"
|
|
|
|
|
|
[ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return
|
|
|
|
|
|
local epoch
|
|
|
|
|
|
epoch=$(iso_to_epoch "$iso_str")
|
|
|
|
|
|
[ -z "$epoch" ] && return
|
|
|
|
|
|
case "$style" in
|
|
|
|
|
|
time)
|
|
|
|
|
|
date -j -r "$epoch" +"%l:%M%p" 2>/dev/null | sed 's/^ //' | tr '[:upper:]' '[:lower:]' ||
|
|
|
|
|
|
date -d "@$epoch" +"%l:%M%P" 2>/dev/null | sed 's/^ //'
|
|
|
|
|
|
;;
|
|
|
|
|
|
datetime)
|
|
|
|
|
|
date -j -r "$epoch" +"%b %-d, %l:%M%p" 2>/dev/null | sed 's/ / /g; s/^ //' | tr '[:upper:]' '[:lower:]' ||
|
|
|
|
|
|
date -d "@$epoch" +"%b %-d, %l:%M%P" 2>/dev/null | sed 's/ / /g; s/^ //'
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Extract JSON =====
|
|
|
|
|
|
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
|
|
|
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
|
|
|
|
|
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Formatted here rather than where it is rendered: the compact-tier branch
|
|
|
|
|
|
# budget needs its width, and it is not fixed ("$4.61" vs "$1234.56").
|
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
|
|
|
|
cost_fmt=""
|
|
|
|
|
|
[ -n "$cost_usd" ] && cost_fmt=$(printf '%.2f' "$cost_usd" 2>/dev/null)
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
|
|
|
|
|
|
[ "$size" -eq 0 ] 2>/dev/null && size=200000
|
|
|
|
|
|
|
|
|
|
|
|
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
|
|
|
|
|
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
|
|
|
|
|
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
|
|
|
|
current=$(( input_tokens + cache_create + cache_read ))
|
|
|
|
|
|
|
|
|
|
|
|
used_tokens=$(format_tokens $current)
|
|
|
|
|
|
total_tokens=$(format_tokens $size)
|
|
|
|
|
|
pct_used=$(( size > 0 ? current * 100 / size : 0 ))
|
|
|
|
|
|
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
# Thinking and effort come from the payload, not from alwaysThinkingEnabled in
|
|
|
|
|
|
# settings.json: Option+T toggles thinking for the session only and never
|
|
|
|
|
|
# writes the file, so a settings read reports the wrong state for the rest of
|
|
|
|
|
|
# the session. Absent means enabled, mirroring Claude Code's own
|
|
|
|
|
|
# `thinking:{enabled: lt !== false}`.
|
|
|
|
|
|
# Both in one jq pass: the status line re-renders on every redraw, so a
|
|
|
|
|
|
# process per field is the dominant cost (see the rate-limit section below).
|
|
|
|
|
|
# `== false` rather than `// false`, which jq's falsy `//` would flip to true.
|
|
|
|
|
|
{ IFS= read -r sl_thinking; IFS= read -r effort; } <<EOF
|
|
|
|
|
|
$(echo "$input" | jq -r '(if .thinking.enabled == false then "false" else "true" end), (.effort.level // "")')
|
|
|
|
|
|
EOF
|
|
|
|
|
|
thinking_on=true
|
|
|
|
|
|
[ "$sl_thinking" = "false" ] && thinking_on=false
|
|
|
|
|
|
|
|
|
|
|
|
# The effort level replaces the static "thinking" label. Whitelisted rather
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# than printed as-is: an unrecognised value would escape the width budget the
|
|
|
|
|
|
# compact tier computes from this string.
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
case "$effort" in
|
|
|
|
|
|
low|medium|high|xhigh|max) thinking_label="$effort" ;;
|
|
|
|
|
|
*) thinking_label="thinking" ;;
|
|
|
|
|
|
esac
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
# ===== Adaptive width tiers =====
|
|
|
|
|
|
#
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# The rate-limit group always gets line two to itself, so tiers choose only how
|
|
|
|
|
|
# much of LINE ONE to show. What that group contains is decided by measurement
|
|
|
|
|
|
# at the end of the script, not here -- tiers cannot see how long the branch,
|
|
|
|
|
|
# cwd or model name is, which is how content used to end up clipped.
|
2026-08-08 11:54:05 +01:00
|
|
|
|
#
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
# full (≥150): CWD, ahead/behind, "◆ high" effort, cost
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# wide (68–149): the same without the CWD, and below COMPACT_WIDTH with a
|
|
|
|
|
|
# short model name and a branch budgeted to what is left
|
|
|
|
|
|
# narrow (<68): short model + branch + token bar only -- the usage group is
|
|
|
|
|
|
# unaffected, it has its own line at every width
|
2026-08-08 11:54:05 +01:00
|
|
|
|
#
|
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
|
|
|
|
if [ "$USABLE_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full"
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
elif [ "$USABLE_WIDTH" -ge 68 ] 2>/dev/null; then width_tier="wide"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
else width_tier="narrow"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Compact: the wide tier with a short model name and the branch cut to whatever
|
|
|
|
|
|
# line one has left. Between 68 and COMPACT_WIDTH columns the wide-tier
|
|
|
|
|
|
# segments do fit, but only once those two are budgeted rather than assumed.
|
|
|
|
|
|
compact=false
|
|
|
|
|
|
if [ "$width_tier" = "wide" ] && [ "$USABLE_WIDTH" -lt "$COMPACT_WIDTH" ] 2>/dev/null; then
|
|
|
|
|
|
compact=true
|
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
|
|
|
|
fi
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# Shorten model name for tight spaces
|
|
|
|
|
|
short_model() {
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
*Opus*) echo "Opus" ;;
|
|
|
|
|
|
*Sonnet*) echo "Sonnet" ;;
|
|
|
|
|
|
*Haiku*) echo "Haiku" ;;
|
|
|
|
|
|
*) echo "$1" | awk '{print $NF}' ;; # last word
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ===== Build output =====
|
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
|
|
|
|
# Explicitly empty: bash imports same-named environment variables, and a
|
|
|
|
|
|
# stray $rl_lean would otherwise leak into the rendered line.
|
2026-08-08 11:54:05 +01:00
|
|
|
|
out=""
|
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
|
|
|
|
rl_bare=""
|
|
|
|
|
|
rl_lean=""
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
rl_pace=""
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
rl_time=""
|
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
|
|
|
|
rl_rich=""
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
pace_len=""
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
# Model — color by family
|
|
|
|
|
|
model_color="$blue"
|
|
|
|
|
|
case "$model_name" in
|
|
|
|
|
|
*Opus*) model_color="$amber" ;;
|
|
|
|
|
|
*Haiku*) model_color="$cyan" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
display_model="$model_name"
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# A long display name ("Opus 5 (1M context)" is 19 cols) would overflow line
|
|
|
|
|
|
# one at these widths, so it gives up everything but the family.
|
|
|
|
|
|
if [ "$width_tier" = "narrow" ] || $compact; then
|
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
|
|
|
|
display_model=$(short_model "$model_name")
|
|
|
|
|
|
fi
|
|
|
|
|
|
out+="${model_color}$(esc_data "$display_model")${reset}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
# CWD — full tier only (branch name gives enough context below that)
|
|
|
|
|
|
if [ "$width_tier" = "full" ] && [ -n "$cwd" ]; then
|
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
|
|
|
|
# Capped like the branch name: nothing else shortens line one, so an
|
|
|
|
|
|
# unusually long project directory would push it past the terminal edge.
|
|
|
|
|
|
display_dir=$(truncate_str "${cwd##*/}" "$CWD_MAX_LEN")
|
|
|
|
|
|
out+="${sep}${dim}$(esc_data "$display_dir")${reset}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
|
|
|
|
|
|
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
|
|
|
|
bar_w="$TOKEN_BAR_WIDTH"
|
|
|
|
|
|
[ "$width_tier" = "wide" ] && bar_w=6
|
|
|
|
|
|
[ "$width_tier" = "narrow" ] && bar_w=4
|
|
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# Git branch + dirty + ahead/behind
|
|
|
|
|
|
if $SHOW_GIT && [ -n "$cwd" ]; then
|
|
|
|
|
|
git_info=$(get_git_info "$cwd")
|
|
|
|
|
|
if [ -n "$git_info" ]; then
|
|
|
|
|
|
IFS=$'\t' read -r g_branch g_dirty g_ahead g_behind <<< "$git_info"
|
|
|
|
|
|
|
|
|
|
|
|
# Progressively tighten branch truncation
|
|
|
|
|
|
local_max="$BRANCH_MAX_LEN"
|
|
|
|
|
|
[ "$width_tier" = "wide" ] && local_max=24
|
|
|
|
|
|
[ "$width_tier" = "narrow" ] && local_max=12
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# In compact mode line one is
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
# model │ ⎇ branch ✔ ↑1 │ <bar> used/total pct% │ ◇ high │ $cost
|
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 the branch gets whatever the rest of it leaves. Everything after
|
|
|
|
|
|
# the branch is appended below, so its width is added up here rather
|
|
|
|
|
|
# than assumed: a flat 56 columns was three short of a four-digit cost,
|
|
|
|
|
|
# which pushed line one past the edge to be clipped by the renderer.
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
if $compact; then
|
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
|
|
|
|
tail_len=$(( 5 + 2 )) # "│ ⎇ " and the dirty mark
|
|
|
|
|
|
[ "${g_ahead:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_ahead} ))
|
|
|
|
|
|
[ "${g_behind:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_behind} ))
|
|
|
|
|
|
$SHOW_TOKENS && tail_len=$(( tail_len + 3 + bar_w + 1 \
|
|
|
|
|
|
+ ${#used_tokens} + 1 + ${#total_tokens} + 1 + ${#pct_used} + 1 ))
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
$SHOW_THINKING && tail_len=$(( tail_len + 3 + 2 + ${#thinking_label} ))
|
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
|
|
|
|
[ -n "$cost_fmt" ] && tail_len=$(( tail_len + 3 + 1 + ${#cost_fmt} ))
|
|
|
|
|
|
local_max=$(( USABLE_WIDTH - $(vis_len "$out") - tail_len ))
|
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_max" -lt 8 ] && local_max=8
|
|
|
|
|
|
[ "$local_max" -gt 24 ] && local_max=24
|
|
|
|
|
|
fi
|
2026-08-08 11:54:05 +01:00
|
|
|
|
g_branch_display=$(truncate_str "$g_branch" "$local_max")
|
|
|
|
|
|
|
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
|
|
|
|
out+="${sep}${dim}⎇${reset} ${magenta}$(esc_data "$g_branch_display")${reset}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
|
|
|
|
|
if [ "$g_dirty" = "dirty" ]; then
|
|
|
|
|
|
out+=" ${red}✗${reset}"
|
|
|
|
|
|
else
|
|
|
|
|
|
out+=" ${green}✔${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Ahead/behind: shown in all tiers except narrow (only if non-zero)
|
|
|
|
|
|
if [ "$width_tier" != "narrow" ]; then
|
|
|
|
|
|
[ "${g_ahead:-0}" -gt 0 ] && out+=" ${green}↑${g_ahead}${reset}"
|
|
|
|
|
|
[ "${g_behind:-0}" -gt 0 ] && out+=" ${orange}↓${g_behind}${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Token bar
|
|
|
|
|
|
if $SHOW_TOKENS; then
|
|
|
|
|
|
token_bar=$(build_bar "$pct_used" "$bar_w")
|
|
|
|
|
|
out+="${sep}${token_bar} ${orange}${used_tokens}${dim}/${reset}${white}${total_tokens}${reset} ${dim}${pct_used}%${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
Read thinking state from the payload and show the effort level
The indicator read alwaysThinkingEnabled from settings.json, which Option+T
never writes - it toggles thinking for the session only. The key was also
absent here, so `// false` pinned the segment to the hollow "off" diamond
while thinking was actually on: an inverted indicator, not just a stale one.
Claude Code pipes the live state in as .thinking.enabled, alongside
.effort.level. Read both, in one jq pass since the line re-renders on every
redraw. Absent thinking means enabled, matching the renderer's own
`thinking:{enabled: lt !== false}`.
The effort level now replaces the static "thinking" label, so the segment
says something that changes: "◆ high" rather than a word that was there
either way. Unrecognised levels fall back to the old label, since the wrap
branch sizes line one from this string.
Test widths are usable widths, not raw terminal widths: the tiers are
computed after padding is subtracted, so a raw width would change tier
whenever statusLine.padding did. The usage cache is seeded for the same
reason width_test.sh seeds it - an unseeded cache makes the first render
fetch live usage over the network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 19:20:56 +01:00
|
|
|
|
# Thinking and effort. The diamond is thinking state, the label is the effort
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# level ("thinking" only when the payload reports no level). Hidden at the
|
|
|
|
|
|
# narrow tier, where line one has no columns to spare.
|
2026-08-08 11:54:05 +01:00
|
|
|
|
if $SHOW_THINKING && [ "$width_tier" != "narrow" ]; then
|
|
|
|
|
|
out+="${sep}"
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
if $thinking_on; then out+="${amber}◆ ${thinking_label}${reset}"
|
|
|
|
|
|
else out+="${dim}◇ ${thinking_label}${reset}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Session cost — wide/full only
|
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
|
|
|
|
if [ -n "$cost_fmt" ] && [ "$width_tier" = "wide" -o "$width_tier" = "full" ]; then
|
|
|
|
|
|
out+="${sep}${dim}\$${cost_fmt}${reset}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-09-02 13:01:12 +01:00
|
|
|
|
# ===== Rate limits (API, cached USAGE_CACHE_SECS) =====
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Always rendered on a line of its own; which variant is emitted is decided by
|
|
|
|
|
|
# the measured ladder at the end. Skipped below RL_MIN_WIDTH, where not even
|
|
|
|
|
|
# the two bars fit on that line -- and skipping it also means no API call.
|
|
|
|
|
|
if $SHOW_RATE_LIMITS && [ "$USABLE_WIDTH" -ge "$RL_MIN_WIDTH" ] 2>/dev/null; then
|
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
|
|
|
|
api_cache="$CACHE_DIR/statusline-usage-cache.json"
|
Back off after a failed usage fetch instead of retrying every redraw
A failed fetch left the cache mtime untouched, so with no network every
redraw — and a redraw happens on every keystroke — tried curl again and
could wait --max-time 10 for it. Cutting the TTL from an hour to five
minutes made that start 55 minutes sooner, so it is worth fixing now.
The marker is a separate file rather than a touch of the cached
response: a cold /tmp has no response to touch, and that is exactly the
case with no stale data to fall back on, where the timeout is paid in
full with nothing to show for it.
It goes down before the request and is cleared when that lands, not
written afterwards. A fetch is in flight for as long as curl takes to
give up, and this cache dir is shared by every session, so redraws
starting inside that window are real rather than hypothetical: they now
serve stale data instead of each launching their own doomed request.
Every attempt writes it, so a repeated failure restarts the backoff, and
only an attempt does, so the redraws it suppresses cannot keep
re-stamping it and it always lapses.
Missing credentials stays outside the backoff — it costs no timeout, and
the redraw after a login should show the bars rather than wait out a
retry window it had no part in earning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:12:45 +01:00
|
|
|
|
fail_marker="$CACHE_DIR/statusline-usage-fail"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
needs_refresh=true
|
|
|
|
|
|
usage_data=""
|
|
|
|
|
|
|
|
|
|
|
|
if [ -f "$api_cache" ]; then
|
|
|
|
|
|
cache_mtime=$(stat -c %Y "$api_cache" 2>/dev/null || stat -f %m "$api_cache" 2>/dev/null)
|
|
|
|
|
|
now=$(date +%s)
|
|
|
|
|
|
cache_age=$(( now - cache_mtime ))
|
2026-09-02 13:01:12 +01:00
|
|
|
|
if [ "$cache_age" -lt "$USAGE_CACHE_SECS" ]; then
|
2026-08-08 11:54:05 +01:00
|
|
|
|
cached_content=$(cat "$api_cache" 2>/dev/null)
|
|
|
|
|
|
# Only use cache if it's valid data (not an error response)
|
|
|
|
|
|
if [ -n "$cached_content" ] && ! echo "$cached_content" | jq -e '.error' >/dev/null 2>&1; then
|
|
|
|
|
|
needs_refresh=false
|
|
|
|
|
|
usage_data="$cached_content"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if $needs_refresh; then
|
Back off after a failed usage fetch instead of retrying every redraw
A failed fetch left the cache mtime untouched, so with no network every
redraw — and a redraw happens on every keystroke — tried curl again and
could wait --max-time 10 for it. Cutting the TTL from an hour to five
minutes made that start 55 minutes sooner, so it is worth fixing now.
The marker is a separate file rather than a touch of the cached
response: a cold /tmp has no response to touch, and that is exactly the
case with no stale data to fall back on, where the timeout is paid in
full with nothing to show for it.
It goes down before the request and is cleared when that lands, not
written afterwards. A fetch is in flight for as long as curl takes to
give up, and this cache dir is shared by every session, so redraws
starting inside that window are real rather than hypothetical: they now
serve stale data instead of each launching their own doomed request.
Every attempt writes it, so a repeated failure restarts the backoff, and
only an attempt does, so the redraws it suppresses cannot keep
re-stamping it and it always lapses.
Missing credentials stays outside the backoff — it costs no timeout, and
the redraw after a login should show the bars rather than wait out a
retry window it had no part in earning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:12:45 +01:00
|
|
|
|
# Hold off after a failed fetch. With no network curl can burn its
|
|
|
|
|
|
# --max-time before giving up, and a redraw happens on every keystroke,
|
|
|
|
|
|
# so retrying each time would stall the whole status line. The marker is
|
|
|
|
|
|
# separate from the cached response, rather than a touch of it: a cold
|
|
|
|
|
|
# /tmp has no response to touch, which is exactly when there is also no
|
|
|
|
|
|
# stale data to fall back on and the timeout is paid in full.
|
|
|
|
|
|
attempt=true
|
|
|
|
|
|
if [ -f "$fail_marker" ]; then
|
|
|
|
|
|
fail_mtime=$(stat -c %Y "$fail_marker" 2>/dev/null || stat -f %m "$fail_marker" 2>/dev/null)
|
|
|
|
|
|
[ $(( $(date +%s) - fail_mtime )) -lt "$USAGE_RETRY_SECS" ] && attempt=false
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if $attempt; then
|
|
|
|
|
|
token=$(get_oauth_token)
|
|
|
|
|
|
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
|
|
|
|
# Marked before the call, cleared when it lands. A fetch is in
|
|
|
|
|
|
# flight for as long as curl takes to time out, and this cache
|
|
|
|
|
|
# dir is shared by every session, so redraws that start inside
|
|
|
|
|
|
# that window are real: they now serve stale data instead of
|
|
|
|
|
|
# each launching their own doomed request. Written on every
|
|
|
|
|
|
# attempt, so a repeated failure restarts the backoff, and only
|
|
|
|
|
|
# by an attempt, so the redraws it suppresses cannot keep
|
|
|
|
|
|
# re-stamping it and it always lapses.
|
|
|
|
|
|
#
|
|
|
|
|
|
# No credentials is not a failure worth suppressing: it costs no
|
|
|
|
|
|
# timeout, and the next redraw after a login should show the
|
|
|
|
|
|
# bars rather than wait out a retry window. Hence inside the
|
|
|
|
|
|
# token check.
|
|
|
|
|
|
: > "$fail_marker"
|
|
|
|
|
|
response=$(curl -s --max-time 10 \
|
|
|
|
|
|
-H "Accept: application/json" \
|
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
|
-H "Authorization: Bearer $token" \
|
|
|
|
|
|
-H "anthropic-beta: oauth-2025-04-20" \
|
|
|
|
|
|
-H "User-Agent: claude-code/2.1.34" \
|
|
|
|
|
|
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
|
|
|
|
|
|
if [ -n "$response" ] && echo "$response" | jq . >/dev/null 2>&1; then
|
|
|
|
|
|
# Only cache successful (non-error) responses
|
|
|
|
|
|
if ! echo "$response" | jq -e '.error' >/dev/null 2>&1; then
|
|
|
|
|
|
usage_data="$response"
|
|
|
|
|
|
echo "$response" > "$api_cache"
|
|
|
|
|
|
rm -f "$fail_marker"
|
|
|
|
|
|
fi
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
Back off after a failed usage fetch instead of retrying every redraw
A failed fetch left the cache mtime untouched, so with no network every
redraw — and a redraw happens on every keystroke — tried curl again and
could wait --max-time 10 for it. Cutting the TTL from an hour to five
minutes made that start 55 minutes sooner, so it is worth fixing now.
The marker is a separate file rather than a touch of the cached
response: a cold /tmp has no response to touch, and that is exactly the
case with no stale data to fall back on, where the timeout is paid in
full with nothing to show for it.
It goes down before the request and is cleared when that lands, not
written afterwards. A fetch is in flight for as long as curl takes to
give up, and this cache dir is shared by every session, so redraws
starting inside that window are real rather than hypothetical: they now
serve stale data instead of each launching their own doomed request.
Every attempt writes it, so a repeated failure restarts the backoff, and
only an attempt does, so the redraws it suppresses cannot keep
re-stamping it and it always lapses.
Missing credentials stays outside the backoff — it costs no timeout, and
the redraw after a login should show the bars rather than wait out a
retry window it had no part in earning.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 13:12:45 +01:00
|
|
|
|
|
2026-08-08 11:54:05 +01:00
|
|
|
|
# Fall back to stale cache if refresh failed — skip if it's an error response
|
|
|
|
|
|
if [ -z "$usage_data" ] && [ -f "$api_cache" ]; then
|
|
|
|
|
|
stale=$(cat "$api_cache" 2>/dev/null)
|
|
|
|
|
|
! echo "$stale" | jq -e '.error' >/dev/null 2>&1 && usage_data="$stale"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
|
|
|
|
|
|
bar_width=6
|
|
|
|
|
|
|
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
|
|
|
|
# One jq pass for the whole payload. Parsing it field by field meant
|
|
|
|
|
|
# ~10 jq processes per render on the same JSON; the status line runs on
|
|
|
|
|
|
# every redraw, so that was the dominant cost. Rounding happens in jq
|
|
|
|
|
|
# too, which removes the per-field awk calls.
|
|
|
|
|
|
#
|
|
|
|
|
|
# The per-model weekly limit (e.g. Fable) lives in .limits[] under kind
|
|
|
|
|
|
# "weekly_scoped" -- the legacy seven_day_opus / seven_day_sonnet fields
|
|
|
|
|
|
# are always null now. Prefer an is_active entry, else take the first.
|
|
|
|
|
|
# `.limits[]?` tolerates limits being absent or not an array, and
|
|
|
|
|
|
# `tonumber? // 0` tolerates a percentage arriving as a string.
|
|
|
|
|
|
# One field per line, not @tsv: tab is IFS whitespace, so `read` collapses
|
|
|
|
|
|
# runs of it and an empty field (no scoped limit -> empty display name)
|
|
|
|
|
|
# would silently shift every later field along by one.
|
|
|
|
|
|
{
|
|
|
|
|
|
IFS= read -r five_hour_pct
|
|
|
|
|
|
IFS= read -r seven_day_pct
|
|
|
|
|
|
IFS= read -r five_hour_reset_iso
|
|
|
|
|
|
IFS= read -r seven_day_reset_iso
|
|
|
|
|
|
IFS= read -r scoped_name
|
|
|
|
|
|
IFS= read -r scoped_pct
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
IFS= read -r scoped_reset_iso
|
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
|
|
|
|
IFS= read -r extra_enabled
|
|
|
|
|
|
IFS= read -r extra_pct
|
|
|
|
|
|
IFS= read -r extra_used
|
|
|
|
|
|
IFS= read -r extra_limit
|
|
|
|
|
|
} <<EOF
|
|
|
|
|
|
$(echo "$usage_data" | jq -r '
|
|
|
|
|
|
def num: (tonumber? // 0);
|
|
|
|
|
|
([.limits[]? | select(.kind == "weekly_scoped")]
|
|
|
|
|
|
| (map(select(.is_active)) + .) | first) as $scoped
|
|
|
|
|
|
| [ (.five_hour.utilization | num | round),
|
|
|
|
|
|
(.seven_day.utilization | num | round),
|
|
|
|
|
|
(.five_hour.resets_at // ""),
|
|
|
|
|
|
(.seven_day.resets_at // ""),
|
|
|
|
|
|
(($scoped.scope.model.display_name // "") | gsub("[\\n\\r\\t]"; " ")),
|
|
|
|
|
|
($scoped.percent | num | round),
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
($scoped.resets_at // ""),
|
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
|
|
|
|
(.extra_usage.is_enabled // false),
|
|
|
|
|
|
(.extra_usage.utilization | num | round),
|
|
|
|
|
|
((.extra_usage.used_credits | num) / 100 * 100 | round / 100),
|
|
|
|
|
|
((.extra_usage.monthly_limit | num) / 100 * 100 | round / 100)
|
|
|
|
|
|
] | .[] | tostring' 2>/dev/null)
|
|
|
|
|
|
EOF
|
|
|
|
|
|
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
# ===== Sustainable burn: pace + trend =====
|
|
|
|
|
|
# pace = the limit's remaining points divided by the WORKING days left
|
|
|
|
|
|
# before it resets, i.e. what can be spent per working day and still
|
|
|
|
|
|
# land on 100% exactly at the reset. A five-day week spreads 100 points
|
|
|
|
|
|
# over 5 days, so a fresh window paces at 20%/day; the 14.3%/day
|
|
|
|
|
|
# calendar figure quietly assumes the weekend is worked too.
|
|
|
|
|
|
# trend compares used% against a BAND rather than a point: during
|
|
|
|
|
|
# workday n of m, anything from (n-1)/m to n/m of the budget is on
|
|
|
|
|
|
# track, because the whole of today's allowance is today's to spend. A
|
|
|
|
|
|
# point built from complete workdays only made every Monday morning red
|
|
|
|
|
|
# the moment 3 points had gone. Ahead of the band means the limit will
|
|
|
|
|
|
# cap before the week is out, behind it means part of the subscription
|
|
|
|
|
|
# goes unused; a +-3 slack outside the band stops it flickering at the
|
|
|
|
|
|
# edges. On a non-workday no allowance is in play, so the band
|
|
|
|
|
|
# collapses to its floor.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Whole workdays only, no time-of-day interpolation: the percentages
|
|
|
|
|
|
# arrive as integers anyway, and "three working days left" is the
|
|
|
|
|
|
# granularity the decision actually gets made at.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Measured against the per-model limit whenever one is being shown --
|
|
|
|
|
|
# that is the limit that runs out first, and the bar this annotates.
|
|
|
|
|
|
# Same gate as seg_scoped below, so pace always describes the bar it is
|
|
|
|
|
|
# printed next to. Its own resets_at wins, falling back to the 7-day
|
|
|
|
|
|
# timestamp (the two reset together) when the payload omits it.
|
|
|
|
|
|
seg_pace=""
|
|
|
|
|
|
if $SHOW_MODEL_LIMIT && [ -n "$scoped_name" ]; then
|
|
|
|
|
|
pace_used="${scoped_pct:-0}"
|
|
|
|
|
|
pace_reset_iso="$scoped_reset_iso"
|
|
|
|
|
|
else
|
|
|
|
|
|
pace_used="${seven_day_pct:-0}"
|
|
|
|
|
|
pace_reset_iso=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
case "$pace_reset_iso" in ''|null) pace_reset_iso="$seven_day_reset_iso" ;; esac
|
|
|
|
|
|
|
|
|
|
|
|
pace_epoch=""
|
|
|
|
|
|
case "$pace_reset_iso" in ''|null) ;; *) pace_epoch=$(iso_to_epoch "$pace_reset_iso") ;; esac
|
|
|
|
|
|
if [ -n "$pace_epoch" ]; then
|
|
|
|
|
|
ds_now=$(day_start_dow "$(now_ts)")
|
|
|
|
|
|
ds_reset=$(day_start_dow "$pace_epoch")
|
|
|
|
|
|
# Whole local days from today to the reset's day. Rounded rather
|
|
|
|
|
|
# than divided: a DST change makes one of those days 23 or 25 hours
|
|
|
|
|
|
# long, and truncation would lose a day either side of it.
|
|
|
|
|
|
pace_days=""
|
|
|
|
|
|
if [ -n "$ds_now" ] && [ -n "$ds_reset" ]; then
|
|
|
|
|
|
pace_days=$(( (${ds_reset%% *} - ${ds_now%% *} + 43200) / 86400 ))
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# No strftime in macOS awk, so the weekday of each day in the
|
|
|
|
|
|
# window is derived from today's, which bash passes in.
|
|
|
|
|
|
pace_tv=""
|
|
|
|
|
|
[ -n "$pace_days" ] && pace_tv=$(awk \
|
|
|
|
|
|
-v used="$pace_used" -v dtr="$pace_days" \
|
|
|
|
|
|
-v dow0="${ds_now##* }" -v wd=",${SL_WORK_DAYS}," 'BEGIN{
|
|
|
|
|
|
# Day indices, 0 = today. The reset is 7 days after the
|
|
|
|
|
|
# previous one, so the window is [dtr-7, dtr) and today has to
|
|
|
|
|
|
# lie inside it; anything else is stale or nonsense data.
|
|
|
|
|
|
if (dtr < 0 || dtr > 7 || dow0 < 1 || dow0 > 7) exit 1
|
|
|
|
|
|
for (i = dtr - 7; i < dtr; i++) {
|
|
|
|
|
|
dow = ((dow0 - 1 + i) % 7 + 7) % 7 + 1
|
|
|
|
|
|
if (index(wd, dow) == 0) continue
|
|
|
|
|
|
total++
|
|
|
|
|
|
if (i < 0) elapsed++; else left++ # today counts as left
|
|
|
|
|
|
}
|
|
|
|
|
|
if (total <= 0) exit 1 # no workdays: no baseline
|
|
|
|
|
|
rem = 100 - used; if (rem < 0) rem = 0
|
|
|
|
|
|
d = (left < 1) ? 1 : left # nothing but today: all that remains
|
|
|
|
|
|
pr = rem / d
|
|
|
|
|
|
# Today is workday elapsed+1 of total, so its band runs from
|
|
|
|
|
|
# elapsed/total to (elapsed+1)/total. Only its distance is
|
|
|
|
|
|
# reported, signed: + past the top, - short of the floor, 0
|
|
|
|
|
|
# anywhere inside the band or its slack. Comparing here rather
|
|
|
|
|
|
# than in bash keeps the band edges off a second rounding.
|
|
|
|
|
|
lo = elapsed / total * 100
|
|
|
|
|
|
hi = (index(wd, dow0) > 0) ? (elapsed + 1) / total * 100 : lo
|
|
|
|
|
|
dv = (used > hi + 3) ? used - hi : (used < lo - 3) ? used - lo : 0
|
|
|
|
|
|
# One decimal below 10, where rounding starts to matter. The
|
|
|
|
|
|
# last two fields are pace and the baseline in tenths, for
|
|
|
|
|
|
# the integer comparison in pacecol.
|
|
|
|
|
|
printf "%s %.0f %.0f %.0f\n", \
|
|
|
|
|
|
(pr < 10) ? sprintf("%.1f", pr) : sprintf("%.0f", pr), \
|
|
|
|
|
|
dv, pr * 10, 1000 / total
|
|
|
|
|
|
}' 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
|
|
if [ -n "$pace_tv" ]; then
|
|
|
|
|
|
read -r pace_val pace_trend pace_tenths pace_base <<< "$pace_tv"
|
|
|
|
|
|
seg_pace=" $(pacecol "$pace_tenths" "$pace_base")${pace_val}%/d${reset}"
|
|
|
|
|
|
if [ "$pace_trend" -gt 0 ] 2>/dev/null; then seg_pace+=" ${red}▲+${pace_trend}${reset}"
|
|
|
|
|
|
elif [ "$pace_trend" -lt 0 ] 2>/dev/null; then seg_pace+=" ${cyan}▼${pace_trend}${reset}"
|
|
|
|
|
|
else seg_pace+=" ${green}✓${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Five nested variants of the rate-limit group, each rung giving up the
|
|
|
|
|
|
# least useful thing left:
|
|
|
|
|
|
# rl_rich bars + pace + reset times + extra usage
|
|
|
|
|
|
# rl_time bars + pace + reset times
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
# rl_pace bars + pace
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# rl_lean bars, per-model included
|
|
|
|
|
|
# rl_bare the 5h and 7d bars alone
|
|
|
|
|
|
# The ladder at the end emits the richest one that fits line two.
|
|
|
|
|
|
# Keeping the leaner rungs matters: extra-usage credits add ~30 columns
|
|
|
|
|
|
# and the two timestamps ~26, either of which can overflow the line.
|
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
|
|
|
|
seg_5h="${dim}5h${reset} $(build_bar "${five_hour_pct:-0}" "$bar_width") ${cyan}${five_hour_pct:-0}%${reset}"
|
|
|
|
|
|
seg_7d="${sep}${dim}7d${reset} $(build_bar "${seven_day_pct:-0}" "$bar_width") ${cyan}${seven_day_pct:-0}%${reset}"
|
|
|
|
|
|
|
|
|
|
|
|
# Rendered inside the 7d segment rather than as its own: both are weekly
|
|
|
|
|
|
# limits resetting at the same time, so one shared reset label covers
|
|
|
|
|
|
# the pair and saves a separator plus a second timestamp.
|
|
|
|
|
|
seg_scoped=""
|
|
|
|
|
|
if $SHOW_MODEL_LIMIT && [ -n "$scoped_name" ]; then
|
|
|
|
|
|
seg_scoped=" ${dim}$(esc_data "$(truncate_str "$scoped_name" 8)")${reset} $(build_bar "${scoped_pct:-0}" "$bar_width") ${cyan}${scoped_pct:-0}%${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Extra usage, when the account has it enabled.
|
|
|
|
|
|
seg_extra=""
|
|
|
|
|
|
if [ "$extra_enabled" = "true" ]; then
|
|
|
|
|
|
seg_extra="${sep}${dim}extra${reset} $(build_bar "${extra_pct:-0}" "$bar_width") ${cyan}\$$(printf '%.2f' "${extra_used:-0}")${dim}/\$$(printf '%.2f' "${extra_limit:-0}")${reset}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# rl_bare drops the per-model bar too: the last thing worth giving up,
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# and all that fits on a terminal barely wider than the two bars.
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
#
|
|
|
|
|
|
# rl_pace is its own rung so pace is given up BEFORE the per-model bar
|
|
|
|
|
|
# it annotates: a bar with no pace still says something, a pace with no
|
|
|
|
|
|
# bar does not. seg_pace lands after the per-model percentage, or after
|
|
|
|
|
|
# the 7d one on an account with no per-model limit (seg_scoped empty).
|
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
|
|
|
|
rl_bare="${seg_5h}${seg_7d}"
|
|
|
|
|
|
rl_lean="${rl_bare}${seg_scoped}"
|
Pace the per-model weekly limit over the days you actually work
The weekly limit resets every 7 days, but a 5-day week means the sustainable
burn is 20% a day, not the 100/7 the calendar implies. Nothing on the line
said that, so staying inside the limit meant doing the division by hand.
Adds pace and trend to the per-model bar: "Fable 6% 19%/d ✓". pace is what
is left divided by the working days still in the window, so it is the
envelope for today. trend compares usage against today's band - during
working day n of m, anywhere between (n-1)/m and n/m of the budget is on
track - and reports the distance outside it.
A band rather than a point because a point built from completed days expects
0% on the first working day of the window, so any usage at all reads as
overspending: 6% on a Monday morning showed a red arrow. The band is also
all whole-day granularity supports, and the sleep-aware glide in
statusline.burnrate.sh is what it would take to say more.
Working days come from SL_WORK_DAYS (ISO weekdays, default Mon-Fri) and are
counted inside the reset window rather than assumed, so a window that starts
mid-week still divides correctly. The colour thresholds derive from
100/total_workdays instead of the hardcoded 14.3-a-day ones in burnrate.sh,
which would call 13%/d healthy against a 20-a-day budget.
SL_NOW is a test seam for the clock. The cache-age checks deliberately stay
on the real clock: a pinned SL_NOW could otherwise age a fresh fixture past
USAGE_CACHE_SECS and send a test to the network with real credentials.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 21:02:47 +01:00
|
|
|
|
rl_pace="${rl_lean}${seg_pace}"
|
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
|
|
|
|
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# rl_time interleaves rather than appends: the 5h timestamp sits with
|
|
|
|
|
|
# its own bar, and the 7d one closes the weekly group it shares with
|
|
|
|
|
|
# the per-model bar and the pace.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Formatting the pair costs ~10 subprocesses (date has no portable
|
|
|
|
|
|
# one-shot form here) on a line that redraws per keystroke, so it is
|
|
|
|
|
|
# skipped when line two provably cannot show them. RESET_COST
|
|
|
|
|
|
# deliberately UNDER-estimates their combined width -- " ↺ 1:00am" and
|
|
|
|
|
|
# " ↺ may 4, 1:00am" at their shortest -- so the ladder below, which
|
|
|
|
|
|
# measures the real string, stays the thing that decides.
|
|
|
|
|
|
RESET_COST=25
|
|
|
|
|
|
pace_len=$(vis_len "$rl_pace")
|
|
|
|
|
|
rl_time="$rl_pace"
|
|
|
|
|
|
if [ $(( pace_len + RESET_COST )) -le "$USABLE_WIDTH" ]; then
|
2026-08-08 11:54:05 +01:00
|
|
|
|
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
|
|
|
|
|
|
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}"
|
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
|
|
|
|
r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}↺ ${seven_day_reset}${reset}"
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
rl_time="${seg_5h}${r5}${seg_7d}${seg_scoped}${seg_pace}${r7}"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
rl_rich="${rl_time}${seg_extra}"
|
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
|
|
|
|
fi
|
|
|
|
|
|
fi
|
2026-08-08 11:54:05 +01:00
|
|
|
|
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Attach the rate-limit group on a line of its own, emitting the richest
|
|
|
|
|
|
# variant that fits it.
|
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
|
|
|
|
#
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
# Line two is never shared with line one's segments, however wide the terminal
|
|
|
|
|
|
# is. Squeezing the group onto line one is what used to cost the reset times
|
|
|
|
|
|
# and the pace figure on a laptop-width window, and it made the layout jump
|
|
|
|
|
|
# between one and two lines as the branch name or the cost changed width.
|
|
|
|
|
|
# A full line to itself is worth more than the columns it wastes.
|
|
|
|
|
|
#
|
|
|
|
|
|
# The extra-usage credits ride on the richest rung alone, so an account with
|
|
|
|
|
|
# overage enabled sees them only on a terminal wide enough for the whole group
|
|
|
|
|
|
# -- around 110 columns. Below that the reset times win the space: they are the
|
|
|
|
|
|
# number you act on, the credits the one you read afterwards.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Every rung is fit-checked, the last one included, so nothing is chosen that
|
|
|
|
|
|
# the renderer would clip, whatever the model name, the extra-usage width or
|
|
|
|
|
|
# the percentages the API reports. If not even the two bars fit, the group is
|
|
|
|
|
|
# dropped rather than overflowed.
|
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
|
|
|
|
if [ -n "$rl_bare" ]; then
|
Give the usage bars a line of their own, always
Squeezing the 5h/7d group onto line one was what cost the reset times and
the pace figure at laptop width, and it made the layout jump between one
and two lines as the branch name or the cost changed width. Line two is
now the group's own at every terminal size, so both timestamps and the
burn rate fit; the ladder only starts giving things up below ~80 columns.
The bars also survive down to 35 columns now rather than being dropped
below 68, since a line of their own is all they need. Every rung of the
ladder is fit-checked, the last one included: with the floor that low, a
percentage the API reports in more digits than anyone expects would
otherwise have overflowed the line rather than dropped the group.
The two timestamps still cost ~10 subprocesses to format on a line that
redraws per keystroke, so that is skipped when line two provably cannot
show them -- an under-estimate, so the fit check stays the decider.
With nothing left to squeeze, WRAP_NARROW, the wrap band and the split
tier go: line one is the full tier down to 150, wide to 68, then narrow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 16:51:05 +01:00
|
|
|
|
if [ "$(vis_len "$rl_rich")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich"
|
|
|
|
|
|
elif [ "$(vis_len "$rl_time")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_time"
|
|
|
|
|
|
elif [ "$pace_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_pace"
|
|
|
|
|
|
elif [ "$(vis_len "$rl_lean")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean"
|
|
|
|
|
|
elif [ "$(vis_len "$rl_bare")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_bare"
|
2026-08-08 11:54:05 +01:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
printf "%b" "$out"
|
|
|
|
|
|
exit 0
|