Skip to content

Instantly share code, notes, and snippets.

@Piotr1215
Created August 17, 2026 07:15
Show Gist options
  • Select an option

  • Save Piotr1215/66ccb16f3510e17bb12e1035ac0c6b65 to your computer and use it in GitHub Desktop.

Select an option

Save Piotr1215/66ccb16f3510e17bb12e1035ac0c6b65 to your computer and use it in GitHub Desktop.
Claude Code statusline: git state, kube context, context-vs-autocompact percentage, and 5h rate-limit burn projection
#!/usr/bin/env bash
#
# Claude Code statusline.
#
# ~/dev/dotfiles [master][!?] ⛵ homelab | Opus 5 43% | 61% of 5h window ⏰ 2h16m(1h45m)
#
# Install:
# 1. save as ~/.claude/statusline-script.sh, chmod +x
# 2. add to ~/.claude/settings.json:
# "statusLine": { "type": "command",
# "command": "bash /home/YOU/.claude/statusline-script.sh" }
#
# Requires: bash 4.4+ (mapfile -d), jq, git. Optional: fd, findmnt, kubectl.
#
# Segments you may want to delete, they are specific to my machine:
# - design-kit indicator, reads .claude/specs/<branch>
# - spawn-mode indicator, gated on a tmux pane named "triage"
# - kube context via `kctx`, falls back to `kubectl config current-context`
#
# The long comments are the reasoning behind each number. Keep or strip as you
# like; nothing below depends on them.
set -eo pipefail
# Read Claude Code session data
input=$(cat)
# One jq pass, not one per field. This runs on a 300ms debounce and Claude Code
# kills an in-flight render when the next update arrives, so every process spawn
# here competes with the line actually appearing. Nine invocations each re-parsed
# the same payload; a single tab-separated read costs one.
#
# NUL-separated, NOT tab. Tab is an IFS *whitespace* character, so `read` with
# IFS=$'\t' collapses a run of them into one delimiter and every field after a
# pair of empty values shifts up. Most of these are empty in a normal session
# (no PR, no project_dir), and the shift put a unix timestamp in the context
# percent slot and the window size in the token count: "1000K 1786753800%".
# NUL is not IFS whitespace and cannot occur inside any value, so mapfile -d ''
# preserves empty fields positionally.
mapfile -d '' -t _f < <(echo "$input" | jq -j '[
.model.display_name // "",
(.workspace.current_dir // .cwd // ""),
.workspace.project_dir // "",
.output_style.name // "default",
.pr.number // "",
.pr.url // "",
.context_window.total_input_tokens // 0,
.context_window.total_output_tokens // 0,
.context_window.used_percentage // "",
.rate_limits.five_hour.used_percentage // "",
.rate_limits.five_hour.resets_at // ""
] | map(tostring) | join("\u0000")')
model_name="${_f[0]}" current_dir="${_f[1]}" project_dir="${_f[2]}"
output_style="${_f[3]}" pr_number="${_f[4]}" pr_url="${_f[5]}"
total_input="${_f[6]}" total_output="${_f[7]}" ctx_pct="${_f[8]}"
rate_5h="${_f[9]}" rate_5h_reset="${_f[10]}"
unset _f
# Get directory for display (relative to project if available, otherwise basename)
if [[ -n "$project_dir" && "$current_dir" == "$project_dir"* ]]; then
rel_dir="${current_dir#$project_dir}"
rel_dir="${rel_dir#/}"
display_dir="${rel_dir:-$(basename "$project_dir")}"
else
display_dir=$(basename "$current_dir")
fi
# Change to the current directory for git operations
cd "$current_dir" 2>/dev/null || true
# Git information (matching Starship format exactly)
git_info=""
if git rev-parse --git-dir >/dev/null 2>&1; then
branch=$(git branch --show-current 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo "detached")
# Git status indicators (matching Starship default symbols)
status=""
if ! git diff --quiet 2>/dev/null; then
status="${status}!" # Modified files
fi
if ! git diff --cached --quiet 2>/dev/null; then
status="${status}+" # Staged files
fi
if [[ -n $(git ls-files --others --exclude-standard 2>/dev/null) ]]; then
status="${status}?" # Untracked files
fi
# Check for ahead/behind
ahead_behind=""
if upstream=$(git rev-parse --abbrev-ref "@{upstream}" 2>/dev/null); then
local_commits=$(git rev-list --count HEAD..@{upstream} 2>/dev/null || echo "0")
remote_commits=$(git rev-list --count @{upstream}..HEAD 2>/dev/null || echo "0")
if [[ $local_commits -gt 0 ]]; then
ahead_behind="${ahead_behind}⇣${local_commits}"
fi
if [[ $remote_commits -gt 0 ]]; then
ahead_behind="${ahead_behind}⇡${remote_commits}"
fi
fi
git_info="[${branch}]"
if [[ -n "$status" || -n "$ahead_behind" ]]; then
git_info="${git_info}[${status}${ahead_behind}]"
fi
# Persist the PR number keyed by repo root so external tools can read it
# without a gh round-trip. Absent .pr means no open PR -> drop the cache.
repo_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
if [[ -n "$repo_root" ]]; then
pr_cache_dir="/tmp/claude-pr-cache"
mkdir -p "$pr_cache_dir" 2>/dev/null || true
pr_key=$(printf '%s' "$repo_root" | md5sum | cut -d' ' -f1)
if [[ -n "$pr_number" ]]; then
printf '%s' "$pr_number" > "$pr_cache_dir/$pr_key" 2>/dev/null || true
printf '%s' "$pr_url" > "$pr_cache_dir/$pr_key.url" 2>/dev/null || true
else
rm -f "$pr_cache_dir/$pr_key" "$pr_cache_dir/$pr_key.url" 2>/dev/null || true
fi
fi
fi
# Kubernetes context. In tmux, read the authoritative pane selection rather
# than Claude's inherited environment. Claude redraws this line on its own
# event cycle; the tmux pane border remains the immediate indicator.
kube_info=""
if [[ -n "${TMUX:-}" ]] && command -v kctx >/dev/null 2>&1; then
context=$(kctx current --output label 2>/dev/null || kctx current --output id 2>/dev/null || true)
elif command -v kubectl >/dev/null 2>&1; then
context=$(kubectl config current-context 2>/dev/null || true)
else
context=""
fi
if [[ -n "$context" && "$context" != "docker-desktop" ]]; then
kube_info="⛵ ${context}"
fi
# AWS context - only show profile name if explicitly set (no slow API calls)
aws_info=""
if [[ -n "$AWS_PROFILE" ]]; then
aws_info="󰅟 ${AWS_PROFILE}"
fi
# Python virtual environment
python_info=""
if [[ -n "$VIRTUAL_ENV" ]]; then
venv_name=$(basename "$VIRTUAL_ENV")
python_info="🐍${venv_name}"
fi
# Node.js version (if in a Node project)
node_info=""
if [[ -f "package.json" ]] && command -v node >/dev/null 2>&1; then
node_version=$(node --version 2>/dev/null | sed 's/v//')
node_info="⬢${node_version}"
fi
# Container indicator
container_info=""
if [[ -f "Dockerfile" ]]; then
container_info="🐋"
fi
# Design-kit indicator (check for branch design-kit progress)
design_info=""
if [[ -d ".claude/specs" ]] && git rev-parse --git-dir >/dev/null 2>&1; then
current_branch=$(git branch --show-current 2>/dev/null || echo "detached")
safe_branch=$(echo "$current_branch" | sed 's/[^a-zA-Z0-9._-]/-/g')
design_dir=".claude/specs/$safe_branch"
if [[ -d "$design_dir" ]]; then
# Check design-kit components
design_parts=""
# Check for PLAN.md (Phase 0)
[[ -f "$design_dir/PLAN.md" ]] && design_parts="${design_parts}P"
# Check for proofs/ directory (Phase 1)
if [[ -d "$design_dir/proofs" ]]; then
proof_count=$(fd -t f . "$design_dir/proofs" 2>/dev/null | wc -l)
if [[ $proof_count -gt 0 ]]; then
design_parts="${design_parts}✓${proof_count}"
else
design_parts="${design_parts}✓0"
fi
fi
# Check for tasks/ directory (Phase 2)
if [[ -d "$design_dir/tasks" ]]; then
task_count=$(fd -t f . "$design_dir/tasks" 2>/dev/null | wc -l)
if [[ $task_count -gt 0 ]]; then
design_parts="${design_parts}T${task_count}"
else
design_parts="${design_parts}T0"
fi
fi
if [[ -n "$design_parts" ]]; then
design_info="🎨[${design_parts}]"
else
design_info="🎨[empty]"
fi
fi
fi
# Mount indicator (matching your Starship custom.mount_indicator)
mount_info=""
if findmnt -T . -o FSTYPE -n 2>/dev/null | grep -q -E '^(nfs|fuse|cifs|smb)'; then
mount_source=$(findmnt -T . -o SOURCE -n 2>/dev/null || echo "")
if [[ -n "$mount_source" ]]; then
mount_info="🐎 (${mount_source})"
else
mount_info="🐎"
fi
fi
# Command duration indicator (if available from previous commands)
duration_info=""
if [[ -n "$HISTTIMEFORMAT" ]] && command -v fc >/dev/null 2>&1; then
# This would need shell integration to work properly, skipping for now
:
fi
# Output style indicator (if not default)
style_info=""
if [[ "$output_style" != "default" && -n "$output_style" ]]; then
style_info="[${output_style}]"
fi
# Gordon spawn-mode indicator: Piotr's dispatch switch, ~/.claude/gordon-spawn-mode.
# This script is shared by EVERY Claude pane, so the segment is gated hard on the
# triage pane. Discriminator is the @agent_name tmux pane option, which
# __tmux_agent_status.sh sets to the registered agent name on agent_register;
# Gordon registers as exactly "triage" (workers register as <repo>-<ISSUE-ID>).
# Fallback covers the gap between pane start and registration: a tmux session or
# window named exactly "triage". Cost is one tmux call plus one short file read.
spawn_info=""
if [[ -n "${TMUX:-}" && -n "${TMUX_PANE:-}" ]] && command -v tmux >/dev/null 2>&1; then
pane_ident=$(tmux display-message -p -t "$TMUX_PANE" \
'#{@agent_name}|#{session_name}|#{window_name}' 2>/dev/null || true)
agent_tag="${pane_ident%%|*}"
rest_ident="${pane_ident#*|}"
sess_name="${rest_ident%%|*}"
win_name="${rest_ident##*|}"
if [[ "$agent_tag" == "triage" || "$sess_name" == "triage" || "$win_name" == "triage" ]]; then
spawn_mode=""
if [[ -r "$HOME/.claude/gordon-spawn-mode" ]]; then
IFS= read -r spawn_mode < "$HOME/.claude/gordon-spawn-mode" 2>/dev/null || spawn_mode=""
fi
spawn_mode="${spawn_mode//[[:space:]]/}"
# Missing, unreadable, or anything other than "manual" is treated as auto,
# matching the contract in triage-agent-system-prompt.md.
if [[ "$spawn_mode" == "manual" ]]; then
spawn_info=$'\033[1;33m✋manual\033[0m'
else
spawn_info=$'\033[0;90m⇢auto\033[0m'
fi
fi
fi
# One scale for every percentage on the line. Both figures answer the same
# question (how much of a budget is gone) so they must grade identically, or the
# eye learns two conventions and trusts neither. Green through 50, yellow to 75,
# red beyond. Colour lands on the number alone: the surrounding words are
# constant and do not change meaning with the value, so tinting them only makes
# the segment shout.
# Format seconds as a duration into $_dur_out. Sets a variable rather than
# printing, because a command substitution forks and this runs on every render.
_fmt_dur() {
if [[ $1 -ge 3600 ]]; then printf -v _dur_out '%dh%02dm' $(( $1 / 3600 )) $(( ($1 % 3600) / 60 ))
elif [[ $1 -ge 60 ]]; then printf -v _dur_out '%dm' $(( $1 / 60 ))
else _dur_out='<1m'
fi
}
_pct_color() { # $1 = raw percentage; prints an SGR colour
local pct=${1%%.*}
if [[ ! "$pct" =~ ^[0-9]+$ ]]; then printf '%s' $'\033[90m'
elif [[ $pct -ge 76 ]]; then printf '%s' $'\033[31m'
elif [[ $pct -ge 51 ]]; then printf '%s' $'\033[33m'
else printf '%s' $'\033[32m'
fi
}
# Context, measured against the budget that actually binds.
#
# The payload's used_percentage is a fraction of context_window_size, and on a
# 1M-window model that number is decorative: nothing happens at 1M, because
# auto-compaction fires long before it. Measured across every compact_boundary
# record in ~/.claude/projects (17 auto-compactions, versions 2.1.220 through
# 2.1.232), preTokens landed between 366,761 and 369,359. A 2,598-token spread,
# under 0.7%, flat across five versions. Auto-compact fires at ~368K, which is
# about 92% of a 400K effective budget, NOT a fraction of the model's window.
#
# So the honest reading of "how full am I" is total_input/368K. On the old
# denominator this session would have shown a comfortable green 37% at the exact
# moment it compacted, which is why compaction kept arriving unannounced.
#
# EVIDENCE BOUND: all 17 samples came from Opus 5 sessions. Whether a 200K model
# carries the same budget is unknown, so this constant may read wrong there.
# Override with CLAUDE_COMPACT_BUDGET_TOKENS if a different model proves to
# differ; correcting it needs only one compact_boundary record from that model:
# rg -N --no-filename compact_boundary -g '*.jsonl' ~/.claude/projects \
# | jq -r 'select(.compactMetadata.trigger=="auto") | .compactMetadata.preTokens'
compact_budget="${CLAUDE_COMPACT_BUDGET_TOKENS:-368000}"
token_info=""
if [[ $total_input -gt 0 || $total_output -gt 0 ]]; then
if [[ $total_input -gt 0 ]]; then
compact_pct=$(( total_input * 100 / compact_budget ))
# Just "Opus 5 43%". Both words that once qualified this number have now
# been learned and dropped, in the same order they were added.
#
# "used" went first: it answered "43% consumed or 43% left?" for one
# round, then became noise once the colour ramp was doing the work, since
# green cannot mean nearly-exhausted.
#
# "-> compaction" went second, for the reason the line exists at all. It
# named the destination of the percentage, which mattered while the
# denominator was surprising (368K, not the 1M badge beside it). Once
# that is known it is a constant string repeated on every render, and on
# a split pane it is the widest constant on the line: nine columns spent
# restating a fact the reader has already learned. Everything on this
# line has to earn its width against the narrowest pane it appears in.
#
# What is deliberately NOT compensated for: the "(1M context)" badge can
# now sit beside a percentage that is not a fraction of it. That badge is
# model spec, this is consumption, and nothing labels the seam. The bet
# is that a reader who knows what the number means also knows what it is
# measured against, which is exactly the knowledge this edit assumes. If
# a fresh reader misreads 43% as 43% of 1M, the label comes back.
#
# Dim the name, colour the number. The model is constant, the percentage
# is the only part that carries information that changes.
# No hint glyph beside this. One was tried at half the budget, meaning
# "if you are switching tasks, clear now", and was rejected on sight:
# the colour ramp already crosses to yellow at 51% and red at 76%, which
# is the same signal arriving through a channel the reader is watching
# anyway. A glyph that fires exactly when the number changes colour adds
# a symbol to decode, not a fact to act on.
token_info=$'\033[90m'"${model_name}"$'\033[0m'" $(_pct_color "$compact_pct")${compact_pct}%"$'\033[0m'
else
# No input count yet. Fall back to the payload's own figure against the
# raw window, which is at least true even though it is not actionable.
ctx_int=${ctx_pct%%.*}
[[ "$ctx_int" =~ ^[0-9]+$ ]] \
&& token_info="$(_pct_color "$ctx_int")${ctx_int}%"$'\033[0m'" of ${model_name}" \
|| token_info="${model_name}"
fi
fi
# Subscription rate limits. Absent for non-subscribers and until the first API
# response of a session, and each window is documented as independently absent,
# so every branch here treats missing as "render nothing" rather than as zero.
#
# This exists because the delegation policy says to route new work to another
# backend as the current provider approaches a limit, and nothing on screen said
# when that was. The failure mode it replaces is discovering the wall through a
# worker that died on it.
#
# Both windows render, each independently. Showing only the higher one hid the
# weekly number exactly when the 5h window was busy, which is when "am I about
# to spend the week's budget" is the question actually worth asking. They cap
# different things and either can be the binding one.
#
# Every present window renders, always, with both its percentage and the time
# until it resets. An earlier cut hid a window below 50% and hid the reset time
# below 75%, on the theory that a comfortable limit is noise. That was wrong for
# this use: the number you want at a glance is not "am I in trouble" but "how
# much of the window is left and when does it refill", and a gauge that appears
# only during trouble cannot be read as a trend.
#
# One number per window, and deliberately a different KIND of number for each,
# because the two windows are asked different questions. Inside a session the
# 5h window is a wait: the useful fact is when it refills, not what fraction is
# gone. Across a week the 7d window is a budget: the useful fact is how much is
# spent, since its reset is days out and never a choice you make today.
#
# Both still colour by percentage, so the 5h countdown reddens as the window
# fills even though the percentage itself is not printed.
rate_info=""
_r5=""
if [[ "$rate_5h" =~ ^[0-9]+ ]]; then
_r5="$(_pct_color "$rate_5h")${rate_5h%%.*}%"$'\033[0m'" of 5h window"
# Countdown, not wall clock. This reverses an earlier decision recorded
# against this file, on the grounds the decision got the need wrong: a fixed
# point in the day answers "when", but the question being asked is "how much
# longer", and only a shrinking number conveys that something is running.
#
# The risk the old decision guarded against is real and unfixed: "5h window"
# and "2h26m" are both durations of the same form, which is what made the
# original "⏳5h 44% ↻2h55m" unreadable. What makes it survive here is that
# there is now exactly one duration, at the end, behind a clock emoji, in a
# sentence rather than a run of figures. Adding a second time value back to
# this segment would break it again.
#
# An alarm clock stands in for "resets in". It is an emoji, not a symbol
# glyph: ↻ was tried and fell back to a substitute font, because the terminal
# font ships colour emoji but not the arrows block. Anything added here must
# come from the emoji range for that reason.
#
# Steady cyan, deliberately off the green/yellow/red scale. Time remaining is
# not graded on the same axis as consumption: painting it green or yellow
# would imply a severity it does not carry, and would contradict itself on
# the days the two disagree (a red 93% beside a green countdown). Cyan says
# "different kind of value" while still pulling the eye.
#
# Minutes are zero-padded, so it reads 2h06m rather than 2h6m and the field
# keeps a constant width as it ticks down. The line redraws on session
# events rather than on a clock tick, so this advances when you interact,
# not second by second.
_now=$(date +%s)
if [[ "$rate_5h_reset" =~ ^[0-9]+$ ]] && [[ $rate_5h_reset -gt $_now ]]; then
_rem=$(( rate_5h_reset - _now ))
_fmt_dur "$_rem"
_r5="${_r5} ⏰ "$'\033[36m'"${_dur_out}"$'\033[0m'
# Pace verdict: at the current burn rate, does the quota outlast the
# window or not. The window is five hours by definition, so elapsed time
# is 18000 - remaining, the rate is spent/elapsed, and the quota runs out
# in (100 - spent)/rate. Compare that against the time left.
#
# A SIGNED DURATION, read as a delta on the countdown beside it: "2h16m
# -31m" means the quota runs dry 31 minutes before the window resets,
# and "+40m" means it would have outlasted the window by 40.
#
# Two earlier forms were tried and both failed on the same point. A
# verdict ("won't last") answered only yes or no. A projected total
# ("-> 112%") made the reader subtract 100 and then translate a
# percentage back into the thing they actually plan in, which is time.
# The question being asked is "when do I stop", so answer it in minutes.
#
# This deliberately breaks the rule recorded against this file that the
# segment carries exactly one duration. "5h window", "2h16m" and "-31m"
# is three, which is the shape that made "⏳5h 44% ↻2h55m" unreadable.
# Two things keep it readable. The sign marks it as an offset rather
# than an independent quantity, and the parentheses bind it visually to
# the countdown it modifies, so the eye takes "2h16m (-31m)" as one
# figure with a correction rather than as two competing durations.
#
# The calculation: at the current rate the remaining quota lasts
# (100 - spent) * elapsed / spent seconds. Subtract the time left in the
# window and the sign falls out.
#
# The old guards suppressed the figure entirely below 1200s elapsed or
# 3% spent, because a thin sample projects wildly. That was the wrong
# trade for the thing this number is for. It is not a readout, it is an
# input to a decision (drop to a cheaper model, hand the task to another
# tool, stop for lunch), and a blank slot supplies nothing to decide
# with. A noisy figure at least carries direction, which is what "am I
# burning fast" needs. So the guards no longer gate the number, they
# grade its confidence: a thin sample still renders, marked provisional.
#
# Tenths, not whole percent. ${rate_5h%%.*} truncated 3.9 to 3, and the
# error lands where it hurts most: at the bottom of the range that is a
# 23% error in the rate, and it biases one way, understating spend and
# so overstating how long the quota lasts. Rounded to tenths from two
# decimals, which cuts the worst case to a rounding error nothing on
# this line can display anyway. 10# on the decimal digits because "08"
# and "09" are not octal.
_int=${rate_5h%%.*}
_dec=""
[[ "$rate_5h" == *.* ]] && _dec="${rate_5h#*.}"
_dec="${_dec}00"; _dec="${_dec:0:2}"
_p10=$(( _int * 10 + (10#$_dec + 5) / 10 ))
_elapsed=$(( 18000 - _rem ))
# Rate is undefined at zero elapsed and unstable just after it, so the
# window opens on a floor rather than a division. One minute of
# manufactured history is a smaller lie than a projection off a
# denominator of three seconds.
[[ $_elapsed -lt 60 ]] && _elapsed=60
# PACE FROM RECENT SPEND, NOT FROM THE WHOLE WINDOW.
#
# Averaging everything since the window opened makes this a function of
# the clock instead of behaviour. Measured: 11% spent reads 33m at four
# minutes in and 6h45m at fifty minutes in, with no work done between
# them. Twelvefold movement while the user sits still. A figure that
# swings that far is not describing a pace, and this one exists to be
# acted on (drop to a cheaper model, stop for lunch), so it has to track
# what is actually happening now.
#
# The burst case is the one that matters and it is the one the average
# gets worst. Several agents on one account can spend 8% in two minutes;
# from window start that reads as the new normal forever, when in fact
# it ended when they did.
#
# So the slope comes from the oldest retained sample rather than from
# zero. History is keyed by the reset stamp, so a new window starts
# clean instead of inheriting the previous one's slope, and samples
# older than 30 minutes fall out so the figure stays responsive.
_hist="${XDG_STATE_HOME:-$HOME/.local/state}/claude-statusline-pace"
_base_ts=""; _base_p10=""; _keep=""; _maxp10=0
if [[ -r "$_hist" ]]; then
while read -r _h_reset _h_ts _h_p10; do
[[ "$_h_reset" == "$rate_5h_reset" ]] || continue
[[ "$_h_ts" =~ ^[0-9]+$ && "$_h_p10" =~ ^[0-9]+$ ]] || continue
(( _now - _h_ts > 1800 )) && continue
# BY MINIMUM TIMESTAMP, NOT BY POSITION. File order is not
# chronological and cannot be made so: concurrent renders race
# the read-copy-swap, and a writer holding an earlier clock can
# win the mv later, appending its older sample after a newer
# one. Sorting on write does not help, because the race is
# between writers.
#
# Taking the first line looks safe while the seeded head is
# alive, since copy-forward keeps that head genuinely oldest.
# It breaks when the head ages past the prune and promotes an
# unordered tail: same samples, no corruption, and the figure
# reads a third low with nothing in the file to show for it.
if [[ -z "$_base_ts" ]] || (( _h_ts < _base_ts )); then
_base_ts="$_h_ts"; _base_p10="$_h_p10"
fi
(( _h_p10 > _maxp10 )) && _maxp10="$_h_p10"
_keep+="${_h_reset} ${_h_ts} ${_h_p10}"$'\n'
done < "$_hist"
fi
# SPEND IS NON-DECREASING INSIDE A WINDOW, THE READINGS ARE NOT.
#
# Every session gets its own snapshot of the same account-wide rate
# limit, and they disagree: one file here held 420, 430, 420, 450, 420,
# 450 within four minutes. Merging several sessions into one series and
# subtracting endpoints therefore produced whatever the noise chose. A
# high baseline against a low current reads as negative burn and clamps
# to "outlasts the window"; the reverse reads as a burst. Observed live
# as the figure flipping between 2h and 19m with nothing happening.
#
# A percentage that cannot fall is the fact that repairs it: carry the
# running maximum forward, so a low reading is treated as a stale
# snapshot rather than as spend being refunded. Burn can then never be
# negative and the endpoints are comparable.
(( _maxp10 > _p10 )) && _p10="$_maxp10"
printf '%s%s %s %s\n' "$_keep" "$rate_5h_reset" "$_now" "$_p10" \
> "$_hist.$$" 2>/dev/null && mv -f "$_hist.$$" "$_hist" 2>/dev/null
rm -f "$_hist.$$" 2>/dev/null
_prov=0
_span=0; _burn=0
[[ -n "$_base_ts" ]] && { _span=$(( _now - _base_ts )); _burn=$(( _p10 - _base_p10 )); }
if [[ $_p10 -le 0 ]]; then
# Nothing spent at all: the projection is unbounded, which is the
# clamped case by definition. Stated here rather than reached by
# dividing by zero.
_life=$(( _rem + 1 ))
[[ $_elapsed -lt 1200 || $_p10 -lt 30 ]] && _prov=1
elif (( _span >= 600 )); then
if (( _burn <= 0 )); then
# Spend has not moved across a real interval. The honest reading
# is that at THIS pace the quota outlasts the window, which the
# clamp below renders as ">". Reporting the window-start average
# here instead would show a shrinking number while nothing is
# being consumed, which is the exact lie this block removes.
_life=$(( _rem + 1 ))
else
_life=$(( (1000 - _p10) * _span / _burn ))
(( _span < 300 )) && _prov=1
fi
else
# Not enough history yet, so fall back to the window-start average.
#
# TEN MINUTES, NOT TWO. The first cut used 120s on the grounds that
# it was arithmetically valid, which it is and which is not the
# bar. The reading is quantised to whole percent, so a two-minute
# span sees either zero ticks or one, and a single tick is the
# whole answer. Measured against the same window: a 129s span
# projected 23m while the window average said 50m, and the account
# figure agreed with 50m. Two minutes tracks the last burst; ten
# tracks a pace, which is the thing being asked for.
#
# The fallback is not a degraded mode here. Once a window has been
# running a while its average is steady and close to right, and the
# short-span slope is the noisy one. The old model was only wrong
# in the opening minutes, which is exactly where this now uses it
# least.
_life=$(( (1000 - _p10) * _elapsed / _p10 ))
[[ $_elapsed -lt 1200 || $_p10 -lt 30 ]] && _prov=1
fi
# TIME TO EMPTY, not the gap between empty and the reset. The signed
# offset that shipped first was arithmetic the reader had to finish:
# "-2h55m" beside "4h40m" means dry in 1h45m, and it was read as "dry in
# 2h55m" twice in one morning, by the person who specified it. The
# number exists to answer one question, how long can I keep working at
# this pace, so it now states that directly and the subtraction happens
# here instead of in the reader's head.
#
# The comparison the offset used to carry is not lost, it is implied:
# a figure smaller than the countdown beside it means the quota goes
# first. Colour makes it immediate without a second reading.
#
# Clamped at the countdown, with ">" for anything past it. Beyond that
# bound the window resets and the quota refills, so a larger figure
# would be describing a quota that no longer exists. It also puts a
# ceiling on the low-spend explosion that once printed "+61h40m": the
# projection carries elapsed/spent and grows without limit as spent
# approaches zero. Nothing here may state a duration the window cannot
# contain.
#
# No label on the figure. "dry" carried it for one round, the same
# single round "used" and "-> compaction" each got before being dropped:
# a word is worth its width until the meaning is learned, and colour
# already says which way this one runs. Red is the quota going first,
# green is the window going first, and the two are never ambiguous
# because they are never both true.
if [[ $_life -gt $_rem ]]; then
_fmt_dur "$_rem"; _pre=">"; _col=$'\033[32m'
else
_fmt_dur "$_life"; _pre=""; _col=$'\033[31m'
fi
# Provisional: real arithmetic on a sample too thin to trust, so it
# renders in the grey that already means "no confident reading" here
# (_pct_color returns the same grey for a percentage it cannot parse)
# behind a "~". Colour alone would carry it, but the tilde survives a
# colour-blind glance and a light-theme pane, and this is the one state
# where mistaking which reading you are looking at costs a decision.
if [[ $_prov -eq 1 ]]; then
_col=$'\033[90m'; _pre="~${_pre}"
fi
_r5="${_r5}${_col}(${_pre}${_dur_out})"$'\033[0m'
unset _life _pre _col _prov _int _dec _p10 _hist _base_ts _base_p10 _keep _span _burn _h_reset _h_ts _h_p10
unset _rem _elapsed
fi
unset _now
fi
# Only the 5h window. The weekly figure was dropped for the reason it was never
# actionable: by the time the week is genuinely at risk the 5h window has been
# hitting its cap all day, so the weekly number restates something already
# obvious rather than warning about it first. Two percentages side by side also
# forced a comparison that had no answer, since neither predicts the other.
#
# "N% of X", the same shape as the context segment, so both figures read as one
# pattern. Key-value shapes failed on parts of speech: in "ctx 29% 5h 45%" the
# label "ctx" is a noun but "5h" is a quantity, so the eye saw four numbers
# rather than two labelled ones.
#
# Consumption, not headroom, and no word says so. The colour ramp does: green
# cannot mean "nearly exhausted", so a green number can only read as spent.
[[ -n "$_r5" ]] && rate_info="${_r5}"
unset _r5
worktree_info=""
if [[ "$current_dir" == */.claude/worktrees/* ]]; then
worktree_info="🌳"
fi
parts=()
[[ -n "$worktree_info" ]] && parts+=("$worktree_info")
parts+=("$display_dir")
[[ -n "$git_info" ]] && parts+=("$git_info")
[[ -n "$design_info" ]] && parts+=("$design_info")
[[ -n "$aws_info" ]] && parts+=("$aws_info")
[[ -n "$mount_info" ]] && parts+=("$mount_info")
[[ -n "$kube_info" ]] && parts+=("$kube_info")
[[ -n "$python_info" ]] && parts+=("$python_info")
[[ -n "$node_info" ]] && parts+=("$node_info")
[[ -n "$container_info" ]] && parts+=("$container_info")
[[ -n "$style_info" ]] && parts+=("$style_info")
[[ -n "$spawn_info" ]] && parts+=("$spawn_info")
# One row, with a rule before the budget group. The line carries two unrelated
# kinds of fact: where I am (account, directory, branch, cluster, model), which
# barely changes, and what I am spending (context, quota windows, cost), which
# changes every turn. Separated by spaces alone they ran together and read as a
# string of numbers however the values were labelled. A single divider gives the
# eye the boundary it was missing without spending a second row on it.
#
# ASCII pipe, not a box-drawing character: the earlier cycle arrow was a
# non-emoji glyph and font-fell-back visibly, and a divider that renders wrong
# is worse than no divider.
# Three segments, divided by scope rather than by topic.
#
# <machine and tree> | <this session> | <this account>
#
# Left: account, directory, branch, cluster. Where the work is happening.
# Middle: context, phrased as a fraction of the named model's window. Both
# halves describe THIS conversation and change as it grows.
# Right: the rate windows. These are account-wide, shared with every other
# session on the machine, and outlive this conversation entirely. That makes
# them a different kind of fact from everything to their left, which is why
# they read as an interruption anywhere but the end.
#
# The middle segment SPEAKS the model name, so printing the badge again beside
# it says "Opus 5 (1M context)" twice on one line. The badge stands alone only
# when there is no percentage to attach it to: a fresh session, or a payload
# with no context figures.
#
# Each rule appears only when its segment does, so a fresh session with no API
# response yet is still just the environment and the model.
_rule=$'\033[90m|\033[0m'
if [[ -n "$token_info" ]]; then
parts+=("$_rule")
parts+=("$token_info")
else
parts+=("$model_name")
fi
if [[ -n "$rate_info" ]]; then
parts+=("$_rule")
parts+=("$rate_info")
fi
unset _rule
IFS=' '
echo "${parts[*]}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment