Last active
July 21, 2026 09:43
-
-
Save cniska/ac63274bd6f641bf66d784d06975a24a to your computer and use it in GitHub Desktop.
Claude Code Statusline Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Claude Code statusline: dir[/subdir] · worktree · branch[*↑↓] · model[↓] effort · pct%/size · $cur[ $tot] · ↑tok-in ↓tok-out | |
| # dir is the repo name (cwd basename if not a repo); subdir/worktree/branch are optional. | |
| # In a repo, if cwd is below the working-tree root, the cwd basename rides dim on dir (proj/subdir). | |
| # The three fold left to right: a name already shown drops. | |
| # Input: JSON on stdin (see https://docs.claude.com/en/docs/claude-code/statusline) | |
| # | |
| # One function per segment, each reading the parsed globals and echoing its rendered | |
| # string (empty when there's nothing to show). main parses stdin, then joins the | |
| # non-empty segments. Sourcing the script (the test suite) defines the helpers and | |
| # returns before main runs, so each segment can be exercised in isolation. | |
| set -u | |
| # Pin numeric formatting to C: awk emits floats with a '.' decimal and bash printf reparses | |
| # them (cost segment), which disagree under a comma-decimal LC_NUMERIC. C keeps both sides | |
| # aligned regardless of the ambient locale. | |
| export LC_NUMERIC=C | |
| # Truecolor palette — One Dark dimmed ~15%, matched perceptual weight. | |
| # Safe states stay the default foreground (RESET); color signals only warning/danger. | |
| # DIM carries everything secondary — separators, effort, git ahead/behind, token | |
| # throughput, the total cost — so it recedes beneath the primary content. | |
| YELLOW=$'\033[38;2;195;163;105m' | |
| RED=$'\033[38;2;190;92;99m' | |
| DIM=$'\033[2m' | |
| RESET=$'\033[0m' | |
| SEP=" ${DIM}·${RESET} " | |
| # Where a segment turns amber, then red. Context is window occupancy (%): red on | |
| # the point good practice says to compact or clear, amber the approach. Cost is | |
| # spend since the last /clear ($). | |
| CONTEXT_YELLOW=50 CONTEXT_RED=60 | |
| COST_YELLOW=20 COST_RED=50 | |
| # Short count: 1234567 -> 1.2M, 1000000 -> 1M, 12345 -> 12k, 999 -> 999. Shared by | |
| # token throughput and the context window size so the two never drift. | |
| human() { | |
| local n=${1:-0} | |
| if [ "$n" -ge 1000000 ]; then awk -v n="$n" 'BEGIN{v=n/1000000; if (v==int(v)) printf "%dM", v; else printf "%.1fM", v}' | |
| elif [ "$n" -ge 1000 ]; then awk -v n="$n" 'BEGIN{printf "%dk", n/1000}' | |
| else printf '%d' "$n"; fi | |
| } | |
| # dir[ subdir] · worktree · branch, each optional, folded left to right (a name already | |
| # shown drops; a worktree usually shares its branch's name). dir is the repo name in a git | |
| # repo, else the cwd basename. When cwd is below the working-tree root, its basename rides | |
| # on dir as a dim suffix (proj sub), the same shape as model + effort. The branch carries a | |
| # dim suffix for dirty (*) and ahead/behind (↑↓). --no-optional-locks avoids fighting an | |
| # active git command's lock. | |
| seg_location() { | |
| local dir wt="" branch="" suffix="" arrows="" agd="" main_root="" ab="" subdir="" | |
| [ "$cwd" = "$HOME" ] && dir="~" || dir=$(basename "$cwd") | |
| if [ -n "$cwd" ] && git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then | |
| agd=$(git -C "$cwd" rev-parse --absolute-git-dir 2>/dev/null) | |
| case "$agd" in */worktrees/*) wt=$(basename "$agd") ;; esac | |
| main_root=${agd%/worktrees/*} # drop a linked-worktree tail (…/.git/worktrees/wt or …/proj.git/worktrees/wt) | |
| main_root=${main_root%/.git} # normal repo: strip the /.git gitdir | |
| main_root=${main_root%.git} # bare repo: strip the trailing .git (proj.git -> proj) | |
| # Keep the ~ marker when $HOME is itself a repo root; below it, show the repo name. | |
| [ -n "$main_root" ] && [ "$cwd" != "$HOME" ] && dir=$(basename "$main_root") | |
| subdir=$(git -C "$cwd" --no-optional-locks rev-parse --show-prefix 2>/dev/null) | |
| subdir=${subdir%/}; subdir=${subdir##*/} # cwd basename when below root, else empty | |
| branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null \ | |
| || git -C "$cwd" --no-optional-locks rev-parse --short HEAD 2>/dev/null) | |
| if [ -n "$branch" ]; then | |
| git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null | grep -q . && suffix="*" | |
| ab=$(git -C "$cwd" --no-optional-locks rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null) | |
| if [ -n "$ab" ]; then | |
| local behind ahead | |
| read -r behind ahead <<<"$ab" # rev-list --left-right --count: behind<TAB>ahead | |
| [ "${ahead:-0}" -gt 0 ] && arrows="↑${ahead}" | |
| [ "${behind:-0}" -gt 0 ] && arrows="${arrows:+$arrows }↓${behind}" | |
| [ -n "$arrows" ] && suffix="${suffix} ${arrows}" | |
| fi | |
| fi | |
| fi | |
| local out="" seen="" n seg | |
| for n in "$dir" "$wt" "$branch"; do | |
| [ -z "$n" ] && continue | |
| case " $seen " in *" $n "*) continue ;; esac | |
| seen="$seen $n" | |
| seg="$n" | |
| { [ "$n" = "$dir" ] && [ -n "$subdir" ]; } && seg="${seg} ${DIM}${subdir}${RESET}" | |
| { [ "$n" = "$branch" ] && [ -n "$suffix" ]; } && seg="${seg}${DIM}${suffix}${RESET}" | |
| [ -n "$out" ] && out="${out}${SEP}" | |
| out="${out}${seg}" | |
| done | |
| printf '%s' "$out" | |
| } | |
| # One forward pass over the transcript (JSONL) for the token segment: sum TOK_IN / TOK_OUT | |
| # (session throughput) across every usage line, sidechain included, since it all costs. | |
| # fromjson? drops a partial final line — a live session always has one — instead of failing | |
| # the whole parse. Leaves the defaults (0/0) without jq or a readable transcript. | |
| scan_transcript() { | |
| TOK_IN=0; TOK_OUT=0 | |
| [ -n "$transcript" ] && [ -f "$transcript" ] || return 0 | |
| command -v jq >/dev/null 2>&1 || return 0 | |
| local out | |
| out=$(jq -n -rRc ' | |
| reduce (inputs | fromjson?) as $l ({i:0,o:0}; | |
| if ($l.message.usage != null) then | |
| .i += (($l.message.usage.input_tokens // 0) + ($l.message.usage.cache_creation_input_tokens // 0)) | |
| | .o += ($l.message.usage.output_tokens // 0) | |
| else . end) | |
| | "\(.i) \(.o)"' "$transcript" 2>/dev/null) | |
| [ -n "$out" ] || return 0 | |
| TOK_IN=${out%% *}; TOK_OUT=${out##* } | |
| } | |
| # Model name with the "(1M context)"-style parenthetical stripped, effort/fast dim. | |
| # fast_mode wins the label since it's the deliberate toggle. | |
| # | |
| # `model` is the live, currently-selected model (stdin .model.display_name), which the | |
| # statusline docs define as the authoritative "current model" and which updates the | |
| # instant a mid-session /model switch happens. That is what we display. | |
| # | |
| # We deliberately do NOT override it with the transcript's last-served model. The stdin | |
| # JSON exposes no served-vs-selected signal, so the transcript is the only source of the | |
| # served model — but the transcript lags the selection: right after the user switches | |
| # (e.g. Sonnet→Opus) the last recorded turn is still the old model, and an override would | |
| # render the stale label (`Sonnet 4.6 ↓`) over the model the user just chose. Since a | |
| # genuine Opus→Sonnet cap fallback is byte-for-byte indistinguishable from that lag, we | |
| # cannot tell them apart from the data, and showing the wrong model over a correct | |
| # selection is the worse failure. Trust the selection. | |
| seg_model() { | |
| local m=${model%% (*} label="$effort" out | |
| [ "$fast_mode" = "true" ] && label="fast" | |
| [ -z "$m" ] && return | |
| out="$m" | |
| [ -n "$label" ] && out="$out ${DIM}${label}${RESET}" | |
| printf '%s' "$out" | |
| } | |
| # Context: pct over the window size (15%/1M), pct colored by threshold, size dim — the | |
| # size labels the segment, so no CTX tag is needed; fall back to one when size is | |
| # unknown. Older clients without context_window: derive pct from the transcript vs 200K. | |
| seg_context() { | |
| local p="$pct" size="$ctx_size" used color sz | |
| if [ -z "$p" ] && [ -n "$transcript" ] && [ -f "$transcript" ] && command -v jq >/dev/null 2>&1; then | |
| # Forward read (fromjson? drops the partial final line a live session always has); | |
| # take the last usage turn's occupancy. tail -r here would fuse that partial tail | |
| # into the prior line and corrupt the JSON, blanking the segment. | |
| used=$(jq -rRc 'fromjson? | select(.message.usage != null) | |
| | .message.usage | ((.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0))' \ | |
| "$transcript" 2>/dev/null | tail -1) | |
| [ -n "$used" ] && [ "$used" != "null" ] && [ "$used" -gt 0 ] 2>/dev/null && { p=$(( used * 100 / 200000 )); size=200000; } | |
| fi | |
| p=${p%.*}; [ -z "$p" ] && p=0; [ "$p" -gt 100 ] && p=100 | |
| if [ "$p" -ge "$CONTEXT_RED" ]; then color=$RED | |
| elif [ "$p" -ge "$CONTEXT_YELLOW" ]; then color=$YELLOW | |
| else color=$RESET; fi | |
| if [ "${size:-0}" -ge 1000 ] 2>/dev/null; then | |
| sz=$(human "$size"); printf '%s%s%%%s%s/%s%s' "$color" "$p" "$RESET" "$DIM" "$sz" "$RESET" | |
| else | |
| printf '%s%s%%%s CTX' "$color" "$p" "$RESET" | |
| fi | |
| } | |
| # Cost: current cost (spend since the last /clear) colored by threshold, with the | |
| # total cost (dim) appended only once a /clear has split the two. total_cost_usd is | |
| # cumulative over the CLI process; we reconstruct current by snapshotting it the first | |
| # time we see a transcript (a /clear starts a fresh one) under ~/.claude/cost-baselines, | |
| # keyed by session so parallel sessions never collide. | |
| # | |
| # Two "not ready" states show $0.00 untouched: | |
| # - total null/empty or <= 0: client hasn't made its first API call yet. | |
| # - transcript empty: session path unknown (brief window after /clear); without a keyed | |
| # baseline there is no valid delta, and falling through with base=0 would leak the | |
| # raw cumulative total as "session" cost. | |
| seg_cost() { | |
| local total="$cost_total" cur base="0" base_dir base_file color tier diverged | |
| awk -v t="${total:-}" 'BEGIN{exit !(t=="" || t+0<=0)}' && { printf '%s$%.2f%s' "$RESET" 0 "$RESET"; return; } | |
| [ -z "$transcript" ] && { printf '%s$%.2f%s' "$RESET" 0 "$RESET"; return; } | |
| base_dir="$HOME/.claude/cost-baselines"; base_file="$base_dir/$(basename "$transcript" .jsonl)" | |
| if [ -f "$base_file" ]; then | |
| base=$(cat "$base_file" 2>/dev/null) | |
| # Recapture when the stored baseline is unusable: a 0 poisoned by an earlier not-ready | |
| # capture (heal), or a stale-high value from a resumed session whose cost counter | |
| # restarted (clamp down, else the delta pins to 0 forever). | |
| awk -v t="$total" -v b="${base:-0}" 'BEGIN{exit !(b+0<=0 || t+0 < b+0)}' \ | |
| && { printf '%s' "$total" >"$base_file" 2>/dev/null; base="$total"; } | |
| else | |
| mkdir -p "$base_dir" 2>/dev/null; printf '%s' "$total" >"$base_file" 2>/dev/null; base="$total" | |
| find "$base_dir" -type f -mtime +7 -delete 2>/dev/null # rare path: prune stale baselines | |
| fi | |
| # One awk derives all three: current (total - baseline, floored at 0), the color tier, | |
| # and whether current and total have diverged enough to show both. | |
| read -r cur tier diverged < <(awk -v t="$total" -v b="${base:-0}" -v y="$COST_YELLOW" -v r="$COST_RED" 'BEGIN{ | |
| d=t-b; if(d<0)d=0; | |
| tier=(d>=r?"r":(d>=y?"y":"n")); | |
| div=((t-d<0?d-t:t-d)>=0.005)?1:0; | |
| printf "%.6f %s %d", d, tier, div}') | |
| case "$tier" in r) color=$RED ;; y) color=$YELLOW ;; *) color=$RESET ;; esac | |
| if [ "$diverged" = 1 ]; then | |
| printf '%s$%.2f%s %s$%.2f%s' "$color" "$cur" "$RESET" "$DIM" "$total" "$RESET" | |
| else | |
| printf '%s$%.2f%s' "$color" "$cur" "$RESET" | |
| fi | |
| } | |
| # Token throughput this session, from the scan_transcript pass: ↑ input (cache reads | |
| # excluded, so it's the new input processed, not the cheap re-reads), ↓ output. Resets | |
| # on /clear like the current cost, since a /clear starts a fresh transcript. | |
| seg_tokens() { | |
| { [ "${TOK_IN:-0}" -gt 0 ] 2>/dev/null || [ "${TOK_OUT:-0}" -gt 0 ] 2>/dev/null; } || return | |
| printf '%s↑%s ↓%s%s' "$DIM" "$(human "${TOK_IN:-0}")" "$(human "${TOK_OUT:-0}")" "$RESET" | |
| } | |
| # When sourced (by the test suite), stop here so only the helpers are defined. | |
| (return 0 2>/dev/null) && return | |
| # Single-pass parse: one jq emits every field as an @sh-quoted assignment we eval, | |
| # instead of spawning a jq per field. Defaults first so set -u stays safe if jq fails. | |
| input=$(cat) | |
| cwd=""; model=""; cost_total=""; transcript=""; pct=""; ctx_size=""; effort=""; fast_mode="false" | |
| TOK_IN=0; TOK_OUT=0 | |
| eval "$(jq -r ' | |
| @sh "cwd=\(.cwd // "")", | |
| @sh "model=\(.model.display_name // "")", | |
| @sh "cost_total=\(.cost.total_cost_usd // "")", | |
| @sh "transcript=\(.transcript_path // "")", | |
| @sh "pct=\(.context_window.used_percentage // "")", | |
| @sh "ctx_size=\(.context_window.context_window_size // "")", | |
| @sh "effort=\(.effort.level // "")", | |
| @sh "fast_mode=\(.fast_mode // false)" | |
| ' <<<"$input" 2>/dev/null)" | |
| # Scan the transcript once for token throughput, then collect every segment and join the | |
| # non-empty ones with the dim middot. | |
| scan_transcript | |
| segs=("$(seg_location)" "$(seg_model)" "$(seg_context)" "$(seg_cost)" "$(seg_tokens)") | |
| out="" | |
| for p in "${segs[@]}"; do | |
| [ -z "$p" ] && continue | |
| [ -n "$out" ] && out="${out}${SEP}" | |
| out="${out}${p}" | |
| done | |
| printf '%s' "$out" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Unit + behavior tests for statusline.sh. Pure bash, no external deps. | |
| # Run: ./statusline.test.sh (exit 0 = all pass, 1 = failures) | |
| # | |
| # human() is tested directly by sourcing the script (it returns early when | |
| # sourced). Everything else is black-box: feed JSON on stdin, assert the | |
| # rendered line. HOME is redirected to a temp dir so the cost-baseline cache | |
| # never touches the real one, and git cases run against throwaway repos. | |
| set -u | |
| HERE="$(cd "$(dirname "$0")" && pwd)" | |
| SL="$HERE/statusline.sh" | |
| ESC=$(printf '\033') | |
| pass=0; fail=0 | |
| strip(){ sed "s/${ESC}\[[0-9;]*m//g"; } # drop ANSI for visible-text asserts | |
| assert(){ # desc, got, want | |
| if [ "$2" = "$3" ]; then pass=$((pass+1)) | |
| else fail=$((fail+1)); printf 'FAIL %s\n want: [%s]\n got: [%s]\n' "$1" "$3" "$2"; fi | |
| } | |
| contains(){ # desc, haystack, needle | |
| case "$2" in *"$3"*) pass=$((pass+1)) ;; | |
| *) fail=$((fail+1)); printf 'FAIL %s\n expected to contain: [%s]\n in: [%s]\n' "$1" "$3" "$2" ;; esac | |
| } | |
| TMPHOME=$(mktemp -d); FIX="" | |
| trap 'rm -rf "$TMPHOME" ${FIX:+"$FIX"}' EXIT | |
| render(){ HOME="$TMPHOME" bash "$SL" <<<"$1" | strip; } # visible text | |
| render_raw(){ HOME="$TMPHOME" bash "$SL" <<<"$1"; } # with ANSI | |
| # human(): number formatting | |
| # Lowercase k, uppercase M, integer M drops the .0, sub-1000 stays raw. | |
| h(){ ( source "$SL" >/dev/null 2>&1; human "$1" ); } | |
| assert "human 0" "$(h 0)" "0" | |
| assert "human 999" "$(h 999)" "999" | |
| assert "human 1000" "$(h 1000)" "1k" | |
| assert "human 12345" "$(h 12345)" "12k" | |
| assert "human 200000" "$(h 200000)" "200k" | |
| assert "human 999999" "$(h 999999)" "999k" | |
| assert "human 1000000" "$(h 1000000)" "1M" | |
| assert "human 1200000" "$(h 1200000)" "1.2M" | |
| assert "human 1500000" "$(h 1500000)" "1.5M" | |
| assert "human 8116275" "$(h 8116275)" "8.1M" | |
| # model, context, cost (non-git cwd -> location falls back to basename) | |
| NOGIT="$TMPHOME/nogit"; mkdir -p "$NOGIT" | |
| base(){ printf '{"cwd":"%s","model":{"display_name":"%s"},"cost":{"total_cost_usd":%s}%s}' "$NOGIT" "$1" "$2" "$3"; } | |
| assert "model parenthetical stripped, ctx size, cost collapsed" \ | |
| "$(render "$(base 'Opus 4.8 (1M context)' 1.11 ',"context_window":{"used_percentage":5,"context_window_size":1000000}')")" \ | |
| 'nogit · Opus 4.8 · 5%/1M · $0.00' | |
| assert "effort appended to model" \ | |
| "$(render '{"cwd":"'"$NOGIT"'","model":{"display_name":"Opus 4.8"},"effort":{"level":"high"},"cost":{"total_cost_usd":0},"context_window":{"used_percentage":5,"context_window_size":1000000}}')" \ | |
| 'nogit · Opus 4.8 high · 5%/1M · $0.00' | |
| assert "fast_mode wins the effort label" \ | |
| "$(render '{"cwd":"'"$NOGIT"'","model":{"display_name":"Opus 4.8"},"effort":{"level":"high"},"fast_mode":true,"cost":{"total_cost_usd":0},"context_window":{"used_percentage":5,"context_window_size":1000000}}')" \ | |
| 'nogit · Opus 4.8 fast · 5%/1M · $0.00' | |
| assert "200k window" \ | |
| "$(render "$(base 'Sonnet 4.6' 0 ',"context_window":{"used_percentage":42,"context_window_size":200000}')")" \ | |
| 'nogit · Sonnet 4.6 · 42%/200k · $0.00' | |
| assert "no window size falls back to CTX tag" \ | |
| "$(render "$(base 'X' 0 '')")" \ | |
| 'nogit · X · 0% CTX · $0.00' | |
| assert "pct over 100 clamps" \ | |
| "$(render "$(base 'X' 0 ',"context_window":{"used_percentage":150,"context_window_size":1000000}')")" \ | |
| 'nogit · X · 100%/1M · $0.00' | |
| assert "fractional pct truncates to whole" \ | |
| "$(render "$(base 'X' 0 ',"context_window":{"used_percentage":7.8,"context_window_size":1000000}')")" \ | |
| 'nogit · X · 7%/1M · $0.00' | |
| # context fallback: older clients omit context_window, so pct is derived from the last | |
| # usage turn's occupancy over a 200K window. A live transcript's unterminated final line | |
| # must not corrupt the read — forward fromjson?, not tail -r (which fused the partial tail | |
| # into the prior line and blanked the segment). cache_read-only keeps the token segment | |
| # silent so this isolates the occupancy path. | |
| mkdir -p "$TMPHOME/cx" | |
| { printf '%s\n' '{"message":{"usage":{"cache_read_input_tokens":50000}}}' | |
| printf '%s' '{"message":{"partial'; } > "$TMPHOME/cx/c.jsonl" | |
| assert "context fallback derives pct over 200K, survives a partial final line" \ | |
| "$(render '{"cwd":"'"$NOGIT"'","model":{"display_name":"X"},"cost":{"total_cost_usd":0},"transcript_path":"'"$TMPHOME"'/cx/c.jsonl"}')" \ | |
| 'nogit · X · 25%/200k · $0.00' | |
| # model display: the segment always shows the live selected model (.model.display_name), | |
| # which the docs define as the authoritative current model and which reflects a /model | |
| # switch immediately. The transcript's last-served model must NOT override it — the | |
| # stdin JSON gives no served-vs-selected signal, so a stale transcript turn (e.g. Sonnet | |
| # recorded just before the user switched to Opus) is indistinguishable from an Opus→Sonnet | |
| # cap, and overriding would render the wrong model over the user's own selection. | |
| mkdir -p "$TMPHOME/mx" | |
| mj(){ printf '{"cwd":"%s","model":{"display_name":"%s"},"cost":{"total_cost_usd":0},"context_window":{"used_percentage":1,"context_window_size":1000000},"transcript_path":"%s"}' "$NOGIT" "$1" "$2"; } | |
| printf '%s\n' '{"message":{"model":"claude-sonnet-4-6","usage":{}}}' > "$TMPHOME/mx/fb.jsonl" | |
| assert "selection wins over a stale/lower transcript model (no ↓ override)" \ | |
| "$(render "$(mj 'Opus 4.8' "$TMPHOME/mx/fb.jsonl")")" \ | |
| 'nogit · Opus 4.8 · 1%/1M · $0.00' | |
| printf '%s\n' '{"message":{"model":"claude-opus-4-8","usage":{}}}' > "$TMPHOME/mx/match.jsonl" | |
| assert "selection shown when transcript matches it" \ | |
| "$(render "$(mj 'Opus 4.8' "$TMPHOME/mx/match.jsonl")")" \ | |
| 'nogit · Opus 4.8 · 1%/1M · $0.00' | |
| { printf '%s\n' '{"message":{"model":"claude-opus-4-8","usage":{}}}' | |
| printf '%s\n' '{"isSidechain":true,"message":{"model":"claude-sonnet-4-6","usage":{}}}' | |
| } > "$TMPHOME/mx/side.jsonl" | |
| assert "selection shown regardless of a sidechain turn" \ | |
| "$(render "$(mj 'Opus 4.8' "$TMPHOME/mx/side.jsonl")")" \ | |
| 'nogit · Opus 4.8 · 1%/1M · $0.00' | |
| { printf '%s\n' '{"message":{"model":"claude-opus-4-8","usage":{}}}' | |
| printf '%s\n' '{"message":{"model":"<synthetic>","usage":{}}}' | |
| } > "$TMPHOME/mx/synth.jsonl" | |
| assert "selection shown regardless of a synthetic turn" \ | |
| "$(render "$(mj 'Opus 4.8' "$TMPHOME/mx/synth.jsonl")")" \ | |
| 'nogit · Opus 4.8 · 1%/1M · $0.00' | |
| # cost: capture, divergence, threshold color, and the not-ready/heal guards | |
| cb="$TMPHOME/.claude/cost-baselines"; mkdir -p "$cb" | |
| costj(){ printf '{"cwd":"%s","model":{"display_name":"X"},"cost":{"total_cost_usd":%s},"context_window":{"used_percentage":1,"context_window_size":1000000},"transcript_path":"/none/%s.jsonl"}' "$NOGIT" "$1" "$2"; } | |
| # cost omitted entirely: the client reports null before its first API response (on a fresh | |
| # session and, transiently, mid-render), so total is "not ready" this pass. | |
| costj_null(){ printf '{"cwd":"%s","model":{"display_name":"X"},"context_window":{"used_percentage":1,"context_window_size":1000000},"transcript_path":"/none/%s.jsonl"}' "$NOGIT" "$1"; } | |
| printf '5.61' > "$cb/dv"; assert "diverged shows current then total" \ | |
| "$(render "$(costj 7.71 dv)")" 'nogit · X · 1%/1M · $2.10 $7.71' | |
| printf '5.61' > "$cb/dim"; contains "diverged total is dim" \ | |
| "$(render_raw "$(costj 7.71 dim)")" "${ESC}[2m\$7.71" | |
| printf '1' > "$cb/yl"; contains "current >= 20 turns yellow" \ | |
| "$(render_raw "$(costj 26.00 yl)")" "${ESC}[38;2;195;163;105m\$25.00" | |
| printf '1' > "$cb/rd"; contains "current >= 50 turns red" \ | |
| "$(render_raw "$(costj 61.00 rd)")" "${ESC}[38;2;190;92;99m\$60.00" | |
| # not-ready: cost is cumulative over the CLI process, so a baseline captured before the | |
| # client reports cost freezes at 0 and makes the session report the whole process spend, | |
| # not this window's. A null reading must show $0.00 and capture nothing. | |
| assert "cost null shows current \$0.00" \ | |
| "$(render "$(costj_null nr)")" 'nogit · X · 1%/1M · $0.00' | |
| assert "cost null captures no baseline" \ | |
| "$([ -e "$cb/nr" ] && echo yes || echo no)" 'no' | |
| # Once cost populates, the first populated reading becomes the baseline (this window has | |
| # spent ~nothing), so current is $0.00 and the dim cumulative rides alongside. | |
| assert "first populated reading captures the cumulative as baseline" \ | |
| "$(render "$(costj 158.35 nr)")" 'nogit · X · 1%/1M · $0.00 $158.35' | |
| assert "baseline written at the first populated total" "$(cat "$cb/nr")" '158.35' | |
| contains "window spend then grows from the captured baseline" \ | |
| "$(render "$(costj 158.53 nr)")" '$0.18' | |
| # no-transcript: cost_total is set but transcript_path is absent (the brief window after | |
| # /clear before the new transcript is established). Must not bleed the process total | |
| # through as "session" cost — floor to $0.00, same as the not-ready case. | |
| assert "positive cost with no transcript floors to zero" \ | |
| "$(render "$(base 'X' 27.53 ',"context_window":{"used_percentage":1,"context_window_size":1000000}')")" \ | |
| 'nogit · X · 1%/1M · $0.00' | |
| # clobber guard: a transient null reading must not overwrite a good baseline. Parsed as 0 | |
| # by the old code, it tripped the resume-clamp (0 < base) and reset the baseline, so the | |
| # next populated render reported the whole cumulative as "current". | |
| printf '158.17' > "$cb/keep"; assert "null reading shows current \$0.00" \ | |
| "$(render "$(costj_null keep)")" 'nogit · X · 1%/1M · $0.00' | |
| assert "null reading leaves the baseline untouched" "$(cat "$cb/keep")" '158.17' | |
| contains "next populated render reports window spend, not the cumulative" \ | |
| "$(render "$(costj 158.35 keep)")" '$0.18' | |
| # heal: a baseline already frozen at 0 (poisoned by the old bug) recaptures on the next | |
| # populated render instead of reporting the whole cumulative forever. | |
| printf '0' > "$cb/heal"; assert "stored-0 baseline heals to window \$0.00" \ | |
| "$(render "$(costj 158.35 heal)")" 'nogit · X · 1%/1M · $0.00 $158.35' | |
| assert "healed baseline rewritten to the current total" "$(cat "$cb/heal")" '158.35' | |
| # A resumed session reports a lower total than the stored baseline (cost counter | |
| # restarts). Without clamping, cur = total - base floors at 0 forever. The baseline | |
| # must clamp down to the current total so the delta resets and grows from there. | |
| printf '20' > "$cb/clamp"; contains "stale-high baseline shows 0, not negative" \ | |
| "$(render "$(costj 5.5 clamp)")" '$0.00' | |
| assert "baseline clamped down to current total" "$(cat "$cb/clamp")" "5.5" | |
| contains "delta grows from the clamped baseline" \ | |
| "$(render "$(costj 8.5 clamp)")" '$3.00' | |
| # token throughput: cache reads excluded, human-formatted | |
| mkdir -p "$TMPHOME/tx" | |
| { printf '%s\n' '{"message":{"usage":{"input_tokens":1000,"cache_creation_input_tokens":500,"cache_read_input_tokens":40000,"output_tokens":2000}}}' | |
| printf '%s\n' '{"message":{"usage":{"input_tokens":1000,"cache_read_input_tokens":45000,"output_tokens":3000}}}' | |
| } > "$TMPHOME/tx/t.jsonl" | |
| assert "token in (no cache reads) and out, human-formatted" \ | |
| "$(render '{"cwd":"'"$NOGIT"'","model":{"display_name":"X"},"cost":{"total_cost_usd":0},"context_window":{"used_percentage":1,"context_window_size":1000000},"transcript_path":"'"$TMPHOME"'/tx/t.jsonl"}')" \ | |
| 'nogit · X · 1%/1M · $0.00 · ↑2k ↓5k' | |
| # git: project / worktree / branch as optional folded segments | |
| FIX=$(mktemp -d) | |
| git init -q "$FIX/proj" | |
| ( cd "$FIX/proj" | |
| git config user.email t@t; git config user.name t; git config commit.gpgsign false | |
| git checkout -q -b main; mkdir sub; printf x > sub/f; git add .; git commit -qm init | |
| git worktree add -q "$FIX/wt-feat" -b feat >/dev/null 2>&1 # name != branch | |
| git worktree add -q "$FIX/mybr" -b mybr >/dev/null 2>&1 ) # name == branch | |
| gitj(){ printf '{"cwd":"%s","model":{"display_name":"X"},"cost":{"total_cost_usd":0}}' "$1"; } | |
| loc(){ render "$(gitj "$1")"; } # project leads; below the root, the cwd basename rides dim on it | |
| assert "main checkout root: project · branch" "$(loc "$FIX/proj")" 'proj · main · X · 0% CTX · $0.00' | |
| assert "main checkout subdir shows project sub" "$(loc "$FIX/proj/sub")" 'proj sub · main · X · 0% CTX · $0.00' | |
| assert "worktree, name != branch" "$(loc "$FIX/wt-feat")" 'proj · wt-feat · feat · X · 0% CTX · $0.00' | |
| assert "worktree subdir rides dim on project" "$(loc "$FIX/wt-feat/sub")" 'proj sub · wt-feat · feat · X · 0% CTX · $0.00' | |
| assert "worktree, name == branch folds to one" "$(loc "$FIX/mybr")" 'proj · mybr · X · 0% CTX · $0.00' | |
| assert "worktree subdir, name == branch: no double" "$(loc "$FIX/mybr/sub")" 'proj sub · mybr · X · 0% CTX · $0.00' | |
| # the subdir basename is dim, mirroring model + effort | |
| contains "subdir basename renders dim" \ | |
| "$(render_raw "$(gitj "$FIX/proj/sub")")" "proj ${ESC}[2msub${ESC}[0m" | |
| # nested cwd shows only the deepest component (basename, not the full relative path) | |
| mkdir -p "$FIX/proj/sub/deep" | |
| assert "nested subdir shows the leaf basename only" \ | |
| "$(loc "$FIX/proj/sub/deep")" 'proj deep · main · X · 0% CTX · $0.00' | |
| # fold collision: repo dir name == branch name, viewed dirty from a subdir. The subdir and | |
| # the dirty marker must both survive — each attach-condition builds on the prior segment, | |
| # not from the bare name (else the branch suffix would clobber the subdir). | |
| git init -q "$FIX/main" | |
| ( cd "$FIX/main" | |
| git config user.email t@t; git config user.name t; git config commit.gpgsign false | |
| git checkout -q -b main; mkdir sub; printf x > sub/f; git add .; git commit -qm init | |
| printf y >> sub/f ) # dirty | |
| assert "dir == branch collision keeps both subdir and dirty marker" \ | |
| "$(loc "$FIX/main/sub")" 'main sub* · X · 0% CTX · $0.00' | |
| # git: dirty + ahead/behind state, placement after fold | |
| printf y >> "$FIX/proj/sub/f" # dirty the main checkout (tracked file) | |
| assert "dirty marks the branch segment" "$(loc "$FIX/proj")" 'proj · main* · X · 0% CTX · $0.00' | |
| printf z >> "$FIX/mybr/sub/f" # dirty a name==branch worktree, viewed from a subdir | |
| assert "folded name carries the dirty state on the kept copy" \ | |
| "$(loc "$FIX/mybr/sub")" 'proj sub · mybr* · X · 0% CTX · $0.00' | |
| # ahead/behind against an upstream | |
| git init -q --bare "$FIX/remote.git" | |
| ( cd "$FIX/proj" | |
| git checkout -q -- sub/f 2>/dev/null # drop the dirty edit so arrows test is clean | |
| git remote add origin "$FIX/remote.git" | |
| git push -q -u origin main >/dev/null 2>&1 | |
| printf a >> sub/f; git commit -qam ahead1 ) # 1 ahead of origin/main | |
| assert "ahead of upstream shows ↑" "$(loc "$FIX/proj")" 'proj · main ↑1 · X · 0% CTX · $0.00' | |
| ( cd "$FIX/proj" | |
| git reset -q --hard origin/main # back in sync | |
| printf b >> sub/f; git commit -qam tmp; git push -q origin main | |
| git reset -q --hard HEAD~1 ) # local now 1 behind origin/main | |
| assert "behind upstream shows ↓" "$(loc "$FIX/proj")" 'proj · main ↓1 · X · 0% CTX · $0.00' | |
| # diverged: a local commit on top of the behind state → both arrows, ahead before behind | |
| ( cd "$FIX/proj"; printf c >> sub/f; git commit -qam local ) # now 1 ahead AND 1 behind | |
| assert "diverged shows ↑ then ↓" "$(loc "$FIX/proj")" 'proj · main ↑1 ↓1 · X · 0% CTX · $0.00' | |
| # dirty combined with arrows: the * leads, arrows follow, all in one dim suffix | |
| printf d >> "$FIX/proj/sub/f" | |
| assert "dirty and arrows compose in order" "$(loc "$FIX/proj")" 'proj · main* ↑1 ↓1 · X · 0% CTX · $0.00' | |
| # detached HEAD: symbolic-ref fails, so the branch falls back to the short sha | |
| git init -q -b main "$FIX/det" | |
| ( cd "$FIX/det" | |
| git config user.email t@t; git config user.name t; git config commit.gpgsign false | |
| printf x > f; git add .; git commit -qm a; git checkout -q --detach HEAD ) | |
| dsha=$(git -C "$FIX/det" rev-parse --short HEAD) | |
| assert "detached HEAD shows the short sha" "$(loc "$FIX/det")" "det · $dsha · X · 0% CTX · \$0.00" | |
| # bare-repo worktree: git-dir is proj.git/worktrees/wt (no literal /.git/), so the project | |
| # name comes from stripping the trailing .git, not a /.git/ component. | |
| git clone -q --bare "$FIX/proj" "$FIX/bare.git" >/dev/null 2>&1 | |
| git -C "$FIX/bare.git" worktree add -q "$FIX/barewt" -b bw >/dev/null 2>&1 | |
| assert "bare-repo worktree resolves the project name" "$(loc "$FIX/barewt")" 'bare · barewt · bw · X · 0% CTX · $0.00' | |
| # $HOME is itself a repo root: the ~ marker is kept rather than overwritten by the repo name | |
| git init -q -b main "$FIX/homerepo" | |
| homej(){ printf '{"cwd":"%s","model":{"display_name":"X"},"cost":{"total_cost_usd":0}}' "$FIX/homerepo"; } | |
| assert "HOME as repo root keeps ~" \ | |
| "$(HOME="$FIX/homerepo" bash "$SL" <<<"$(homej)" | strip)" '~ · main · X · 0% CTX · $0.00' | |
| # summary | |
| total=$((pass + fail)) | |
| printf '\n%d/%d passed' "$pass" "$total" | |
| [ "$fail" -eq 0 ] && { printf ' — all green\n'; exit 0; } || { printf ' — %d FAILED\n' "$fail"; exit 1; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment