Files
claude/hooks/statusline.sh
T
bastien 0d035fcab6 feat(profile): default profile = full; reset applies it, current is label-driven
No profile selected (.active-profile absent, empty or legacy "none") now
means the `full` profile is in force: DEFAULT_PROFILE declared once in
lib/profile.sh, resolved by active_profile(); the statusline reads the
constant and shows `full` instead of `?`; `gstack off` trims to it instead
of erroring. `reset` goes to the default profile (= `set full`: enables its
list, parks any non-listed gstack or managed item). `current` names the
active label and scores that profile only, saying `default — not applied
yet` until a set/apply/reset wrote the cache; the "none" sentinel and the
cross-profile best-guess scan are gone (they keyed on the parked-gstack
count, which says nothing under BDR-030's gstack-off default). Hermetic
suite lib/tests/profile-default.test.sh (29 checks) seeds gstack as OFF like
a real tree. Citers updated: profile SKILL, Makefile help, plugin-advisor
PROFILE line + reset paragraph, toggle-external header.
2026-09-25 16:28:21 +02:00

84 lines
2.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Claude Code statusline — model, folder, branch, profile, effort, context bar, session start
# Receives JSON on stdin from Claude Code.
INPUT=$(cat)
MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "?"')
DIR=$(echo "$INPUT" | jq -r '.cwd // "?"')
FOLDER="${DIR##*/}"
PCT=$(echo "$INPUT" | jq -r \
'.context_window.used_percentage // 0' \
| cut -d. -f1)
# Git branch (fast, no network)
BRANCH=""
if [ -d "$DIR" ]; then
BRANCH=$(git -C "$DIR" branch --show-current 2>/dev/null)
fi
BRANCH_STR="${BRANCH:+ ($BRANCH)}"
REPO="$(cd -P "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Default profile — same constant as lib/profile.sh's active_profile(),
# read without sourcing the lib (speed). Unreadable lib → literal fallback.
DEFAULT_PROFILE=$(sed -n 's/^DEFAULT_PROFILE="\([^"]*\)".*/\1/p' "$REPO/lib/profile.sh" 2>/dev/null)
[ -n "$DEFAULT_PROFILE" ] || DEFAULT_PROFILE=full
# Active profile (written by lib/profile.sh set|apply|reset to <repo>/.active-profile).
# Read directly — `profile.sh current` is 12s+ and unusable in a statusline.
# Cache absent/empty/legacy "none" (same rule as active_profile()) → default.
PROFILE=$(head -n1 "$REPO/.active-profile" 2>/dev/null | tr -d '[:space:]')
if [ -z "$PROFILE" ] || [ "$PROFILE" = "none" ]; then
PROFILE="$DEFAULT_PROFILE"
fi
# Effort level from settings.json (.effortLevel — set by /effort or manual edit).
# settings.json is the source-of-truth, symlinked into ~/.claude/settings.json.
EFFORT="?"
if [ -f "$REPO/settings.json" ]; then
EFFORT=$(jq -r '.effortLevel // "?"' "$REPO/settings.json" 2>/dev/null)
[ -z "$EFFORT" ] && EFFORT="?"
fi
# Session duration (from total_duration_ms)
DURATION_MS=$(echo "$INPUT" | jq -r \
'.cost.total_duration_ms // 0' | cut -d. -f1)
DURATION_S=$((DURATION_MS / 1000))
if [ "$DURATION_S" -ge 3600 ]; then
DURATION="$((DURATION_S / 3600))h$((DURATION_S % 3600 / 60))m"
elif [ "$DURATION_S" -ge 60 ]; then
DURATION="$((DURATION_S / 60))m"
else
DURATION="<1m"
fi
# Progress bar (20 chars wide)
WIDTH=20
FILLED=$((PCT * WIDTH / 100))
EMPTY=$((WIDTH - FILLED))
if [ "$FILLED" -gt 0 ]; then
printf -v FILL "%${FILLED}s"
else
FILL=""
fi
if [ "$EMPTY" -gt 0 ]; then
printf -v PAD "%${EMPTY}s"
else
PAD=""
fi
BAR="${FILL// /█}${PAD// /░}"
# Color: green <50%, yellow 50-79%, red >=80%
if [ "$PCT" -ge 80 ]; then
COLOR="\033[31m"
elif [ "$PCT" -ge 50 ]; then
COLOR="\033[33m"
else
COLOR="\033[32m"
fi
RESET="\033[0m"
# Output: single line
echo -e "$MODEL | $FOLDER${BRANCH_STR} | profile: ${PROFILE} | effort: ${EFFORT} | ${COLOR}${BAR}${RESET} ${PCT}% | ${DURATION}"