8a04d6f1bb
The statusline resolved its handle from ~/.althing/session_handles.json.
forseti corrected the grounding and I verified it: that file is a v2
artifact and v3 never opens it. `grep -rn session_handles althing/` is
empty, postbox's resolve_config takes --handle then ALTHING_HANDLE and
nothing else, and `althing-cli use` -- the tool that maintained the map
-- was deleted at the cutover. Whatever is in it now is hand-kept and
drifts silently.
launch-history.json is written by dev_launch, which is the thing that
sets ALTHING_HANDLE in the first place, so it is the real cwd-to-handle
binding. Shape is {cwd: {command: {at, handle}}} with several commands
per directory, so this takes the most recent by timestamp rather than
whichever key happens to sort first. The v2 map stays as a fallback for
its broader coverage.
Worth recording why this was wrong: I wrote the resolution this morning
by reading the v2 statusline block it replaced and keeping its data
source while updating its commands. The commands were the visible half
of the cutover and the data source was not, so it survived a rewrite
that was otherwise about removing v2.
193 lines
8.9 KiB
Bash
Executable File
193 lines
8.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# CANONICAL COPY of the Claude Code statusline. Deployed to (and read from):
|
|
#
|
|
# ~/.claude/statusline-command.sh <- the LIVE path CC actually runs
|
|
#
|
|
# Install / update after editing here:
|
|
# cp scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
|
|
#
|
|
# Copies, not symlinks — same rule as stacks/: this tree is intent, the live
|
|
# path is reality, and they diverge until someone deploys. Diff them with
|
|
# diff -u scripts/claude-statusline-command.sh ~/.claude/statusline-command.sh
|
|
#
|
|
# It is version-controlled here because the althing v3 cutover broke it in a way
|
|
# that was invisible: the segment gated on `command -v althing-cli`, a binary the
|
|
# cutover deleted, so the 📬 badge and 🔔 bell silently vanished for every session
|
|
# on the box. With 71 of 73 handles pull-only, that badge is the ONLY out-of-band
|
|
# signal telling a session with no armed waiter that it has mail — a dead
|
|
# statusline made a working bus look like an empty one.
|
|
#
|
|
# Smoke test (CC pipes session JSON on stdin):
|
|
# echo '{"model":{"display_name":"opus"},"workspace":{"current_dir":"/tmp"},"cwd":"/tmp"}' \
|
|
# | ALTHING_HANDLE=<a-handle-with-unread> bash scripts/claude-statusline-command.sh
|
|
# want: a leading `📬 N`; and with an unreachable post office, degradation in
|
|
# ~2s rather than a hang.
|
|
# Claude Code statusline. Layout:
|
|
# [📬N] [🔔/🔕] | <proj> ⎇<branch> *<dirty> ↑<unpushed> | <model> | ctx:<pct> <toks> | $<session-cost> | 5h:% 7d:%
|
|
# ctx% and rate-limit %s are threshold-colored: green <60, yellow 60-90, red >90.
|
|
# All segments degrade gracefully (missing tool / non-git dir / no handle => segment omitted).
|
|
input=$(cat)
|
|
|
|
# --- threshold color: $1=numeric pct, $2=display text -> colored text ---
|
|
color_pct() {
|
|
local p="$1" txt="$2" c
|
|
if awk "BEGIN{exit !($p < 60)}"; then c=$'\033[32m' # green <60
|
|
elif awk "BEGIN{exit !($p > 90)}"; then c=$'\033[31m' # red >90
|
|
else c=$'\033[33m' # yellow 60-90
|
|
fi
|
|
printf '%s%s\033[0m' "$c" "$txt"
|
|
}
|
|
|
|
# --- reset countdown: $1=unix ts -> "1d3h"/"3h20m"/"45m" (2-unit; "now"/empty edge) ---
|
|
reset_in() {
|
|
local ts="$1" now delta d h m
|
|
[ -z "$ts" ] && return
|
|
now=$(date +%s)
|
|
delta=$(( ts - now ))
|
|
[ "$delta" -le 0 ] && { printf 'now'; return; }
|
|
if [ "$delta" -ge 86400 ]; then
|
|
d=$(( delta / 86400 )); h=$(( (delta % 86400) / 3600 ))
|
|
if [ "$h" -gt 0 ]; then printf '%dd%dh' "$d" "$h"; else printf '%dd' "$d"; fi
|
|
elif [ "$delta" -ge 3600 ]; then
|
|
h=$(( delta / 3600 )); m=$(( (delta % 3600) / 60 ))
|
|
if [ "$m" -gt 0 ]; then printf '%dh%dm' "$h" "$m"; else printf '%dh' "$h"; fi
|
|
else
|
|
printf '%dm' $(( delta / 60 ))
|
|
fi
|
|
}
|
|
|
|
# --- one jq pass for every payload field ---
|
|
# \x1f (unit separator) delimiter, NOT tab: tab is IFS-whitespace so `read` would
|
|
# collapse consecutive tabs and shift every field after an empty one (e.g. a
|
|
# session with no rate_limits). \x1f is non-whitespace -> empty fields preserved.
|
|
IFS=$'\x1f' read -r model used_pct input_tok five_pct week_pct cwd fast cost_usd model_id five_reset week_reset < <(
|
|
printf '%s' "$input" | jq -r '[
|
|
(.model.display_name // "unknown"),
|
|
(.context_window.used_percentage // ""),
|
|
(.context_window.total_input_tokens // 0),
|
|
(.rate_limits.five_hour.used_percentage // ""),
|
|
(.rate_limits.seven_day.used_percentage // ""),
|
|
(.cwd // .workspace.current_dir // ""),
|
|
(.fast_mode // false),
|
|
(.cost.total_cost_usd // 0),
|
|
(.model.id // ""),
|
|
(.rate_limits.five_hour.resets_at // ""),
|
|
(.rate_limits.seven_day.resets_at // "")
|
|
] | map(tostring) | join("")'
|
|
)
|
|
[ -z "$model" ] && model="unknown"
|
|
|
|
# --- model (compact) + fast-mode flag ---
|
|
model="${model%% (*}" # "Opus 4.8 (1M context)" -> "Opus 4.8"
|
|
[ "$fast" = "true" ] && model="⚡$model"
|
|
|
|
# --- context % (colored) + absolute input tokens ---
|
|
if [ -n "$used_pct" ]; then
|
|
ctx_seg=$(color_pct "$used_pct" "ctx:$(printf '%.0f%%' "$used_pct")")
|
|
else
|
|
ctx_seg="ctx:--"
|
|
fi
|
|
if [ "${input_tok:-0}" -ge 1000 ] 2>/dev/null; then
|
|
toks=$(awk "BEGIN{printf \"%.0fk\", ${input_tok}/1000}")
|
|
else
|
|
toks="${input_tok:-0}"
|
|
fi
|
|
|
|
# --- per-session cost (Claude Code's own cache/model-aware accounting) ---
|
|
# adaptive precision: whole dollars once it's real money, cents when small.
|
|
cost=$(awk "BEGIN{c=${cost_usd:-0}; if(c>=100) printf \"%.0f\",c; else if(c>=10) printf \"%.1f\",c; else printf \"%.2f\",c}")
|
|
|
|
# --- rate limits (each % colored on the same thresholds, + reset countdown) ---
|
|
_rl() { # $1=pct $2=label $3=reset_ts -> "<label>:NN%·<reset>"
|
|
local seg r; seg=$(color_pct "$1" "$2:$(printf '%.0f' "$1")%")
|
|
r=$(reset_in "$3"); [ -n "$r" ] && seg="$seg·$r"
|
|
printf '%s' "$seg"
|
|
}
|
|
rate=""
|
|
[ -n "$five_pct" ] && rate=$(_rl "$five_pct" "5h" "$five_reset")
|
|
if [ -n "$week_pct" ]; then
|
|
wk=$(_rl "$week_pct" "7d" "$week_reset")
|
|
[ -n "$rate" ] && rate="$rate "
|
|
rate="${rate}${wk}"
|
|
fi
|
|
|
|
# --- project tag + git state (branch, dirty, unpushed) ---
|
|
proj=""; gitseg=""
|
|
if [ -n "$cwd" ]; then
|
|
proj=$(basename "$cwd")
|
|
if git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
|
|
br=$(git -C "$cwd" branch --show-current 2>/dev/null)
|
|
[ -z "$br" ] && br=$(git -C "$cwd" rev-parse --short HEAD 2>/dev/null)
|
|
dirty=$(git -C "$cwd" status --porcelain 2>/dev/null | grep -c .)
|
|
ahead=$(git -C "$cwd" rev-list --count '@{upstream}..HEAD' 2>/dev/null)
|
|
gitseg="⎇${br:-?}"
|
|
[ "${dirty:-0}" -gt 0 ] 2>/dev/null && gitseg="$gitseg *$dirty"
|
|
[ -n "$ahead" ] && [ "$ahead" -gt 0 ] 2>/dev/null && gitseg="$gitseg ↑$ahead"
|
|
fi
|
|
fi
|
|
|
|
# --- althing: unread count (📬 N) + waiter-armed (🔔 armed / 🔕 not) ---
|
|
# v3 (the post office, 2026-08-28). ⚠ This block used to gate on
|
|
# `command -v althing-cli`, which the v3 cutover DELETED -- so the whole segment,
|
|
# badge and bell both, silently disappeared for every session on this box. That is
|
|
# worse than a cosmetic loss: 71 of 73 handles are pull-only (no waiter armed, never
|
|
# poked), and this badge is the ONLY out-of-band signal telling such a session it has
|
|
# mail waiting. A dead statusline made the new bus look like an empty one.
|
|
althing=""; mon=""
|
|
if command -v postbox >/dev/null 2>&1; then
|
|
# postbox has NO default post-office address and the statusline runs in a bare
|
|
# shell with neither var set. Hardcoded here deliberately: an unset address makes
|
|
# postbox error, which in a must-never-crash segment is indistinguishable from
|
|
# "no mail" -- the exact conflation v3 exists to prevent.
|
|
export ALTHING_POST_OFFICE="${ALTHING_POST_OFFICE:-http://10.100.50.40:8390}"
|
|
h="${ALTHING_HANDLE:-}"
|
|
# ALTHING_HANDLE isn't set in the statusline env, so resolve the handle from the
|
|
# cwd Claude Code passes on stdin.
|
|
#
|
|
# ⚠ PREFER launch-history.json. session_handles.json is a **v2 artifact** — v3's
|
|
# postbox never opens it (`grep -rn session_handles althing/` is empty; resolve_config
|
|
# takes --handle then ALTHING_HANDLE and nothing else), and the tool that used to
|
|
# maintain it, `althing-cli use`, was deleted at the cutover. Whatever is in it now is
|
|
# hand-kept and drifts silently.
|
|
#
|
|
# launch-history.json is written by dev_launch, which is the thing that sets
|
|
# ALTHING_HANDLE in the first place, so it is the real cwd->handle binding. Shape is
|
|
# {cwd: {command: {at, handle}}} with several commands per directory (claude, kimi,
|
|
# grok), so take the most recent by `at` rather than whichever key sorts first.
|
|
if [ -z "$h" ] && [ -n "$cwd" ]; then
|
|
h=$(jq -r --arg d "$cwd" '(.[$d] // {}) | to_entries | max_by(.value.at) | .value.handle // empty' \
|
|
"$HOME/.althing/launch-history.json" 2>/dev/null)
|
|
# Fallback only: broader coverage, but frozen and hand-maintained.
|
|
[ -z "$h" ] && h=$(jq -r --arg d "$cwd" '.[$d] // empty' "$HOME/.althing/session_handles.json" 2>/dev/null)
|
|
fi
|
|
if [ -n "$h" ]; then
|
|
# `timeout` is load-bearing, not belt-and-braces: v2 read a local SQLite file,
|
|
# v3 makes an HTTP call. An unreachable post office must cost this segment two
|
|
# seconds and nothing else — a statusline that hangs blocks the whole prompt.
|
|
unread=$(timeout 2 postbox --handle "$h" status --json 2>/dev/null </dev/null | jq -r '.unread // 0' 2>/dev/null)
|
|
case "${unread:-0}" in ''|0|*[!0-9]*) : ;; *) althing="📬 $unread" ;; esac
|
|
# v3 has ONE arming mechanism where v2 had three: `althing-listen` takes
|
|
# wake-listener-<handle>.lock. monitor-*.lock and light-monitor-*.lock belonged
|
|
# to binaries that no longer exist. kill -0 discards a crashed listener's lock.
|
|
mon="🔕"
|
|
lk="$HOME/.althing/wake-listener-$h.lock"
|
|
if [ -f "$lk" ]; then
|
|
pid=$(tr -dc '0-9' < "$lk" 2>/dev/null)
|
|
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && mon="🔔"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- assemble ---
|
|
lead="$althing"
|
|
[ -n "$mon" ] && lead="${lead:+$lead }$mon"
|
|
pg="$proj"
|
|
[ -n "$gitseg" ] && pg="${pg:+$pg }$gitseg"
|
|
|
|
parts="$lead"
|
|
[ -n "$pg" ] && parts="${parts:+$parts | }$pg"
|
|
parts="${parts:+$parts | }$model | $ctx_seg $toks | \$$cost"
|
|
[ -n "$rate" ] && parts="$parts | $rate"
|
|
|
|
printf '%s' "$parts"
|