Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab96dfc52a |
||
|
|
2a785a55a7 |
8 changed files with 548 additions and 148 deletions
|
|
@ -11,14 +11,16 @@ SHOW_TOKENS=true # token usage bar
|
||||||
SHOW_THINKING=true # extended thinking indicator
|
SHOW_THINKING=true # extended thinking indicator
|
||||||
SHOW_RATE_LIMITS=true # 5h / 7d rate limit bars
|
SHOW_RATE_LIMITS=true # 5h / 7d rate limit bars
|
||||||
SHOW_MODEL_LIMIT=true # per-model weekly limit bar (e.g. Fable), when the API reports one
|
SHOW_MODEL_LIMIT=true # per-model weekly limit bar (e.g. Fable), when the API reports one
|
||||||
WRAP_NARROW=true # wrap onto a second line rather than dropping segments or being clipped
|
RL_MIN_WIDTH=35 # narrowest usable width the 5h + 7d bars fit on their own
|
||||||
WRAP_MIN_WIDTH=100 # below this, keep wide-tier line-one content (it can wrap)
|
# line: two of "5h ██████ 100%" plus a separator, +4
|
||||||
|
COMPACT_WIDTH=100 # below this, shorten line one to what it has room for
|
||||||
BRANCH_MAX_LEN=28 # truncate branch names longer than this
|
BRANCH_MAX_LEN=28 # truncate branch names longer than this
|
||||||
CWD_MAX_LEN=20 # truncate the cwd basename longer than this
|
CWD_MAX_LEN=20 # truncate the cwd basename longer than this
|
||||||
GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large repos)
|
GIT_CACHE_SECS=10 # seconds to cache git status (git diff is slow on large repos)
|
||||||
USAGE_CACHE_SECS=300 # seconds to cache the usage API response (the 5h/7d bars)
|
USAGE_CACHE_SECS=300 # seconds to cache the usage API response (the 5h/7d bars)
|
||||||
USAGE_RETRY_SECS=60 # seconds to wait before retrying a failed usage API fetch
|
USAGE_RETRY_SECS=60 # seconds to wait before retrying a failed usage API fetch
|
||||||
TOKEN_BAR_WIDTH=8 # width of token progress bar
|
TOKEN_BAR_WIDTH=8 # width of token progress bar
|
||||||
|
SL_WORK_DAYS="${SL_WORK_DAYS:-12345}" # ISO weekdays you work: Mon=1 ... Sun=7
|
||||||
|
|
||||||
# Where the git and usage-API caches live. Overridable via the environment so a
|
# 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
|
# test run can render into its own directory: ~/.claude/statusline.sh is a
|
||||||
|
|
@ -26,6 +28,14 @@ TOKEN_BAR_WIDTH=8 # width of token progress bar
|
||||||
# the next redraw and shown as real usage until USAGE_CACHE_SECS is up.
|
# the next redraw and shown as real usage until USAGE_CACHE_SECS is up.
|
||||||
CACHE_DIR="${STATUSLINE_CACHE_DIR:-/tmp/claude}"
|
CACHE_DIR="${STATUSLINE_CACHE_DIR:-/tmp/claude}"
|
||||||
|
|
||||||
|
# "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; }
|
||||||
|
|
||||||
# Terminal width detection.
|
# Terminal width detection.
|
||||||
# Claude Code exports COLUMNS for this subprocess. There is no controlling
|
# Claude Code exports COLUMNS for this subprocess. There is no controlling
|
||||||
# terminal, so the stty fallback fails; when both fail we default to 80
|
# terminal, so the stty fallback fails; when both fail we default to 80
|
||||||
|
|
@ -169,6 +179,25 @@ build_bar() {
|
||||||
printf "${bar_color}${f}${dim}${e}${reset}"
|
printf "${bar_color}${f}${dim}${e}${reset}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
# ===== Git info with per-directory caching =====
|
# ===== Git info with per-directory caching =====
|
||||||
get_git_info() {
|
get_git_info() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
|
|
@ -278,6 +307,16 @@ iso_to_epoch() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
|
||||||
format_reset_time() {
|
format_reset_time() {
|
||||||
local iso_str="$1" style="$2"
|
local iso_str="$1" style="$2"
|
||||||
[ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return
|
[ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return
|
||||||
|
|
@ -300,8 +339,8 @@ format_reset_time() {
|
||||||
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
||||||
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
cwd=$(echo "$input" | jq -r '.cwd // empty')
|
||||||
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
|
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // empty')
|
||||||
# Formatted here rather than where it is rendered: the wrap-mode branch budget
|
# Formatted here rather than where it is rendered: the compact-tier branch
|
||||||
# needs its width, and it is not fixed ("$4.61" vs "$1234.56").
|
# budget needs its width, and it is not fixed ("$4.61" vs "$1234.56").
|
||||||
cost_fmt=""
|
cost_fmt=""
|
||||||
[ -n "$cost_usd" ] && cost_fmt=$(printf '%.2f' "$cost_usd" 2>/dev/null)
|
[ -n "$cost_usd" ] && cost_fmt=$(printf '%.2f' "$cost_usd" 2>/dev/null)
|
||||||
|
|
||||||
|
|
@ -332,8 +371,8 @@ thinking_on=true
|
||||||
[ "$sl_thinking" = "false" ] && thinking_on=false
|
[ "$sl_thinking" = "false" ] && thinking_on=false
|
||||||
|
|
||||||
# The effort level replaces the static "thinking" label. Whitelisted rather
|
# The effort level replaces the static "thinking" label. Whitelisted rather
|
||||||
# than printed as-is: an unrecognised value would escape the width budget that
|
# than printed as-is: an unrecognised value would escape the width budget the
|
||||||
# the wrap branch computes from this string.
|
# compact tier computes from this string.
|
||||||
case "$effort" in
|
case "$effort" in
|
||||||
low|medium|high|xhigh|max) thinking_label="$effort" ;;
|
low|medium|high|xhigh|max) thinking_label="$effort" ;;
|
||||||
*) thinking_label="thinking" ;;
|
*) thinking_label="thinking" ;;
|
||||||
|
|
@ -341,36 +380,28 @@ esac
|
||||||
|
|
||||||
# ===== Adaptive width tiers =====
|
# ===== Adaptive width tiers =====
|
||||||
#
|
#
|
||||||
# Tiers now choose only how much of LINE ONE to show. What the rate-limit
|
# The rate-limit group always gets line two to itself, so tiers choose only how
|
||||||
# group contains, and whether it wraps, is decided by measurement at the end of
|
# much of LINE ONE to show. What that group contains is decided by measurement
|
||||||
# the script, not here -- tiers cannot see how long the branch, cwd or model
|
# at the end of the script, not here -- tiers cannot see how long the branch,
|
||||||
# name is, which is how content used to end up clipped.
|
# cwd or model name is, which is how content used to end up clipped.
|
||||||
#
|
#
|
||||||
# full (≥150): CWD, ahead/behind, "◆ high" effort, cost
|
# full (≥150): CWD, ahead/behind, "◆ high" effort, cost
|
||||||
# wide (100–149): ahead/behind, "◆ high" effort, cost
|
# wide (68–149): the same without the CWD, and below COMPACT_WIDTH with a
|
||||||
# split (76–99): short model, ahead/behind, "◆" symbol
|
# short model name and a branch budgeted to what is left
|
||||||
# (reachable only with WRAP_NARROW=false; otherwise the wrap
|
# narrow (<68): short model + branch + token bar only -- the usage group is
|
||||||
# band below overrides this range to wide)
|
# unaffected, it has its own line at every width
|
||||||
# narrow (<76): short model + branch + token only; no rate limits at all
|
|
||||||
#
|
#
|
||||||
if [ "$USABLE_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full"
|
if [ "$USABLE_WIDTH" -ge 150 ] 2>/dev/null; then width_tier="full"
|
||||||
elif [ "$USABLE_WIDTH" -ge 100 ] 2>/dev/null; then width_tier="wide"
|
elif [ "$USABLE_WIDTH" -ge 68 ] 2>/dev/null; then width_tier="wide"
|
||||||
elif [ "$USABLE_WIDTH" -ge 76 ] 2>/dev/null; then width_tier="split"
|
|
||||||
else width_tier="narrow"
|
else width_tier="narrow"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Two-line mode. Between WRAP_FLOOR and WRAP_MIN_WIDTH there isn't room for
|
# Compact: the wide tier with a short model name and the branch cut to whatever
|
||||||
# everything on one line, but there IS room across two — so instead of dropping
|
# line one has left. Between 68 and COMPACT_WIDTH columns the wide-tier
|
||||||
# the rate-limit group we break before it and render the wide-tier content.
|
# segments do fit, but only once those two are budgeted rather than assumed.
|
||||||
# Below WRAP_FLOOR the narrow tier applies instead, which drops the rate-limit
|
compact=false
|
||||||
# group entirely -- so there is nothing to wrap and no second line to put it on.
|
if [ "$width_tier" = "wide" ] && [ "$USABLE_WIDTH" -lt "$COMPACT_WIDTH" ] 2>/dev/null; then
|
||||||
WRAP_FLOOR=68
|
compact=true
|
||||||
wrap_mode=false
|
|
||||||
if $WRAP_NARROW \
|
|
||||||
&& [ "$USABLE_WIDTH" -lt "$WRAP_MIN_WIDTH" ] 2>/dev/null \
|
|
||||||
&& [ "$USABLE_WIDTH" -ge "$WRAP_FLOOR" ] 2>/dev/null; then
|
|
||||||
wrap_mode=true
|
|
||||||
width_tier="wide"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Shorten model name for tight spaces
|
# Shorten model name for tight spaces
|
||||||
|
|
@ -389,8 +420,10 @@ short_model() {
|
||||||
out=""
|
out=""
|
||||||
rl_bare=""
|
rl_bare=""
|
||||||
rl_lean=""
|
rl_lean=""
|
||||||
rl_mid=""
|
rl_pace=""
|
||||||
|
rl_time=""
|
||||||
rl_rich=""
|
rl_rich=""
|
||||||
|
pace_len=""
|
||||||
|
|
||||||
# Model — color by family
|
# Model — color by family
|
||||||
model_color="$blue"
|
model_color="$blue"
|
||||||
|
|
@ -400,10 +433,9 @@ case "$model_name" in
|
||||||
esac
|
esac
|
||||||
|
|
||||||
display_model="$model_name"
|
display_model="$model_name"
|
||||||
# split/narrow: shorten so the 5h + 7d bars still fit on one line.
|
# A long display name ("Opus 5 (1M context)" is 19 cols) would overflow line
|
||||||
# wrap mode too: it renders wide-tier content at a split-tier width, and a long
|
# one at these widths, so it gives up everything but the family.
|
||||||
# display name ("Opus 5 (1M context)" is 19 cols) would overflow line one.
|
if [ "$width_tier" = "narrow" ] || $compact; then
|
||||||
if [ "$width_tier" = "split" -o "$width_tier" = "narrow" ] || $wrap_mode; then
|
|
||||||
display_model=$(short_model "$model_name")
|
display_model=$(short_model "$model_name")
|
||||||
fi
|
fi
|
||||||
out+="${model_color}$(esc_data "$display_model")${reset}"
|
out+="${model_color}$(esc_data "$display_model")${reset}"
|
||||||
|
|
@ -418,7 +450,6 @@ fi
|
||||||
|
|
||||||
bar_w="$TOKEN_BAR_WIDTH"
|
bar_w="$TOKEN_BAR_WIDTH"
|
||||||
[ "$width_tier" = "wide" ] && bar_w=6
|
[ "$width_tier" = "wide" ] && bar_w=6
|
||||||
[ "$width_tier" = "split" ] && bar_w=5
|
|
||||||
[ "$width_tier" = "narrow" ] && bar_w=4
|
[ "$width_tier" = "narrow" ] && bar_w=4
|
||||||
|
|
||||||
# Git branch + dirty + ahead/behind
|
# Git branch + dirty + ahead/behind
|
||||||
|
|
@ -430,15 +461,14 @@ if $SHOW_GIT && [ -n "$cwd" ]; then
|
||||||
# Progressively tighten branch truncation
|
# Progressively tighten branch truncation
|
||||||
local_max="$BRANCH_MAX_LEN"
|
local_max="$BRANCH_MAX_LEN"
|
||||||
[ "$width_tier" = "wide" ] && local_max=24
|
[ "$width_tier" = "wide" ] && local_max=24
|
||||||
[ "$width_tier" = "split" ] && local_max=18
|
|
||||||
[ "$width_tier" = "narrow" ] && local_max=12
|
[ "$width_tier" = "narrow" ] && local_max=12
|
||||||
# In wrap mode line one is
|
# In compact mode line one is
|
||||||
# model │ ⎇ branch ✔ ↑1 │ <bar> used/total pct% │ ◇ high │ $cost
|
# model │ ⎇ branch ✔ ↑1 │ <bar> used/total pct% │ ◇ high │ $cost
|
||||||
# and the branch gets whatever the rest of it leaves. Everything after
|
# 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
|
# 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,
|
# 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.
|
# which pushed line one past the edge to be clipped by the renderer.
|
||||||
if $wrap_mode; then
|
if $compact; then
|
||||||
tail_len=$(( 5 + 2 )) # "│ ⎇ " and the dirty mark
|
tail_len=$(( 5 + 2 )) # "│ ⎇ " and the dirty mark
|
||||||
[ "${g_ahead:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_ahead} ))
|
[ "${g_ahead:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_ahead} ))
|
||||||
[ "${g_behind:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_behind} ))
|
[ "${g_behind:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_behind} ))
|
||||||
|
|
@ -475,18 +505,12 @@ if $SHOW_TOKENS; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Thinking and effort. The diamond is thinking state, the label is the effort
|
# Thinking and effort. The diamond is thinking state, the label is the effort
|
||||||
# level ("thinking" only when the payload reports no level):
|
# level ("thinking" only when the payload reports no level). Hidden at the
|
||||||
# full/wide → "◆ high" / "◇ high" (label)
|
# narrow tier, where line one has no columns to spare.
|
||||||
# split → "◆" / "◇" (symbol only, saves the label's columns)
|
|
||||||
# narrow → hidden
|
|
||||||
if $SHOW_THINKING && [ "$width_tier" != "narrow" ]; then
|
if $SHOW_THINKING && [ "$width_tier" != "narrow" ]; then
|
||||||
out+="${sep}"
|
out+="${sep}"
|
||||||
if $thinking_on; then
|
if $thinking_on; then out+="${amber}◆ ${thinking_label}${reset}"
|
||||||
if [ "$width_tier" = "split" ]; then out+="${amber}◆${reset}"
|
else out+="${dim}◇ ${thinking_label}${reset}"
|
||||||
else out+="${amber}◆ ${thinking_label}${reset}"; fi
|
|
||||||
else
|
|
||||||
if [ "$width_tier" = "split" ]; then out+="${dim}◇${reset}"
|
|
||||||
else out+="${dim}◇ ${thinking_label}${reset}"; fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -496,9 +520,10 @@ if [ -n "$cost_fmt" ] && [ "$width_tier" = "wide" -o "$width_tier" = "full" ]; t
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== Rate limits (API, cached USAGE_CACHE_SECS) =====
|
# ===== Rate limits (API, cached USAGE_CACHE_SECS) =====
|
||||||
# Built at every tier except narrow. Which variant is actually emitted,
|
# Always rendered on a line of its own; which variant is emitted is decided by
|
||||||
# and on how many lines, is decided by the measured ladder at the end.
|
# the measured ladder at the end. Skipped below RL_MIN_WIDTH, where not even
|
||||||
if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then
|
# 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
|
||||||
api_cache="$CACHE_DIR/statusline-usage-cache.json"
|
api_cache="$CACHE_DIR/statusline-usage-cache.json"
|
||||||
fail_marker="$CACHE_DIR/statusline-usage-fail"
|
fail_marker="$CACHE_DIR/statusline-usage-fail"
|
||||||
needs_refresh=true
|
needs_refresh=true
|
||||||
|
|
@ -575,7 +600,6 @@ if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then
|
||||||
|
|
||||||
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
|
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
|
||||||
bar_width=6
|
bar_width=6
|
||||||
[ "$width_tier" = "split" ] && bar_width=4
|
|
||||||
|
|
||||||
# One jq pass for the whole payload. Parsing it field by field meant
|
# 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
|
# ~10 jq processes per render on the same JSON; the status line runs on
|
||||||
|
|
@ -597,6 +621,7 @@ if $SHOW_RATE_LIMITS && [ "$width_tier" != "narrow" ]; then
|
||||||
IFS= read -r seven_day_reset_iso
|
IFS= read -r seven_day_reset_iso
|
||||||
IFS= read -r scoped_name
|
IFS= read -r scoped_name
|
||||||
IFS= read -r scoped_pct
|
IFS= read -r scoped_pct
|
||||||
|
IFS= read -r scoped_reset_iso
|
||||||
IFS= read -r extra_enabled
|
IFS= read -r extra_enabled
|
||||||
IFS= read -r extra_pct
|
IFS= read -r extra_pct
|
||||||
IFS= read -r extra_used
|
IFS= read -r extra_used
|
||||||
|
|
@ -612,6 +637,7 @@ $(echo "$usage_data" | jq -r '
|
||||||
(.seven_day.resets_at // ""),
|
(.seven_day.resets_at // ""),
|
||||||
(($scoped.scope.model.display_name // "") | gsub("[\\n\\r\\t]"; " ")),
|
(($scoped.scope.model.display_name // "") | gsub("[\\n\\r\\t]"; " ")),
|
||||||
($scoped.percent | num | round),
|
($scoped.percent | num | round),
|
||||||
|
($scoped.resets_at // ""),
|
||||||
(.extra_usage.is_enabled // false),
|
(.extra_usage.is_enabled // false),
|
||||||
(.extra_usage.utilization | num | round),
|
(.extra_usage.utilization | num | round),
|
||||||
((.extra_usage.used_credits | num) / 100 * 100 | round / 100),
|
((.extra_usage.used_credits | num) / 100 * 100 | round / 100),
|
||||||
|
|
@ -619,13 +645,110 @@ $(echo "$usage_data" | jq -r '
|
||||||
] | .[] | tostring' 2>/dev/null)
|
] | .[] | tostring' 2>/dev/null)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Three variants of the rate-limit group, richest first:
|
# ===== Sustainable burn: pace + trend =====
|
||||||
# rl_rich bars + reset times + extra usage
|
# pace = the limit's remaining points divided by the WORKING days left
|
||||||
# rl_mid bars + extra usage
|
# before it resets, i.e. what can be spent per working day and still
|
||||||
# rl_lean bars only
|
# land on 100% exactly at the reset. A five-day week spreads 100 points
|
||||||
# The ladder at the end emits the richest one that fits the space it
|
# over 5 days, so a fresh window paces at 20%/day; the 14.3%/day
|
||||||
# has. Keeping a lean variant matters: extra-usage credits add ~30
|
# calendar figure quietly assumes the weekend is worked too.
|
||||||
# columns, which can overflow line two on its own.
|
# 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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# rl_pace bars + pace
|
||||||
|
# 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.
|
||||||
seg_5h="${dim}5h${reset} $(build_bar "${five_hour_pct:-0}" "$bar_width") ${cyan}${five_hour_pct:-0}%${reset}"
|
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}"
|
seg_7d="${sep}${dim}7d${reset} $(build_bar "${seven_day_pct:-0}" "$bar_width") ${cyan}${seven_day_pct:-0}%${reset}"
|
||||||
|
|
||||||
|
|
@ -644,60 +767,64 @@ EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# rl_bare drops the per-model bar too: the last thing worth giving up,
|
# rl_bare drops the per-model bar too: the last thing worth giving up,
|
||||||
# and the only way to fit at all when wrapping is disabled and the
|
# and all that fits on a terminal barely wider than the two bars.
|
||||||
# terminal is narrow.
|
#
|
||||||
|
# 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).
|
||||||
rl_bare="${seg_5h}${seg_7d}"
|
rl_bare="${seg_5h}${seg_7d}"
|
||||||
rl_lean="${rl_bare}${seg_scoped}"
|
rl_lean="${rl_bare}${seg_scoped}"
|
||||||
rl_mid="${rl_lean}${seg_extra}"
|
rl_pace="${rl_lean}${seg_pace}"
|
||||||
rl_rich="$rl_mid"
|
|
||||||
|
|
||||||
# Formatting the two reset timestamps costs ~10 subprocesses (date has
|
# rl_time interleaves rather than appends: the 5h timestamp sits with
|
||||||
# no portable one-shot form here), so only pay for it when there is a
|
# its own bar, and the 7d one closes the weekly group it shares with
|
||||||
# chance they will be shown: appended to line one, or alone on line two.
|
# the per-model bar and the pace.
|
||||||
# RESET_COST is the combined width of " reset 4:40p.m." and
|
#
|
||||||
# " reset aug 28, 3:00a.m.".
|
# Formatting the pair costs ~10 subprocesses (date has no portable
|
||||||
line1_len=$(vis_len "$out")
|
# one-shot form here) on a line that redraws per keystroke, so it is
|
||||||
mid_len=$(vis_len "$rl_mid")
|
# skipped when line two provably cannot show them. RESET_COST
|
||||||
RESET_COST=30
|
# deliberately UNDER-estimates their combined width -- " ↺ 1:00am" and
|
||||||
if [ $(( line1_len + 3 + mid_len + RESET_COST )) -le "$USABLE_WIDTH" ] \
|
# " ↺ may 4, 1:00am" at their shortest -- so the ladder below, which
|
||||||
|| { $WRAP_NARROW && [ $(( mid_len + RESET_COST )) -le "$USABLE_WIDTH" ]; }; then
|
# 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
|
||||||
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
|
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
|
||||||
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
|
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
|
||||||
r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}"
|
r5=""; [ -n "$five_hour_reset" ] && r5=" ${dim}↺ ${five_hour_reset}${reset}"
|
||||||
r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}↺ ${seven_day_reset}${reset}"
|
r7=""; [ -n "$seven_day_reset" ] && r7=" ${dim}↺ ${seven_day_reset}${reset}"
|
||||||
rl_rich="${seg_5h}${r5}${seg_7d}${seg_scoped}${r7}${seg_extra}"
|
rl_time="${seg_5h}${r5}${seg_7d}${seg_scoped}${seg_pace}${r7}"
|
||||||
fi
|
fi
|
||||||
|
rl_rich="${rl_time}${seg_extra}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Attach the rate-limit group, emitting the richest layout that fits.
|
# Attach the rate-limit group on a line of its own, emitting the richest
|
||||||
|
# variant that fits it.
|
||||||
#
|
#
|
||||||
# A second line is worth it for the per-model bar and nothing else: rl_bare is
|
# Line two is never shared with line one's segments, however wide the terminal
|
||||||
# the only variant that drops that bar, and it is the whole reason the group is
|
# is. Squeezing the group onto line one is what used to cost the reset times
|
||||||
# worth showing on an account that has one. Reset times and extra-usage credits
|
# and the pace figure on a laptop-width window, and it made the layout jump
|
||||||
# are given up instead of wrapped for, so content is NOT monotone in width -- at
|
# between one and two lines as the branch name or the cost changed width.
|
||||||
# one column narrower, rung 3 stops fitting and the wrap that follows has room
|
# A full line to itself is worth more than the columns it wastes.
|
||||||
# for the timestamps as well. Only wrapping being switched off makes rl_bare the
|
#
|
||||||
# answer.
|
# The extra-usage credits ride on the richest rung alone, so an account with
|
||||||
# 1-3. one line: with resets / with extra usage / bars + per-model
|
# overage enabled sees them only on a terminal wide enough for the whole group
|
||||||
# 4-6. two lines: same order, line two having room the single line lacked
|
# -- around 110 columns. Below that the reset times win the space: they are the
|
||||||
# 7. one line, bars only -- WRAP_NARROW=false, the least-bad single line
|
# number you act on, the credits the one you read afterwards.
|
||||||
# Every branch is fit-checked, so no layout is chosen that would be clipped by
|
#
|
||||||
# the renderer, whatever the branch name, cwd, model name or extra-usage width.
|
# 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.
|
||||||
if [ -n "$rl_bare" ]; then
|
if [ -n "$rl_bare" ]; then
|
||||||
rich_len=$(vis_len "$rl_rich")
|
if [ "$(vis_len "$rl_rich")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich"
|
||||||
lean_len=$(vis_len "$rl_lean")
|
elif [ "$(vis_len "$rl_time")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_time"
|
||||||
if [ $(( line1_len + 3 + rich_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_rich}"
|
elif [ "$pace_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_pace"
|
||||||
elif [ $(( line1_len + 3 + mid_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_mid}"
|
elif [ "$(vis_len "$rl_lean")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean"
|
||||||
elif [ $(( line1_len + 3 + lean_len )) -le "$USABLE_WIDTH" ]; then out+="${sep}${rl_lean}"
|
elif [ "$(vis_len "$rl_bare")" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_bare"
|
||||||
elif $WRAP_NARROW; then
|
|
||||||
if [ "$rich_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_rich"
|
|
||||||
elif [ "$mid_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_mid"
|
|
||||||
elif [ "$lean_len" -le "$USABLE_WIDTH" ]; then out+=$'\n'"$rl_lean"
|
|
||||||
else out+=$'\n'"$rl_bare"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
out+="${sep}${rl_bare}"
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ status=0
|
||||||
for t in statusline.isaacaudet.payload_test.sh \
|
for t in statusline.isaacaudet.payload_test.sh \
|
||||||
statusline.isaacaudet.thinking_test.sh \
|
statusline.isaacaudet.thinking_test.sh \
|
||||||
statusline.isaacaudet.width_test.sh \
|
statusline.isaacaudet.width_test.sh \
|
||||||
|
statusline.isaacaudet.pace_test.sh \
|
||||||
statusline.isaacaudet.cache_test.sh; do
|
statusline.isaacaudet.cache_test.sh; do
|
||||||
echo "== $t"
|
echo "== $t"
|
||||||
bash "$t" || status=1
|
bash "$t" || status=1
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,13 @@ out=$(render 245)
|
||||||
|| bad "30min old: refetched, not held for the hour" "$out"
|
|| bad "30min old: refetched, not held for the hour" "$out"
|
||||||
|
|
||||||
# ------------------------------------------------- bars shown => bars refreshed
|
# ------------------------------------------------- bars shown => bars refreshed
|
||||||
# The refresh is gated on the same tier check as the bars themselves, so the
|
# The refresh is gated on the same width check as the bars themselves, so the
|
||||||
# two must agree at every width: wrapped onto line two included, and 68 being
|
# two must agree at every width, 35 being RL_MIN_WIDTH -- the narrowest usable
|
||||||
# WRAP_FLOOR, the narrowest width that still shows them.
|
# width the bars still fit on their own line.
|
||||||
echo
|
echo
|
||||||
echo "Every width that shows the bars refreshes them:"
|
echo "Every width that shows the bars refreshes them:"
|
||||||
|
|
||||||
for u in 245 115 85 68; do
|
for u in 245 115 85 68 50 35; do
|
||||||
cached_response_aged 360
|
cached_response_aged 360
|
||||||
out=$(render "$u")
|
out=$(render "$u")
|
||||||
if case "$out" in *"5h"*) true ;; *) false ;; esac; then
|
if case "$out" in *"5h"*) true ;; *) false ;; esac; then
|
||||||
|
|
@ -141,14 +141,14 @@ for u in 245 115 85 68; do
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# One column below the wrap floor the group is dropped entirely — nothing is
|
# One column below RL_MIN_WIDTH the group is dropped entirely — nothing is
|
||||||
# shown, so nothing should be paid for either.
|
# shown, so nothing should be paid for either.
|
||||||
cached_response_aged 360
|
cached_response_aged 360
|
||||||
out=$(render 67)
|
out=$(render 34)
|
||||||
case "$out" in *"5h"*) bad "usable 67: no bars at the narrow tier" "$out" ;;
|
case "$out" in *"5h"*) bad "usable 34: no bars below the group's floor" "$out" ;;
|
||||||
*) ok "usable 67: no bars at the narrow tier" ;; esac
|
*) ok "usable 34: no bars below the group's floor" ;; esac
|
||||||
[ "$(calls)" = "0" ] && ok "usable 67: no API call when no bars are shown" \
|
[ "$(calls)" = "0" ] && ok "usable 34: no API call when no bars are shown" \
|
||||||
|| bad "usable 67: no API call when no bars are shown" "$out"
|
|| bad "usable 34: no API call when no bars are shown" "$out"
|
||||||
|
|
||||||
# --------------------------------------------------------- offline retry backoff
|
# --------------------------------------------------------- offline retry backoff
|
||||||
# With no network, curl can burn --max-time 10 before giving up. A failed fetch
|
# With no network, curl can burn --max-time 10 before giving up. A failed fetch
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@ SCRIPT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))
|
||||||
BRANCHES = ["main", "feature/some-longer-branch-name",
|
BRANCHES = ["main", "feature/some-longer-branch-name",
|
||||||
"feature/an-extremely-long-branch-name-that-keeps-going-and-going"]
|
"feature/an-extremely-long-branch-name-that-keeps-going-and-going"]
|
||||||
MODELS = ["Opus 5", "Opus 5 (1M context)"]
|
MODELS = ["Opus 5", "Opus 5 (1M context)"]
|
||||||
WIDTHS = range(64, 245, 10)
|
# Starts at the narrowest terminal line ONE fits on with the longest branch
|
||||||
|
# below (measured: 44 usable columns, i.e. 49 with padding 2) -- the pre-existing
|
||||||
|
# narrow-tier floor, which width_test.sh marks XFAIL. Everything above it is
|
||||||
|
# fair game, and that now includes the band from RL_MIN_WIDTH up, where the
|
||||||
|
# usage group is shown but has few columns to spare.
|
||||||
|
WIDTHS = range(49, 245, 10)
|
||||||
# The cwd basename is rendered at the widest layouts and is capped by
|
# The cwd basename is rendered at the widest layouts and is capped by
|
||||||
# CWD_MAX_LEN; vary it, since a long project directory was one of the ways
|
# CWD_MAX_LEN; vary it, since a long project directory was one of the ways
|
||||||
# line one used to overflow.
|
# line one used to overflow.
|
||||||
|
|
|
||||||
266
claude/tests/statusline.isaacaudet.pace_test.sh
Executable file
266
claude/tests/statusline.isaacaudet.pace_test.sh
Executable file
|
|
@ -0,0 +1,266 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Tests for the sustainable-burn indicator (pace + trend) in
|
||||||
|
# claude/statusline.isaacaudet.sh -- the variant symlinked from
|
||||||
|
# ~/.claude/statusline.sh. The sibling statusline.burnrate.sh, which has a
|
||||||
|
# richer sleep-aware version of the same idea, is NOT covered here.
|
||||||
|
#
|
||||||
|
# pace answers "how much of the weekly limit can I spend per WORKING day and
|
||||||
|
# still land on 100% at the reset", so every expectation below depends on
|
||||||
|
# which day of the week "now" is. SL_NOW pins it; without that the assertions
|
||||||
|
# would pass or fail according to the day the suite happened to run.
|
||||||
|
#
|
||||||
|
# TZ is pinned too: pace counts LOCAL calendar days, so the local day a UTC
|
||||||
|
# reset timestamp falls in -- and therefore how many workdays are left -- is
|
||||||
|
# otherwise a property of the machine running the tests.
|
||||||
|
export TZ=UTC
|
||||||
|
|
||||||
|
SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh"
|
||||||
|
# Widths below are USABLE widths, converted to a TERM_WIDTH here: the script
|
||||||
|
# picks its layout from USABLE_WIDTH (columns minus padding on both sides minus
|
||||||
|
# a margin), so a raw TERM_WIDTH would land on a different rung of the ladder
|
||||||
|
# the moment statusLine.padding changed -- same convention as width_test.sh.
|
||||||
|
PAD=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null || echo 0)
|
||||||
|
[ "${PAD:-0}" -ge 0 ] 2>/dev/null || PAD=0
|
||||||
|
OVERHEAD=$(( 2 * PAD + 1 ))
|
||||||
|
|
||||||
|
export STATUSLINE_CACHE_DIR="$(mktemp -d)"
|
||||||
|
CACHE="$STATUSLINE_CACHE_DIR/statusline-usage-cache.json"
|
||||||
|
REPO="$(mktemp -d)"
|
||||||
|
SHIM="$(mktemp -d)"
|
||||||
|
cleanup() { rm -rf "$STATUSLINE_CACHE_DIR" "$REPO" "$SHIM"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
git -C "$REPO" init -q 2>/dev/null
|
||||||
|
git -C "$REPO" -c user.email=t@t -c user.name=t commit -q --allow-empty -m init 2>/dev/null
|
||||||
|
|
||||||
|
# Seed the usage cache. Without a fixture the first render treats the rate
|
||||||
|
# limits as live and curls /api/oauth/usage with the real OAuth token, which
|
||||||
|
# makes the test non-hermetic and sensitive to account state.
|
||||||
|
#
|
||||||
|
# $1 = seven_day utilization, $2 = seven_day resets_at (a JSON value, so `null`
|
||||||
|
# is expressible), $3 = the limits array, $4 = the extra_usage block.
|
||||||
|
EXTRA_OFF='{"is_enabled":false}'
|
||||||
|
EXTRA_ON='{"is_enabled":true,"monthly_limit":200000,"used_credits":123456,"utilization":62.0}'
|
||||||
|
fixture() {
|
||||||
|
cat > "$CACHE" <<JSON
|
||||||
|
{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"},
|
||||||
|
"seven_day":{"utilization":$1,"resets_at":$2},
|
||||||
|
"extra_usage":${4:-$EXTRA_OFF},
|
||||||
|
"limits":$3}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
# A weekly_scoped (per-model) limit: $1 = percent, $2 = resets_at JSON value.
|
||||||
|
scoped() {
|
||||||
|
printf '[{"kind":"weekly_scoped","group":"weekly","percent":%s,"resets_at":%s,
|
||||||
|
"scope":{"model":{"display_name":"Fable"}},"is_active":true}]' "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
at() { date -j -f '%Y-%m-%dT%H:%M:%S' "$1" +%s 2>/dev/null || date -d "$1" +%s; }
|
||||||
|
|
||||||
|
MON=$(at 2026-08-24T09:00:00) # Monday
|
||||||
|
WED=$(at 2026-08-26T09:00:00) # Wednesday
|
||||||
|
SAT=$(at 2026-08-29T09:00:00) # Saturday
|
||||||
|
R_MON_ISO='"2026-08-31T02:00:00+00:00"' # the following Monday, 2am: a fresh week at MON
|
||||||
|
R_WED_ISO='"2026-09-02T02:00:00+00:00"' # the Wednesday after that
|
||||||
|
|
||||||
|
# $1 = SL_NOW, $2 = SL_WORK_DAYS, $3 = usable width (default 200). ANSI stripped.
|
||||||
|
render() {
|
||||||
|
printf '%s' "{\"model\":{\"display_name\":\"Opus 5\"},\"cwd\":\"$REPO\",
|
||||||
|
\"cost\":{\"total_cost_usd\":0.5},\"context_window\":{\"context_window_size\":200000,
|
||||||
|
\"current_usage\":{\"input_tokens\":1000}}}" \
|
||||||
|
| SL_NOW="$1" SL_WORK_DAYS="$2" TERM_WIDTH="$(( ${3:-200} + OVERHEAD ))" \
|
||||||
|
PATH="$SHIM:$PATH" bash "$SCRIPT" 2>&1 | sed $'s/\033\[[0-9;]*m//g'
|
||||||
|
}
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
ok() { echo " PASS $1"; pass=$((pass+1)); }
|
||||||
|
bad() { echo " FAIL $1"; [ -n "$2" ] && printf '%s\n' "$2" | sed 's/^/ /'; fail=$((fail+1)); }
|
||||||
|
|
||||||
|
# $1 = rendered output, $2 = wanted substring, $3 = label
|
||||||
|
has() { case "$1" in *"$2"*) ok "$3";; *) bad "$3" "want '$2' in: $1";; esac; }
|
||||||
|
lacks() { case "$1" in *"$2"*) bad "$3" "unwanted '$2' in: $1";; *) ok "$3";; esac; }
|
||||||
|
|
||||||
|
# A curl that fails loudly, first on PATH for every render above. Nothing here
|
||||||
|
# should reach the network: the fixture cache is fresh, so an invocation means
|
||||||
|
# the test would otherwise be spending the user's real OAuth token.
|
||||||
|
cat > "$SHIM/curl" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
echo "curl invoked" >&2
|
||||||
|
: > "$SHIM/curl-was-called"
|
||||||
|
exit 1
|
||||||
|
EOF
|
||||||
|
chmod +x "$SHIM/curl"
|
||||||
|
|
||||||
|
echo "Pace against the per-model limit (SL_WORK_DAYS=12345, Mon-Fri):"
|
||||||
|
# Fresh week on a Monday: the workdays left are Mon-Fri, so the whole 100
|
||||||
|
# points spread over 5 days rather than the 7-day figure of 14.
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)"
|
||||||
|
o=$(render "$MON" 12345)
|
||||||
|
has "$o" "20%/d" "fresh week on a Monday over a 5-day week paces at 20%/d, not 14"
|
||||||
|
has "$o" "Fable ░░░░░░ 0% 20%/d" "pace sits inside the per-model segment, after its percentage"
|
||||||
|
|
||||||
|
# Mid-week, overspent. Wed 09:00 with the reset the following Mon 02:00:
|
||||||
|
# workdays left = Wed, Thu, Fri = 3, so pace = (100-50)/3 = 16.7 -> "17%/d".
|
||||||
|
# Elapsed workdays = Mon, Tue = 2 of the window's 5, so expected = 40% and the
|
||||||
|
# trend is 50 - 40 = +10.
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 50 null)"
|
||||||
|
o=$(render "$WED" 12345)
|
||||||
|
has "$o" "17%/d" "mid-window Wednesday at 50% used paces at (100-50)/3 = 17%/d"
|
||||||
|
|
||||||
|
# Saturday is not a workday, so today does not count itself. Reset on the
|
||||||
|
# Wednesday: workdays left = Mon, Tue = 2.
|
||||||
|
fixture 7.0 "$R_WED_ISO" "$(scoped 40 null)"
|
||||||
|
o=$(render "$SAT" 12345)
|
||||||
|
has "$o" "30%/d" "a Saturday does not count itself: pace spreads over Mon+Tue only"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Trend against today's band, not against a point:"
|
||||||
|
# During workday n of m, anything between (n-1)/m and n/m of the budget is on
|
||||||
|
# track -- the whole of today's allowance is today's to spend. Comparing
|
||||||
|
# against a single point built from COMPLETE workdays only turned every Monday
|
||||||
|
# morning red as soon as 3 points had gone, which is what this replaces.
|
||||||
|
# $1 = used%, $2 = SL_NOW, $3 = reset iso, $4 = wanted token, $5 = label
|
||||||
|
trend_is() {
|
||||||
|
fixture 7.0 "$3" "$(scoped "$1" null)"
|
||||||
|
has "$(render "$2" 12345)" "$4" "$5"
|
||||||
|
}
|
||||||
|
# Monday of a fresh window is workday 1 of 5: the band is 0-20.
|
||||||
|
trend_is 6 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, 6% used: inside the day's own band"
|
||||||
|
trend_is 0 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, nothing used yet: on track"
|
||||||
|
trend_is 20 "$MON" "$R_MON_ISO" "%/d ✓" "workday 1 of 5, the whole day's allowance spent: still on track"
|
||||||
|
trend_is 30 "$MON" "$R_MON_ISO" "▲+10" "workday 1 of 5, 30% used: 10 points past the band top of 20"
|
||||||
|
# Wednesday with the reset the following Monday is workday 3 of 5: band 40-60.
|
||||||
|
trend_is 45 "$WED" "$R_MON_ISO" "%/d ✓" "workday 3 of 5, 45% used: inside the 40-60 band"
|
||||||
|
trend_is 70 "$WED" "$R_MON_ISO" "▲+10" "workday 3 of 5, 70% used: 10 points past the band top"
|
||||||
|
trend_is 30 "$WED" "$R_MON_ISO" "▼-10" "workday 3 of 5, 30% used: 10 points short of the band floor"
|
||||||
|
# The +-3 slack sits OUTSIDE the band, so the first flagged point is 4 past it.
|
||||||
|
trend_is 63 "$WED" "$R_MON_ISO" "%/d ✓" "3 points past the band top is still within the slack"
|
||||||
|
trend_is 64 "$WED" "$R_MON_ISO" "▲+4" "4 points past the band top breaks the slack"
|
||||||
|
trend_is 37 "$WED" "$R_MON_ISO" "%/d ✓" "3 points below the band floor is still within the slack"
|
||||||
|
trend_is 36 "$WED" "$R_MON_ISO" "▼-4" "4 points below the band floor breaks the slack"
|
||||||
|
# On a non-workday no day's allowance is in play, so the band collapses to the
|
||||||
|
# point where the elapsed workdays leave it. Saturday with the reset on the
|
||||||
|
# Wednesday: Wed, Thu, Fri elapsed of 5, so the point is 60.
|
||||||
|
trend_is 40 "$SAT" "$R_WED_ISO" "▼-20" "a non-workday collapses the band to a point and still trends"
|
||||||
|
trend_is 58 "$SAT" "$R_WED_ISO" "%/d ✓" "a non-workday within the slack of that point is on track"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Degenerate windows:"
|
||||||
|
# Saturday with the reset on Monday 02:00 leaves NO workdays at all. The
|
||||||
|
# workdays-left clamp makes pace the whole remaining budget, which is the
|
||||||
|
# honest answer (nothing constrains you) rather than a division by zero.
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 7 null)"
|
||||||
|
o=$(render "$SAT" 12345)
|
||||||
|
has "$o" "93%/d" "no workdays left: pace is everything that remains"
|
||||||
|
p=$(printf '%s' "$o" | sed -n 's/.* \([0-9.]*\)%\/d.*/\1/p')
|
||||||
|
case "$p" in
|
||||||
|
''|*[!0-9.]*) bad "the pace on a workday-less window is a plain number" "got '$p'" ;;
|
||||||
|
*) awk -v p="$p" 'BEGIN{exit !(p >= 0 && p <= 100)}' \
|
||||||
|
&& ok "the pace on a workday-less window stays within 0-100" \
|
||||||
|
|| bad "the pace on a workday-less window stays within 0-100" "got '$p'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# A 7-day working week is the old behaviour: 100/7 = 14.3, which the shared
|
||||||
|
# pv formatting (one decimal only below 10) renders as an integer.
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)"
|
||||||
|
o=$(render "$MON" 1234567)
|
||||||
|
has "$o" "14%/d" "SL_WORK_DAYS=1234567 reproduces the plain 100/7 figure"
|
||||||
|
|
||||||
|
# Nonsense SL_WORK_DAYS leaves no workdays in the window at all, so there is
|
||||||
|
# no baseline to pace against and nothing to render.
|
||||||
|
o=$(render "$MON" "xyz")
|
||||||
|
lacks "$o" "%/d" "an SL_WORK_DAYS with no weekdays in it renders no pace"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Which limit pace is computed against:"
|
||||||
|
# The per-model limit is the one that bites, so its own resets_at wins over the
|
||||||
|
# seven_day one even when the two disagree.
|
||||||
|
fixture 7.0 "\"not-a-date\"" "$(scoped 0 "$R_MON_ISO")"
|
||||||
|
o=$(render "$MON" 12345)
|
||||||
|
has "$o" "20%/d" "the scoped limit's own resets_at is used when it has one"
|
||||||
|
|
||||||
|
# No per-model limit: fall back to the 7-day figure, and put pace where the
|
||||||
|
# per-model segment would have been. 20% used over 5 workdays -> 80/5 = 16.
|
||||||
|
fixture 20.0 "$R_MON_ISO" "[]"
|
||||||
|
o=$(render "$MON" 12345)
|
||||||
|
has "$o" "16%/d" "with no per-model limit pace falls back to the 7-day figure"
|
||||||
|
lacks "$o" "Fable" "the fallback really has no per-model segment"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Missing and unusable reset times:"
|
||||||
|
# Every one of these must render the rest of the status line untouched: no
|
||||||
|
# pace, no trend, and no separator left dangling where they would have been.
|
||||||
|
no_pace() { # $1 = output, $2 = label
|
||||||
|
lacks "$1" "%/d" "$2: no pace"
|
||||||
|
lacks "$1" "│ │" "$2: no doubled separator"
|
||||||
|
case "$1" in
|
||||||
|
*'│') bad "$2: no trailing separator" "$1" ;;
|
||||||
|
*) ok "$2: no trailing separator" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
fixture 7.0 null "$(scoped 0 null)"
|
||||||
|
no_pace "$(render "$MON" 12345)" "an absent reset time"
|
||||||
|
fixture 7.0 "\"not-a-date\"" "$(scoped 0 "\"also-not-a-date\"")"
|
||||||
|
no_pace "$(render "$MON" 12345)" "an unparseable reset time"
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)"
|
||||||
|
no_pace "$(render "$(at 2026-09-10T09:00:00)" 12345)" "a reset time already in the past"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Width ladder (pace must never be the reason a line overflows):"
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)"
|
||||||
|
widths() { python3 -c "
|
||||||
|
import sys,re,unicodedata
|
||||||
|
for ln in sys.stdin.read().rstrip('\n').split('\n'):
|
||||||
|
p=re.sub(r'\033\[[0-9;]*m','',ln)
|
||||||
|
print(sum(2 if unicodedata.east_asian_width(c) in 'WF' else 1 for c in p))"; }
|
||||||
|
over=0; toomany=0; orphan=0; dropped=0; shown=0; missing_wide=0
|
||||||
|
for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68 60 55 50 45 40; do
|
||||||
|
o=$(render "$MON" 12345 "$u")
|
||||||
|
ws=$(printf '%s' "$o" | widths)
|
||||||
|
while read -r c; do [ "$c" -gt "$u" ] && over=1; done <<< "$ws"
|
||||||
|
[ "$(printf '%s\n' "$ws" | wc -l | tr -d ' ')" -gt 2 ] && toomany=1
|
||||||
|
# pace is given up BEFORE the per-model bar it annotates, so it can never
|
||||||
|
# be the last thing standing.
|
||||||
|
case "$o" in
|
||||||
|
*"%/d"*) shown=1; case "$o" in *Fable*) ;; *) orphan=1 ;; esac ;;
|
||||||
|
*) case "$o" in *Fable*) dropped=1 ;; esac
|
||||||
|
[ "$u" -ge 68 ] && missing_wide=1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ "$over" = 0 ] && ok "no line exceeds the usable width at any width" || bad "no line exceeds the usable width at any width"
|
||||||
|
[ "$toomany" = 0 ] && ok "never more than two lines" || bad "never more than two lines"
|
||||||
|
[ "$orphan" = 0 ] && ok "pace never outlives the per-model bar" || bad "pace never outlives the per-model bar"
|
||||||
|
[ "$shown" = 1 ] && ok "pace is actually rendered somewhere in the sweep" || bad "pace is actually rendered somewhere in the sweep"
|
||||||
|
# Line two is the group's own, so pace survives every width that fits a normal
|
||||||
|
# status line -- it is only given up on a genuinely tiny terminal.
|
||||||
|
[ "$missing_wide" = 0 ] && ok "pace is kept at every width from 68 columns up" \
|
||||||
|
|| bad "pace is kept at every width from 68 columns up"
|
||||||
|
[ "$dropped" = 1 ] && ok "the pace rung is reachable: some width keeps the bar but drops pace" \
|
||||||
|
|| bad "the pace rung is reachable: some width keeps the bar but drops pace"
|
||||||
|
|
||||||
|
# Pace outranks the extra-usage credits: the credits are ~30 columns of dollar
|
||||||
|
# readout, pace is the number this segment exists for, so the credits go first.
|
||||||
|
fixture 7.0 "$R_MON_ISO" "$(scoped 0 null)" "$EXTRA_ON"
|
||||||
|
pace_alone=0; credits_alone=0
|
||||||
|
for u in 200 180 160 155 150 145 140 135 130 125 120 116 110 105 100 95 90 85 80 75 70 68 60 55 50 45 40; do
|
||||||
|
o=$(render "$MON" 12345 "$u")
|
||||||
|
case "$o" in
|
||||||
|
*"%/d"*) case "$o" in *extra*) ;; *) pace_alone=1 ;; esac ;;
|
||||||
|
*) case "$o" in *extra*) credits_alone=1 ;; esac ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ "$credits_alone" = 0 ] && ok "the extra-usage credits never outlive pace" \
|
||||||
|
|| bad "the extra-usage credits never outlive pace"
|
||||||
|
[ "$pace_alone" = 1 ] && ok "some width keeps pace but drops the credits" \
|
||||||
|
|| bad "some width keeps pace but drops the credits"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Hermeticity:"
|
||||||
|
[ -f "$SHIM/curl-was-called" ] \
|
||||||
|
&& bad "curl is never invoked (the fixture cache is fresh)" \
|
||||||
|
|| ok "curl is never invoked (the fixture cache is fresh)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " $pass passed, $fail failed"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
|
|
@ -68,13 +68,25 @@ out=$(render 250)
|
||||||
case "$out" in *"Fable"*"10%"*) ok "wide: renders 'Fable' with its percentage" ;;
|
case "$out" in *"Fable"*"10%"*) ok "wide: renders 'Fable' with its percentage" ;;
|
||||||
*) bad "wide: renders 'Fable' with its percentage" "$out" ;; esac
|
*) bad "wide: renders 'Fable' with its percentage" "$out" ;; esac
|
||||||
|
|
||||||
# Wrapped: assert Fable is on line TWO specifically, not merely present.
|
# The usage group always gets a line of its own, however wide the terminal is.
|
||||||
out=$(render 90)
|
for w in 250 116 90; do
|
||||||
[ "$(count "$out")" = "2" ] && ok "narrow: wraps to two lines" || bad "narrow: wraps to two lines" "$out"
|
out=$(render "$w")
|
||||||
case "$(nth "$out" 2)" in *"Fable"*"10%"*) ok "narrow: Fable is on line two" ;;
|
[ "$(count "$out")" = "2" ] && ok "width $w: usage is on its own line" \
|
||||||
*) bad "narrow: Fable is on line two" "$out" ;; esac
|
|| bad "width $w: usage is on its own line" "$out"
|
||||||
case "$(nth "$out" 1)" in *"Fable"*) bad "narrow: Fable not duplicated on line one" "$out" ;;
|
case "$(nth "$out" 2)" in *"Fable"*"10%"*) ok "width $w: Fable is on line two" ;;
|
||||||
*) ok "narrow: Fable not duplicated on line one" ;; esac
|
*) bad "width $w: Fable is on line two" "$out" ;; esac
|
||||||
|
case "$(nth "$out" 1)" in *"Fable"*) bad "width $w: Fable not duplicated on line one" "$out" ;;
|
||||||
|
*) ok "width $w: Fable not duplicated on line one" ;; esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# A line of its own is what buys room for the reset times: both the 5-hour
|
||||||
|
# clock time and the 7-day date survive down to a laptop-sized terminal, where
|
||||||
|
# they used to be the first thing given up.
|
||||||
|
for w in 250 116 100; do
|
||||||
|
out=$(render "$w")
|
||||||
|
case "$(nth "$out" 2)" in *"↺"*"↺"*) ok "width $w: both reset times are shown" ;;
|
||||||
|
*) bad "width $w: both reset times are shown" "$out" ;; esac
|
||||||
|
done
|
||||||
|
|
||||||
# The per-model bar is the reason the group exists on a Fable-capable account,
|
# The per-model bar is the reason the group exists on a Fable-capable account,
|
||||||
# so it must survive by wrapping and never be dropped to squeeze the group onto
|
# so it must survive by wrapping and never be dropped to squeeze the group onto
|
||||||
|
|
@ -105,7 +117,7 @@ for desc in "no limits key:{$BASE,\"extra_usage\":{\"is_enabled\":false}}" \
|
||||||
name="${desc%%:*}"; json="${desc#*:}"
|
name="${desc%%:*}"; json="${desc#*:}"
|
||||||
printf '%s' "$json" | fixture
|
printf '%s' "$json" | fixture
|
||||||
out=$(render 250)
|
out=$(render 250)
|
||||||
if [ "$(count "$out")" = "1" ] && case "$out" in *"5h"*"7d"*) true;; *) false;; esac \
|
if [ "$(count "$out")" = "2" ] && case "$(nth "$out" 2)" in *"5h"*"7d"*) true;; *) false;; esac \
|
||||||
&& case "$out" in *error*|*null*|*"jq:"*|*"line "*) false;; *) true;; esac; then
|
&& case "$out" in *error*|*null*|*"jq:"*|*"line "*) false;; *) true;; esac; then
|
||||||
ok "$name"
|
ok "$name"
|
||||||
else bad "$name" "$out"; fi
|
else bad "$name" "$out"; fi
|
||||||
|
|
@ -139,33 +151,17 @@ fixture <<JSON
|
||||||
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"A\nB"}},"is_active":true}]}
|
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"A\nB"}},"is_active":true}]}
|
||||||
JSON
|
JSON
|
||||||
out=$(render 250)
|
out=$(render 250)
|
||||||
[ "$(count "$out")" = "1" ] && ok "newline in display_name does not split the line" \
|
[ "$(count "$out")" = "2" ] && ok "newline in display_name does not split the line" \
|
||||||
|| bad "newline in display_name does not split the line" "$out"
|
|| bad "newline in display_name does not split the line" "$out"
|
||||||
fixture <<JSON
|
fixture <<JSON
|
||||||
{$BASE,"extra_usage":{"is_enabled":false},"limits":[]}
|
{$BASE,"extra_usage":{"is_enabled":false},"limits":[]}
|
||||||
JSON
|
JSON
|
||||||
mkdir -p "$REPO/sub"
|
mkdir -p "$REPO/sub"
|
||||||
out=$(render 250 "$REPO/pro\\nj")
|
out=$(render 250 "$REPO/pro\\nj")
|
||||||
[ "$(count "$out")" = "1" ] && ok "literal backslash-n in cwd does not split the line" \
|
[ "$(count "$out")" = "2" ] && ok "literal backslash-n in cwd does not split the line" \
|
||||||
|| bad "literal backslash-n in cwd does not split the line" "$out"
|
|| bad "literal backslash-n in cwd does not split the line" "$out"
|
||||||
out=$(render 250 "$REPO" 'Opus\n5')
|
out=$(render 250 "$REPO" 'Opus\n5')
|
||||||
[ "$(count "$out")" = "1" ] && ok "literal backslash-n in model name does not split the line" \
|
[ "$(count "$out")" = "2" ] && ok "literal backslash-n in model name does not split the line" \
|
||||||
|| bad "literal backslash-n in model name does not split the line" "$out"
|
|| bad "literal backslash-n in model name does not split the line" "$out"
|
||||||
|
|
||||||
# --------------------------------------------- WRAP_NARROW=false (regression §2)
|
|
||||||
echo "With WRAP_NARROW=false the single line must still be fit-checked:"
|
|
||||||
NOWRAP=$(mktemp); sed 's/^WRAP_NARROW=true/WRAP_NARROW=false/' "$SCRIPT" > "$NOWRAP"
|
|
||||||
fixture <<JSON
|
|
||||||
{$BASE,"extra_usage":{"is_enabled":false},
|
|
||||||
"limits":[{"kind":"weekly_scoped","percent":10,"scope":{"model":{"display_name":"Fable"}},"is_active":true}]}
|
|
||||||
JSON
|
|
||||||
for w in 81 105 130; do
|
|
||||||
out=$(stdin_json "$REPO" "Opus 5" | TERM_WIDTH=$w bash "$NOWRAP" 2>&1 | sed $'s/\033\[[0-9;]*m//g')
|
|
||||||
u=$(( w - 5 )); c=$(vis "$out")
|
|
||||||
[ "$(count "$out")" = "1" ] && [ "$c" -le "$u" ] \
|
|
||||||
&& ok "width $w: one line, $c <= usable $u" \
|
|
||||||
|| bad "width $w: one line within usable $u (got $(count "$out") line/s, $c cols)" "$out"
|
|
||||||
done
|
|
||||||
rm -f "$NOWRAP"
|
|
||||||
|
|
||||||
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]
|
echo; echo "pass=$pass fail=$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,10 @@ echo "Fallbacks and tiers:"
|
||||||
has true "" "◆ thinking" 200 "absent effort falls back to the old label"
|
has true "" "◆ thinking" 200 "absent effort falls back to the old label"
|
||||||
has true garbage "◆ thinking" 200 "an unrecognised level falls back rather than widening the line"
|
has true garbage "◆ thinking" 200 "an unrecognised level falls back rather than widening the line"
|
||||||
has true high "◆ high" 120 "the label survives the wide tier"
|
has true high "◆ high" 120 "the label survives the wide tier"
|
||||||
# The split tier (76-99) is unreachable while WRAP_NARROW=true -- that band is
|
# Below 68 columns the narrow tier drops the segment entirely; between there
|
||||||
# overridden to wide and wrapped -- so only narrow is asserted here.
|
# and 100 the compact tier keeps the label, since the usage group has moved off
|
||||||
|
# line one and left room for it.
|
||||||
|
has true high "◆ high" 85 "the label survives the compact tier"
|
||||||
lacks true high "◆" 60 "narrow tier hides the segment"
|
lacks true high "◆" 60 "narrow tier hides the segment"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,13 @@ check() { # usable, branch, model, cost, [xfail-reason]
|
||||||
[ "$over" = "1" ] && why="line exceeds usable width"
|
[ "$over" = "1" ] && why="line exceeds usable width"
|
||||||
# INVARIANT 2: at most two lines
|
# INVARIANT 2: at most two lines
|
||||||
[ "$n" -gt 2 ] && why="${why:-more than two lines}"
|
[ "$n" -gt 2 ] && why="${why:-more than two lines}"
|
||||||
# INVARIANT 3: only wrap when it was actually necessary
|
# INVARIANT 3: the usage group always gets a line of its own, so that the
|
||||||
if [ -z "$why" ] && [ "$n" = "2" ]; then
|
# reset times and the pace figure have room whatever line one holds. Below
|
||||||
local l1 l2; l1=$(printf '%s\n' "$ws" | sed -n 1p); l2=$(printf '%s\n' "$ws" | sed -n 2p)
|
# RL_MIN_WIDTH there is no group to wrap, so a single line is expected.
|
||||||
[ $(( l1 + 3 + l2 )) -le "$u" ] && why="wrapped unnecessarily (would have fit on one line)"
|
if [ -z "$why" ]; then
|
||||||
|
if [ "$u" -ge 35 ] && [ "$n" != "2" ]; then why="usage not on its own line"
|
||||||
|
elif [ "$u" -lt 35 ] && [ "$n" != "1" ]; then why="wrapped with no usage group"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local label="usable=$u branch=${#branch}ch model='${model:0:12}' lines=$n max=$mx"
|
local label="usable=$u branch=${#branch}ch model='${model:0:12}' lines=$n max=$mx"
|
||||||
|
|
@ -75,7 +78,7 @@ M1="Opus 5"
|
||||||
M2="Opus 5 (1M context)"
|
M2="Opus 5 (1M context)"
|
||||||
|
|
||||||
echo "Sweep of usable widths, short branch:"
|
echo "Sweep of usable widths, short branch:"
|
||||||
for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 40; do check "$u" "$SHORT" "$M1" 0.50; done
|
for u in 200 155 150 130 116 110 100 95 85 75 68 67 60 50 40; do check "$u" "$SHORT" "$M1" 0.50; done
|
||||||
# Narrow tier has a ~35-col floor; nothing can fit below that. Verified byte-identical
|
# Narrow tier has a ~35-col floor; nothing can fit below that. Verified byte-identical
|
||||||
# on the pre-change script, so not a regression.
|
# on the pre-change script, so not a regression.
|
||||||
check 25 "$SHORT" "$M1" 0.50 "narrow tier floor ~35 cols"
|
check 25 "$SHORT" "$M1" 0.50 "narrow tier floor ~35 cols"
|
||||||
|
|
@ -86,7 +89,7 @@ for u in 200 150 116 100 85 68; do check "$u" "$LONG" "$M2" 4.61; done
|
||||||
check 40 "$LONG" "$M2" 4.61 "narrow tier floor ~43 cols with a long branch"
|
check 40 "$LONG" "$M2" 4.61 "narrow tier floor ~43 cols with a long branch"
|
||||||
echo "Large cost figure:"
|
echo "Large cost figure:"
|
||||||
for u in 116 85 68; do check "$u" "$SHORT" "$M1" 1234.56; done
|
for u in 116 85 68; do check "$u" "$SHORT" "$M1" 1234.56; done
|
||||||
# ... and with a long branch, which is what makes the wrap-mode branch budget
|
# ... and with a long branch, which is what makes the compact-tier branch budget
|
||||||
# bite: a four-digit cost is three columns wider than the budget assumed.
|
# bite: a four-digit cost is three columns wider than the budget assumed.
|
||||||
echo "Large cost figure with a long branch:"
|
echo "Large cost figure with a long branch:"
|
||||||
for u in 68 69 70 75 85; do check "$u" "$LONG" "$M1" 1234.56; done
|
for u in 68 69 70 75 85; do check "$u" "$LONG" "$M1" 1234.56; done
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue