diff --git a/claude/statusline.isaacaudet.sh b/claude/statusline.isaacaudet.sh index 22927e8..928cb56 100755 --- a/claude/statusline.isaacaudet.sh +++ b/claude/statusline.isaacaudet.sh @@ -38,16 +38,12 @@ CACHE_DIR="${STATUSLINE_CACHE_DIR:-/tmp/claude}" # # To override detection entirely, set TERM_WIDTH in settings.json: # "command": "TERM_WIDTH=160 ~/.claude/statusline.sh" -# Read both settings this script needs in one jq pass. # Only the user-level file: Claude Code also merges project .claude/settings.json, # .claude/settings.local.json and managed policy, so a statusLine.padding set at # project level would be applied by the renderer but missed here. sl_padding=0 -sl_thinking=false if [ -f "$HOME/.claude/settings.json" ]; then - { IFS= read -r sl_padding; IFS= read -r sl_thinking; } </dev/null) -EOF + sl_padding=$(jq -r '.statusLine.padding // 0' "$HOME/.claude/settings.json" 2>/dev/null) [ "${sl_padding:-0}" -ge 0 ] 2>/dev/null || sl_padding=0 fi @@ -321,8 +317,27 @@ used_tokens=$(format_tokens $current) total_tokens=$(format_tokens $size) pct_used=$(( size > 0 ? current * 100 / size : 0 )) -thinking_on=false -[ "$sl_thinking" = "true" ] && thinking_on=true +# Thinking and effort come from the payload, not from alwaysThinkingEnabled in +# settings.json: Option+T toggles thinking for the session only and never +# writes the file, so a settings read reports the wrong state for the rest of +# the session. Absent means enabled, mirroring Claude Code's own +# `thinking:{enabled: lt !== false}`. +# Both in one jq pass: the status line re-renders on every redraw, so a +# process per field is the dominant cost (see the rate-limit section below). +# `== false` rather than `// false`, which jq's falsy `//` would flip to true. +{ IFS= read -r sl_thinking; IFS= read -r effort; } < used/total pct% │ ◇ thinking │ $cost + # model │ ⎇ branch ✔ ↑1 │ used/total pct% │ ◇ high │ $cost # 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 # than assumed: a flat 56 columns was three short of a four-digit cost, @@ -429,7 +444,7 @@ if $SHOW_GIT && [ -n "$cwd" ]; then [ "${g_behind:-0}" -gt 0 ] && tail_len=$(( tail_len + 2 + ${#g_behind} )) $SHOW_TOKENS && tail_len=$(( tail_len + 3 + bar_w + 1 \ + ${#used_tokens} + 1 + ${#total_tokens} + 1 + ${#pct_used} + 1 )) - $SHOW_THINKING && tail_len=$(( tail_len + 3 + 10 )) + $SHOW_THINKING && tail_len=$(( tail_len + 3 + 2 + ${#thinking_label} )) [ -n "$cost_fmt" ] && tail_len=$(( tail_len + 3 + 1 + ${#cost_fmt} )) local_max=$(( USABLE_WIDTH - $(vis_len "$out") - tail_len )) [ "$local_max" -lt 8 ] && local_max=8 @@ -459,18 +474,19 @@ if $SHOW_TOKENS; then out+="${sep}${token_bar} ${orange}${used_tokens}${dim}/${reset}${white}${total_tokens}${reset} ${dim}${pct_used}%${reset}" fi -# Thinking: -# full/wide → "◆ thinking" / "◇ thinking" (label) -# split → "◆" / "◇" (symbol only, saves ~9 chars) +# Thinking and effort. The diamond is thinking state, the label is the effort +# level ("thinking" only when the payload reports no level): +# full/wide → "◆ high" / "◇ high" (label) +# split → "◆" / "◇" (symbol only, saves the label's columns) # narrow → hidden if $SHOW_THINKING && [ "$width_tier" != "narrow" ]; then out+="${sep}" if $thinking_on; then if [ "$width_tier" = "split" ]; then out+="${amber}◆${reset}" - else out+="${amber}◆ thinking${reset}"; fi + else out+="${amber}◆ ${thinking_label}${reset}"; fi else if [ "$width_tier" = "split" ]; then out+="${dim}◇${reset}" - else out+="${dim}◇ thinking${reset}"; fi + else out+="${dim}◇ ${thinking_label}${reset}"; fi fi fi diff --git a/claude/tests/run.sh b/claude/tests/run.sh index 75bef2d..a195446 100755 --- a/claude/tests/run.sh +++ b/claude/tests/run.sh @@ -11,6 +11,7 @@ cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1 status=0 for t in statusline.isaacaudet.payload_test.sh \ + statusline.isaacaudet.thinking_test.sh \ statusline.isaacaudet.width_test.sh \ statusline.isaacaudet.cache_test.sh; do echo "== $t" diff --git a/claude/tests/statusline.isaacaudet.thinking_test.sh b/claude/tests/statusline.isaacaudet.thinking_test.sh new file mode 100755 index 0000000..203edf7 --- /dev/null +++ b/claude/tests/statusline.isaacaudet.thinking_test.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Tests for the thinking/effort segment of claude/statusline.isaacaudet.sh. +# +# The state comes from the payload Claude Code pipes in (.thinking.enabled and +# .effort.level), not from alwaysThinkingEnabled in settings.json: the Option+T +# toggle is session-only, so a settings read cannot track it. Claude Code's own +# payload builder is `thinking:{enabled: lt !== false}`, so an absent field +# means enabled -- which is why the missing-field cases below expect the filled +# diamond rather than the hollow one. +SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/statusline.isaacaudet.sh" +# Widths below are USABLE widths, converted to a TERM_WIDTH here. The script +# tiers on USABLE_WIDTH (columns minus padding both sides minus a margin), so a +# raw TERM_WIDTH would land in a different tier 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) +OVERHEAD=$(( 2 * PAD + 1 )) +export STATUSLINE_CACHE_DIR="$(mktemp -d)" +CACHE="$STATUSLINE_CACHE_DIR/statusline-usage-cache.json" +REPO="$(mktemp -d)" + +cleanup() { rm -rf "$STATUSLINE_CACHE_DIR" "$REPO"; } +trap cleanup EXIT + +# 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. +cat > "$CACHE" <<'JSON' +{"five_hour":{"utilization":5.0,"resets_at":"2026-08-27T15:40:00+00:00"}, + "seven_day":{"utilization":7.0,"resets_at":"2026-08-28T02:00:00+00:00"}, + "extra_usage":{"is_enabled":false}, + "limits":[]} +JSON + +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 + +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)); } + +# Build the payload in python so a thinking/effort block can be omitted +# entirely -- absent and false are different states here. +stdin_json() { + CWD="$REPO" THINKING="$1" EFFORT="$2" python3 -c ' +import json, os +p = {"model": {"display_name": "Opus 5"}, + "cwd": os.environ["CWD"], + "cost": {"total_cost_usd": 0.5}, + "context_window": {"context_window_size": 200000, + "current_usage": {"input_tokens": 1000}}} +t = os.environ["THINKING"] +if t: p["thinking"] = {"enabled": t == "true"} +e = os.environ["EFFORT"] +if e: p["effort"] = {"level": e} +print(json.dumps(p))' +} + +# $1 = thinking ("true"/"false"/"" for absent), $2 = effort level or "", +# $3 = USABLE width. ANSI stripped. +render() { + stdin_json "$1" "$2" \ + | TERM_WIDTH="$(( ${3:-200} + OVERHEAD ))" bash "$SCRIPT" 2>&1 | sed $'s/\033\[[0-9;]*m//g' +} + +has() { case "$(render "$1" "$2" "$4")" in *"$3"*) ok "$5";; *) bad "$5" "want '$3' in: $(render "$1" "$2" "$4")";; esac; } +lacks() { case "$(render "$1" "$2" "$4")" in *"$3"*) bad "$5" "unwanted '$3' in: $(render "$1" "$2" "$4")";; *) ok "$5";; esac; } + +echo "Thinking state from the payload:" +has true high "◆ high" 200 "enabled + high effort renders a filled diamond and the level" +has false low "◇ low" 200 "disabled renders a hollow diamond" +has "" high "◆ high" 200 "absent thinking field means enabled, matching Claude Code's default" + +echo +echo "Effort level as the label:" +has true medium "◆ medium" 200 "medium is spelled out" +has true xhigh "◆ xhigh" 200 "xhigh is spelled out" +has true max "◆ max" 200 "max is spelled out" +lacks true high "thinking" 200 "the static word 'thinking' is gone when an effort level is known" + +echo +echo "Fallbacks and tiers:" +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 high "◆ high" 120 "the label survives the wide tier" +# The split tier (76-99) is unreachable while WRAP_NARROW=true -- that band is +# overridden to wide and wrapped -- so only narrow is asserted here. +lacks true high "◆" 60 "narrow tier hides the segment" + +echo +echo " $pass passed, $fail failed" +[ "$fail" -eq 0 ]