feat(claude): update statusline
This commit is contained in:
@@ -10,7 +10,18 @@
|
|||||||
"hooks": {},
|
"hooks": {},
|
||||||
"statusLine": {
|
"statusLine": {
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "sh \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline-command.sh\""
|
"command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/statusline-command.sh\""
|
||||||
|
},
|
||||||
|
"enabledPlugins": {
|
||||||
|
"typesafe@typesafe-ai": true
|
||||||
|
},
|
||||||
|
"extraKnownMarketplaces": {
|
||||||
|
"typesafe-ai": {
|
||||||
|
"source": {
|
||||||
|
"source": "github",
|
||||||
|
"repo": "typesafe-ai/skills"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"effortLevel": "high",
|
"effortLevel": "high",
|
||||||
"tui": "default",
|
"tui": "default",
|
||||||
|
|||||||
188
claude/statusline-command.sh
Normal file → Executable file
188
claude/statusline-command.sh
Normal file → Executable file
@@ -1,52 +1,158 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
|
# NOTE:(@janezicmatej) layout adapted from github.com/nejcr/grind statusline.js:
|
||||||
|
# model + context, repo, 5h/7d limits, session id, optional dev-links row
|
||||||
|
|
||||||
input=$(cat)
|
input=$(cat)
|
||||||
|
|
||||||
model=$(echo "$input" | jq -r '.model.display_name // empty')
|
# one jq call emits every field as a quoted shell assignment
|
||||||
total_in=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
|
eval "$(jq -r '
|
||||||
total_out=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
|
def s: tostring | @sh;
|
||||||
ctx=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
|
"model=\(.model.display_name // "" | s)",
|
||||||
five_hr=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
|
"used=\(.context_window.total_input_tokens // 0 | s)",
|
||||||
five_hr_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
|
"total=\(.context_window.context_window_size // 0 | s)",
|
||||||
seven_day=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
|
"ctx_pct=\(.context_window.used_percentage // "" | s)",
|
||||||
seven_day_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
|
"cwd=\(.workspace.current_dir // "" | s)",
|
||||||
|
"project_dir=\(.workspace.project_dir // "" | s)",
|
||||||
|
"session_id=\(.session_id // "" | s)"
|
||||||
|
' <<<"$input")"
|
||||||
|
|
||||||
# format seconds until reset as human-readable
|
# claude only sends rate_limits after the first api response, so a fresh session
|
||||||
fmt_remaining() {
|
# would show none until its first prompt; every refresh that has them writes the
|
||||||
now=$(date +%s)
|
# last known limits next to the config, and a fresh session reads those back dimmed
|
||||||
diff=$(( $1 - now ))
|
cfg_dir=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
|
||||||
[ "$diff" -le 0 ] && return
|
limits_cache=$cfg_dir/statusline-limits.json
|
||||||
h=$(( diff / 3600 ))
|
read_limits() {
|
||||||
m=$(( (diff % 3600) / 60 ))
|
jq -r '
|
||||||
if [ "$h" -gt 0 ]; then
|
def s: tostring | @sh;
|
||||||
printf '%dh%dm' "$h" "$m"
|
"five_pct=\(.five_hour.used_percentage // "" | s)",
|
||||||
else
|
"five_reset=\(.five_hour.resets_at // "" | s)",
|
||||||
printf '%dm' "$m"
|
"seven_pct=\(.seven_day.used_percentage // "" | s)",
|
||||||
fi
|
"seven_reset=\(.seven_day.resets_at // "" | s)"
|
||||||
|
' 2>/dev/null
|
||||||
|
}
|
||||||
|
current=$(jq -c '.rate_limits // {} | with_entries(select(.value.used_percentage != null))' <<<"$input")
|
||||||
|
stale=""
|
||||||
|
if [[ $current != "{}" ]]; then
|
||||||
|
eval "$(read_limits <<<"$current")"
|
||||||
|
# merged, not replaced: a turn carrying one window must not wipe the other;
|
||||||
|
# written then renamed so a concurrent session never reads it half-written
|
||||||
|
merged=$(jq -c --argjson cur "$current" '. * $cur' "$limits_cache" 2>/dev/null) || merged=$current
|
||||||
|
tmp=$limits_cache.$$.tmp
|
||||||
|
if { printf '%s\n' "$merged" > "$tmp"; } 2>/dev/null; then mv -f "$tmp" "$limits_cache" 2>/dev/null || rm -f "$tmp"; fi
|
||||||
|
elif [[ -f $limits_cache ]]; then
|
||||||
|
stale=1
|
||||||
|
eval "$(read_limits < "$limits_cache")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
R=$'\e[0m' C=$'\e[36m' G=$'\e[32m' Y=$'\e[33m' RE=$'\e[31m' D=$'\e[2m'
|
||||||
|
|
||||||
|
# "Claude 3.5 Sonnet" -> "Sonnet 3.5", "Claude Fable 5.1" -> "Fable 5.1"
|
||||||
|
model=$(sed -E -e 's/^Claude +([0-9.]+) +([A-Za-z0-9_]+)$/\2 \1/; t' -e 's/^Claude +//' <<<"${model:-unknown}")
|
||||||
|
|
||||||
|
# 61234 -> 61.2k, half-up rounding to match Math.round
|
||||||
|
fmt() { awk -v n="$1" 'BEGIN { if (n >= 1000) printf "%.1fk", n / 1000; else printf "%d", n }'; }
|
||||||
|
round() { awk -v x="$1" 'BEGIN { printf "%d", int(x + 0.5) }'; }
|
||||||
|
|
||||||
|
# same scale for context and limits: green under 50, yellow under 80, red above
|
||||||
|
color_for() {
|
||||||
|
if (( $1 >= 80 )); then printf %s "$RE"; elif (( $1 >= 50 )); then printf %s "$Y"; else printf %s "$G"; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
parts=""
|
# repo: origin url from the host onwards (host/group/repo), else the folder name
|
||||||
|
repo=""
|
||||||
[ -n "$model" ] && parts="$model"
|
if url=$(git -C "$cwd" --no-optional-locks remote get-url origin 2>/dev/null); then
|
||||||
|
repo=$(sed -E \
|
||||||
if [ -n "$total_in" ] && [ "$total_in" != "0" ]; then
|
-e 's#^[^@/]+@([^:/]+):(.+)$#\1/\2#; t done' \
|
||||||
total=$((total_in + total_out))
|
-e 's#^[a-z+]+://([^@/]+@)?([^/:]+)(:[0-9]+)?/(.+)$#\2/\4#I; t done' \
|
||||||
parts="${parts:+$parts | }tokens:${total}"
|
-e 's/.*//' -e ':done' -e 's#/+$##; s/\.git$//' <<<"$url")
|
||||||
|
fi
|
||||||
|
if [[ -z $repo ]]; then
|
||||||
|
top=$(git -C "$cwd" --no-optional-locks rev-parse --show-toplevel 2>/dev/null)
|
||||||
|
repo=$(basename "${top:-${project_dir:-$cwd}}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$ctx" ]; then
|
# limits, same shape as the context group: "5h 42% (15:36)" and "7d 81% (5d)"
|
||||||
parts="${parts:+$parts | }ctx:$(printf '%.0f' "$ctx")%"
|
# the 5h window resets within hours so the clock answers "when", the 7d one is
|
||||||
fi
|
# far enough out that days are all that matters; a rolled-over window shows a dash
|
||||||
|
now=$(date +%s)
|
||||||
|
rl_part() {
|
||||||
|
local label=$1 pct=$2 reset=${3%.*} mode=$4 p s left
|
||||||
|
[[ -n $pct ]] || return 0
|
||||||
|
s=$(( ${reset:-0} - now ))
|
||||||
|
if [[ -z $reset ]] || (( s <= 0 )); then
|
||||||
|
printf '%s%s —%s' "$D" "$label" "$R"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
p=$(round "$pct")
|
||||||
|
case $mode in
|
||||||
|
clock) left=$(date -d "@$reset" +%H:%M) ;;
|
||||||
|
days) if (( s < 86400 )); then left="$(( (s + 3599) / 3600 ))h"; else left="$(( (s + 43200) / 86400 ))d"; fi ;;
|
||||||
|
esac
|
||||||
|
# last known values from the cache render fully dim to say they may be behind
|
||||||
|
local col; col=$(color_for "$p"); [[ -n $stale ]] && col=$D
|
||||||
|
printf '%s%s%s %s%s%%%s %s(%s)%s' "$D" "$label" "$R" "$col" "$p" "$R" "$D" "$left" "$R"
|
||||||
|
}
|
||||||
|
|
||||||
if [ -n "$five_hr" ]; then
|
groups=()
|
||||||
five_hr_ttl=""
|
|
||||||
[ -n "$five_hr_reset" ] && five_hr_ttl=$(fmt_remaining "$five_hr_reset")
|
|
||||||
parts="${parts:+$parts | }5h:$(printf '%.0f' "$five_hr")%${five_hr_ttl:+($five_hr_ttl)}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$seven_day" ]; then
|
session="${C}${model}${R} ${G}$(fmt "$used")/$(fmt "$total")${R}"
|
||||||
seven_day_ttl=""
|
if [[ -n $ctx_pct ]]; then
|
||||||
[ -n "$seven_day_reset" ] && seven_day_ttl=$(fmt_remaining "$seven_day_reset")
|
p=$(round "$ctx_pct")
|
||||||
parts="${parts:+$parts | }7d:$(printf '%.0f' "$seven_day")%${seven_day_ttl:+($seven_day_ttl)}"
|
session+=" ${D}(${R}$(color_for "$p")${p}%${R}${D})${R}"
|
||||||
fi
|
fi
|
||||||
|
groups+=("$session")
|
||||||
|
|
||||||
[ -n "$parts" ] && printf '%s' "$parts"
|
[[ -n $repo ]] && groups+=("${C}${repo}${R}")
|
||||||
|
|
||||||
|
# limits are only sent once the session has talked to the api
|
||||||
|
part=$(rl_part 5h "$five_pct" "$five_reset" clock); [[ -n $part ]] && groups+=("$part")
|
||||||
|
part=$(rl_part 7d "$seven_pct" "$seven_reset" days); [[ -n $part ]] && groups+=("$part")
|
||||||
|
|
||||||
|
# full session id so it can be pasted into claude --resume, dim and last
|
||||||
|
[[ -n $session_id ]] && groups+=("${D}${session_id}${R}")
|
||||||
|
|
||||||
|
sep=" ${D}│${R} "
|
||||||
|
out=""
|
||||||
|
for g in "${groups[@]}"; do out+="${out:+$sep}$g"; done
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
|
||||||
|
# pinned dev links, opt-in per project: .claude/dev-links.json adds a second row
|
||||||
|
# a value is either "url" or {url, note}, note being a throwaway local credential
|
||||||
|
link_file=""
|
||||||
|
for d in "$project_dir" "$cwd"; do
|
||||||
|
[[ -n $d && -f $d/.claude/dev-links.json ]] && { link_file=$d/.claude/dev-links.json; break; }
|
||||||
|
done
|
||||||
|
[[ -n $link_file ]] || exit 0
|
||||||
|
|
||||||
|
pins=$(jq -r '
|
||||||
|
to_entries[]
|
||||||
|
| { label: .key,
|
||||||
|
url: (if (.value | type) == "string" then .value
|
||||||
|
elif (.value | type) == "object" then .value.url else null end),
|
||||||
|
note: (if (.value | type) == "object" and (.value.note | type) == "string" then .value.note else "" end) }
|
||||||
|
| select((.url | type) == "string" and (.url | test("^https?://")))
|
||||||
|
| [.label, .url, .note] | @tsv
|
||||||
|
' "$link_file" 2>/dev/null) || exit 0
|
||||||
|
[[ -n $pins ]] || exit 0
|
||||||
|
|
||||||
|
# bare urls rather than osc 8: claude code resolves plain http:// on ctrl+click
|
||||||
|
# itself; COLUMNS is the only readable width since stdout is captured
|
||||||
|
cols=${COLUMNS:-120}
|
||||||
|
[[ $cols =~ ^[0-9]+$ ]] || cols=120
|
||||||
|
gap=3
|
||||||
|
row="" width=0
|
||||||
|
while IFS=$'\t' read -r label url note; do
|
||||||
|
len=$(( ${#label} + 1 + ${#url} + (${#note} > 0 ? ${#note} + 3 : 0) ))
|
||||||
|
cost=$(( width ? len + gap : len ))
|
||||||
|
if (( width && width + cost > cols - 4 )); then
|
||||||
|
printf '%s\n' "$row"
|
||||||
|
row="" width=$len
|
||||||
|
else
|
||||||
|
width=$(( width + cost ))
|
||||||
|
fi
|
||||||
|
cell="${C}${label}${R} ${D}${url}${R}"
|
||||||
|
[[ -n $note ]] && cell+=" ${Y}${note}${R}"
|
||||||
|
row+="${row:+${D} · ${R}}$cell"
|
||||||
|
done <<<"$pins"
|
||||||
|
[[ -n $row ]] && printf '%s\n' "$row"
|
||||||
|
exit 0
|
||||||
|
|||||||
Reference in New Issue
Block a user