Files
repo_Claude/.claude/statusline.sh
T
egutierrez fa2b2e16bc feat(statusline): refresco idle via refreshInterval + cache de git
El statusline solo se re-ejecutaba al cambiar los mensajes, asi que el estado de
reposo que el Stop hook escribe ~2s despues (en background) no se reflejaba hasta
que el usuario interactuaba. Se anade refreshInterval=2 para que el harness re-
ejecute el statusline cada 2s tambien estando idle, mostrando el valor final sin
necesidad de escribir y sin bloquear el turno.

Para que el refresco continuo no sea caro en repos grandes, el bloque git se
cachea por directorio con TTL de 6s (el estado git no cambia estando parado); los
ticks idle reusan el cache (~0.1s vs ~0.33s recomputando). El goal file (la fase)
se lee siempre fresco.

Se revierte el intento previo de Stop sincrono (bloqueaba ~2s el control).

Nota: Claude Code no soporta acotar el refresco a una ventana (p.ej. 10s y
parar); refreshInterval es continuo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 15:19:21 +02:00

342 lines
12 KiB
Bash
Executable File

#!/bin/bash
# Status Line para Claude Code - Optimizado para Vibecoding
# Muestra: modelo, contexto, git, costos, rate limits y más
# Leer JSON desde stdin
INPUT=$(cat)
# Colores ANSI
RESET='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'
CYAN='\033[36m'
GREEN='\033[32m'
YELLOW='\033[33m'
RED='\033[31m'
BLUE='\033[34m'
MAGENTA='\033[35m'
WHITE='\033[37m'
GRAY='\033[90m'
# Extraer datos del JSON
MODEL=$(echo "$INPUT" | jq -r '.model.display_name // "Unknown"')
CONTEXT_PCT=$(echo "$INPUT" | jq -r '.context_window.used_percentage // 0' | xargs printf "%.0f")
CONTEXT_TOTAL=$(echo "$INPUT" | jq -r '.context_window.context_window_size // 200000')
CURRENT_DIR=$(echo "$INPUT" | jq -r '.workspace.current_dir // "~"' | sed "s|$HOME|~|")
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
# Purga: borra goal files de sesiones muertas (no tocados en >7 dias). El worker
# refresca el mtime en cada respuesta, asi que las sesiones vivas nunca caen.
find "$HOME/.claude/goals" -maxdepth 1 -name '*.json' -mtime +7 -delete 2>/dev/null
# Tokens de entrada y salida (current_usage puede ser null antes del primer API call)
INPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.input_tokens // 0')
OUTPUT_TOKENS=$(echo "$INPUT" | jq -r '.context_window.current_usage.output_tokens // 0')
CACHE_READ=$(echo "$INPUT" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
CACHE_CREATION=$(echo "$INPUT" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
TOTAL_INPUT=$(echo "$INPUT" | jq -r '.context_window.total_input_tokens // 0')
TOTAL_OUTPUT=$(echo "$INPUT" | jq -r '.context_window.total_output_tokens // 0')
# Calcular contexto usado (suma de todos los tokens de entrada)
CONTEXT_USED=$((INPUT_TOKENS + CACHE_READ + CACHE_CREATION))
# Calcular porcentaje manualmente si el del JSON es 0
if [ "$CONTEXT_PCT" -eq 0 ] && [ "$CONTEXT_USED" -gt 0 ]; then
CONTEXT_PCT=$(echo "scale=0; $CONTEXT_USED * 100 / $CONTEXT_TOTAL" | bc)
fi
# Costos
TOTAL_COST=$(echo "$INPUT" | jq -r '.cost.total_cost_usd // 0' | xargs printf "%.3f")
SESSION_DURATION=$(echo "$INPUT" | jq -r '.cost.total_duration_ms // 0')
LINES_ADDED=$(echo "$INPUT" | jq -r '.cost.total_lines_added // 0')
LINES_REMOVED=$(echo "$INPUT" | jq -r '.cost.total_lines_removed // 0')
# Rate Limits
RATE_5H=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.used_percentage // 0' | xargs printf "%.0f")
RATE_7D=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.used_percentage // 0' | xargs printf "%.0f")
RESET_5H_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.five_hour.resets_at // 0')
RESET_7D_EPOCH=$(echo "$INPUT" | jq -r '.rate_limits.seven_day.resets_at // 0')
# Formatear resets (vacio si epoch=0)
RESET_5H=""
RESET_7D=""
[ "$RESET_5H_EPOCH" -gt 0 ] && RESET_5H=$(date -d "@$RESET_5H_EPOCH" +"%H:%M" 2>/dev/null)
[ "$RESET_7D_EPOCH" -gt 0 ] && RESET_7D=$(date -d "@$RESET_7D_EPOCH" +"%a %H:%M" 2>/dev/null)
# Git info (si estamos en un repo). Con cache de TTL corto: como el statusline
# se re-ejecuta cada pocos segundos (refreshInterval), recomputar git en cada
# tick es caro en repos grandes y el estado git no cambia estando idle. Se cachea
# por directorio y se recomputa solo si el cache tiene mas de 6s.
GIT_BRANCH=""
GIT_STATUS=""
GIT_CACHE="/tmp/fn_sl_git_$(printf '%s' "$CURRENT_DIR" | cksum | cut -d' ' -f1).cache"
GIT_CACHE_AGE=999
[ -f "$GIT_CACHE" ] && GIT_CACHE_AGE=$(( $(date +%s) - $(stat -c %Y "$GIT_CACHE" 2>/dev/null || echo 0) ))
if [ "$GIT_CACHE_AGE" -lt 6 ]; then
. "$GIT_CACHE"
elif git rev-parse --git-dir > /dev/null 2>&1; then
GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
# Obtener archivos staged, modified, untracked
STAGED=$(git diff --cached --numstat 2>/dev/null | wc -l)
MODIFIED=$(git diff --numstat 2>/dev/null | wc -l)
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l)
# Commits ahead/behind
UPSTREAM=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)
if [ -n "$UPSTREAM" ]; then
AHEAD=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "0")
BEHIND=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "0")
else
AHEAD="0"
BEHIND="0"
fi
# Construir status compacto
GIT_STATUS=""
[ "$STAGED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}+${STAGED} "
[ "$MODIFIED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}~${MODIFIED} "
[ "$UNTRACKED" -gt 0 ] && GIT_STATUS="${GIT_STATUS}?${UNTRACKED} "
[ "$AHEAD" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${AHEAD} "
[ "$BEHIND" -gt 0 ] && GIT_STATUS="${GIT_STATUS}${BEHIND} "
# Trim trailing space
GIT_STATUS=$(echo "$GIT_STATUS" | sed 's/ $//')
# Guardar en cache (quoting seguro para re-source).
printf 'GIT_BRANCH=%q\nGIT_STATUS=%q\n' "$GIT_BRANCH" "$GIT_STATUS" > "$GIT_CACHE" 2>/dev/null
fi
# Color estable por sesión (hash del session_id → paleta ANSI 256 legible).
# Cada terminal mantiene su color toda su vida; distinto entre terminales.
goal_color() {
local sid="$1"
local palette=(39 45 51 75 81 114 120 156 183 210 215 222 213 159 228)
local h
h=$(printf '%s' "$sid" | cksum | cut -d' ' -f1)
local idx=$(( h % ${#palette[@]} ))
printf '\033[1;38;5;%dm' "${palette[$idx]}"
}
# Fase de trabajo → icono | color ANSI | etiqueta visible.
# El slug (clave) lo escribe el agente del Stop hook; aqui se mapea a su estilo.
phase_style() {
case "$1" in
investigando) printf '🔎|36|investigando' ;;
planificando) printf '📋|34|planificando' ;;
haciendo) printf '🔨|33|haciendo' ;;
testeando) printf '🧪|35|testeando' ;;
puliendo) printf '✨|95|puliendo detalles' ;;
pendiente_revision) printf '👀|93|pendiente de revisión' ;;
bloqueado) printf '⛔|31|bloqueado' ;;
en_pausa) printf '⏸️|90|en pausa' ;;
hecho) printf '✅|32|hecho' ;;
iterando) printf '🔁|94|iterando' ;;
*) printf "•|90|$1" ;;
esac
}
# Función para crear barra de progreso
progress_bar() {
local pct=$1
local width=10
local filled=$(( pct * width / 100 ))
local empty=$(( width - filled ))
# Color según porcentaje
local color=$GREEN
if [ "$pct" -ge 80 ]; then
color=$RED
elif [ "$pct" -ge 60 ]; then
color=$YELLOW
fi
printf "${color}"
for ((i=0; i<filled; i++)); do printf "█"; done
printf "${GRAY}"
for ((i=0; i<empty; i++)); do printf "░"; done
printf "${RESET}"
}
# Función para formatear duración
format_duration() {
local ms=$1
local seconds=$((ms / 1000))
local minutes=$((seconds / 60))
local hours=$((minutes / 60))
if [ "$hours" -gt 0 ]; then
printf "%dh%dm" $hours $((minutes % 60))
elif [ "$minutes" -gt 0 ]; then
printf "%dm%ds" $minutes $((seconds % 60))
else
printf "%ds" $seconds
fi
}
# Función para formatear tokens (k para miles, M para millones)
format_tokens() {
local tokens=$1
if [ "$tokens" -ge 1000000 ]; then
printf "%.1fM" $(echo "scale=1; $tokens / 1000000" | bc)
elif [ "$tokens" -ge 1000 ]; then
printf "%.0fk" $(echo "scale=0; $tokens / 1000" | bc)
else
printf "%d" $tokens
fi
}
# Hora actual
CURRENT_TIME=$(date +"%H:%M")
# ===== CONSTRUIR STATUS LINE (2 LÍNEAS) =====
# ===== LÍNEA 1: Información Principal =====
LINE1=""
# 1. Modelo
LINE1="${LINE1}${BOLD}${CYAN}${MODEL}${RESET}"
# 2. Contexto con barra y usado/total
CONTEXT_USED_FMT=$(format_tokens $CONTEXT_USED)
CONTEXT_TOTAL_FMT=$(format_tokens $CONTEXT_TOTAL)
LINE1="${LINE1} ${GRAY}${RESET} ${BOLD}CTX:${RESET} $(progress_bar $CONTEXT_PCT) ${YELLOW}${CONTEXT_PCT}%${RESET} ${DIM}(${CONTEXT_USED_FMT}/${CONTEXT_TOTAL_FMT})${RESET}"
# 3. Tokens entrada/salida (formateados)
INPUT_FMT=$(format_tokens $INPUT_TOKENS)
OUTPUT_FMT=$(format_tokens $OUTPUT_TOKENS)
LINE1="${LINE1} ${GRAY}${RESET} ${CYAN}IN:${RESET}${INPUT_FMT} ${MAGENTA}OUT:${RESET}${OUTPUT_FMT}"
if [ "$CACHE_READ" -gt 0 ]; then
CACHE_FMT=$(format_tokens $CACHE_READ)
LINE1="${LINE1} ${DIM}(cache:${CACHE_FMT})${RESET}"
fi
# 4. Git (si existe)
if [ -n "$GIT_BRANCH" ]; then
LINE1="${LINE1} ${GRAY}${RESET} ${BOLD}${MAGENTA}${GIT_BRANCH}${RESET}"
if [ -n "$GIT_STATUS" ]; then
LINE1="${LINE1} ${DIM}[${GIT_STATUS}]${RESET}"
fi
fi
# 5. Hora actual
LINE1="${LINE1} ${GRAY}${RESET} ${WHITE}${CURRENT_TIME}${RESET}"
# ===== LÍNEA 2: Detalles =====
LINE2=""
# 1. Costos
if (( $(echo "$TOTAL_COST > 0" | bc -l) )); then
LINE2="${LINE2}${BOLD}${GREEN}\$${TOTAL_COST}${RESET}"
else
LINE2="${LINE2}${DIM}\$0.000${RESET}"
fi
# 2. Ediciones
LINE2="${LINE2} ${GRAY}${RESET} ${GREEN}+${LINES_ADDED}${RESET}${GRAY}/${RED}-${LINES_REMOVED}${RESET}"
# 3. Tokens totales acumulados (formateados)
TOTAL_IN_FMT=$(format_tokens $TOTAL_INPUT)
TOTAL_OUT_FMT=$(format_tokens $TOTAL_OUTPUT)
LINE2="${LINE2} ${GRAY}${RESET} ${DIM}Total:${RESET} ${CYAN}${TOTAL_IN_FMT}${RESET}${GRAY}/${MAGENTA}${TOTAL_OUT_FMT}${RESET}"
# 4. Rate Limits (siempre mostrar)
LINE2="${LINE2} ${GRAY}${RESET} ${BOLD}Limits:${RESET}"
# Color por burndown vs tasa esperada
# Tasa: % consumible permitido por unidad de tiempo (5h: 20%/h, 7d: 14%/dia)
# expected = tasa * unidades_restantes_hasta_reset
# available = 100 - used%
# verde: available >= expected (consumo bajo control)
# amarillo: available >= expected/2 (consumo agresivo)
# rojo: available < expected/2 (riesgo de agotar antes del reset)
NOW_EPOCH=$(date +%s)
burndown_color() {
local used_pct=$1
local secs_left=$2
local rate=$3
local secs_per_unit=$4
local available=$((100 - used_pct))
if [ "$secs_left" -le 0 ]; then
printf "%s" "$GREEN"; return
fi
local expected
expected=$(echo "scale=2; $rate * $secs_left / $secs_per_unit" | bc)
if (( $(echo "$available >= $expected" | bc -l) )); then
printf "%s" "$GREEN"
elif (( $(echo "$available >= $expected / 2" | bc -l) )); then
printf "%s" "$YELLOW"
else
printf "%s" "$RED"
fi
}
# 5h limit (tasa 20%/h)
SECS_5H=0
[ "$RESET_5H_EPOCH" -gt "$NOW_EPOCH" ] && SECS_5H=$((RESET_5H_EPOCH - NOW_EPOCH))
C5=$(burndown_color $RATE_5H $SECS_5H 20 3600)
RESET_5H_STR=""
[ -n "$RESET_5H" ] && RESET_5H_STR=" ${C5}${RESET_5H}${RESET}"
LINE2="${LINE2} ${C5}5h:${RATE_5H}%${RESET}${RESET_5H_STR} ${GRAY}${RESET}"
# 7d limit (tasa 14%/dia)
SECS_7D=0
[ "$RESET_7D_EPOCH" -gt "$NOW_EPOCH" ] && SECS_7D=$((RESET_7D_EPOCH - NOW_EPOCH))
C7=$(burndown_color $RATE_7D $SECS_7D 14 86400)
RESET_7D_STR=""
[ -n "$RESET_7D" ] && RESET_7D_STR=" ${C7}${RESET_7D}${RESET}"
LINE2="${LINE2} ${C7}7d:${RATE_7D}%${RESET}${RESET_7D_STR}"
# 5. Duración sesión
if [ "$SESSION_DURATION" -gt 0 ]; then
DURATION_STR=$(format_duration $SESSION_DURATION)
LINE2="${LINE2} ${GRAY}${RESET} ${DIM}${DURATION_STR}${RESET}"
fi
# 6. Directorio actual
LINE2="${LINE2} ${GRAY}${RESET} ${BLUE}${CURRENT_DIR}${RESET}"
# ===== LÍNEA 0: Objetivo (izq) + Fase (der) =====
# Solo si la sesión tiene archivo de objetivo con goal no vacío.
GOAL_FILE="$HOME/.claude/goals/${SESSION_ID}.json"
if [ -n "$SESSION_ID" ] && [ -f "$GOAL_FILE" ]; then
GOAL=$(jq -r '.goal // ""' "$GOAL_FILE" 2>/dev/null)
PHASE=$(jq -r '.phase // ""' "$GOAL_FILE" 2>/dev/null)
if [ -n "$GOAL" ]; then
GC=$(goal_color "$SESSION_ID")
LEFT_PLAIN="🎯 ${GOAL}"
LEFT="${GC}${LEFT_PLAIN}${RESET}"
LINE0="${LEFT}"
# Historial: emojis de los ultimos 7 estados PREVIOS (sin el actual, que
# se muestra completo a la derecha), atenuados y separados por │.
PREV=$(jq -r '(.history // []) | .[0:-1] | .[-7:] | .[]' "$GOAL_FILE" 2>/dev/null)
if [ -n "$PREV" ]; then
HJOIN=""
while IFS= read -r slug; do
[ -z "$slug" ] && continue
HS=$(phase_style "$slug")
HIC="${HS%%|*}"
HJOIN="${HJOIN}${HIC}"
done <<< "$PREV"
[ -n "$HJOIN" ] && LINE0="${LINE0} ${GRAY}${RESET} ${DIM}${HJOIN}${RESET}"
fi
# Fase actual (completa, con color e icono).
if [ -n "$PHASE" ]; then
PS=$(phase_style "$PHASE")
PICON="${PS%%|*}"
REST="${PS#*|}"
PCOL="${REST%%|*}"
PLABEL="${REST#*|}"
LINE0="${LINE0} ${GRAY}${RESET} \033[1;${PCOL}m${PICON} ${PLABEL}${RESET}"
fi
echo -e "$LINE0"
fi
fi
# Imprimir resultado (2 líneas)
echo -e "$LINE1"
echo -e "$LINE2"