claude code statusline: barra de progreso de contexto por categorías

Contexto

Partiendo de la statusline con desglose de tokens (ver claude-statusline-costos-y-contexto.org), quisimos ir más lejos: mostrar el uso del contexto como una barra de progreso visual, desglosada por categorías semánticas (sistema, mensajes, espacio libre), con un marcador en el umbral de advertencia del 40%.

Exploración

¿El JSON de la statusline tiene desglose por categoría?

La primera pregunta fue si el JSON de entrada ya incluye el desglose que muestra /context (system prompt, tools, skills, messages). Para averiguarlo, capturamos el JSON a un archivo temporal:

input=$(cat)
echo "$input" > /tmp/statusline-debug.json

El JSON no incluye ese desglose. Solo tiene los totales por tipo de caché:

"current_usage": {
  "input_tokens": 1,
  "cache_creation_input_tokens": 880,
  "cache_read_input_tokens": 17000,
  "output_tokens": 8
}

La categorización de /context se computa internamente en Claude Code a partir del contenido del prompt construido en memoria — no está expuesta al exterior.

Aproximación desde el transcript

El JSON de la statusline sí incluye transcript_path, que apunta al .jsonl de la sesión. Cada entrada de tipo assistant tiene un campo usage con el desglose de tokens de esa llamada a la API.

La idea: parsear el transcript para estimar categorías:

La limitación conocida: el primer turno también cachea el primer mensaje de usuario, así que sys está ligeramente sobreestimado. Es una aproximación, no una medición exacta.

Rendimiento en transcripts grandes

Parsear el transcript completo con json.loads por línea puede ser lento para sesiones largas. La solución fue una estrategia de dos fases:

  1. Cabeza: parsear completamente solo las primeras 200 líneas, para encontrar el baseline de first_cache_creation.
  2. Cola: mantener un deque(maxlen=30) con las últimas 30 líneas crudas durante el recorrido. Al terminar, parsear solo esas para obtener el last_usage.
  3. Conteo de turnos: en lugar de parsear cada línea como JSON, hacer string matching sobre el texto crudo. Las líneas que empiezan con { y contienen =”type”:”user”= se cuentan directamente. Esto es ~10x más rápido que json.loads para el bulk del archivo.
for i, raw in enumerate(f):
    raw = raw.strip()
    recent.append(raw)

    # Conteo rápido sin parsear JSON
    if raw.startswith('{') and ('"type":"user"' in raw or '"type": "user"' in raw):
        turns += 1
        continue

    # Parseo completo solo para la cabeza
    if i < HEAD_LINES and first_cache_creation is None:
        ...

El timeout de 2 segundos es la red de seguridad final; si el archivo es gigante o el sistema está cargado, el category_line queda vacío y la statusline sigue funcionando.

Review de calidad (regla de 5)

Antes de cerrar, se hizo una revisión sistemática del script completo:

#ProblemaFix
1GREEN y out_tok definidos pero nunca usadosEliminados
2echo "$input"= puede malinterpretar flags (-n=, -e)Reemplazado por =printf ‘%s\n’ “$input”=
37 llamadas separadas a jq (7 subprocesos)Batched en una sola llamada con salida TSV
4Sin límite de líneas: transcripts grandes agotan el timeoutEstrategia cabeza+cola+string matching
5first_cache_creation tomaba el primer valor aunque fuera 0 (post-compactación)Ahora escanea hasta encontrar un valor > 0

El batching de jq usa IFS=$'\t' read -r sobre la salida separada por tabs:

_jq=$(printf '%s\n' "$input" | jq -r '
    (.model.display_name // "") + "\t" +
    (.workspace.current_dir // "") + "\t" +
    ...
')
IFS=$'\t' read -r model cwd used_pct fresh cache_w cache_r out \
    total_cost transcript_path ctx_size <<< "$_jq"

Barra de progreso con marcador de umbral

La barra se construye carácter a carácter para poder insertar el marcador del 40% en la posición visual correcta sin desplazar el ancho total:

LIMIT_POS = round(0.4 * BAR_WIDTH)  # posición 16 de 40

for pos in range(BAR_WIDTH):
    if pos == LIMIT_POS:
        bar_parts.append(RESET + WHITE + '\u254e' + RESET)  # ╎
        cur_color = ''
        continue
    if pos < sys_w:
        color, char = CYAN, '█'
    elif pos < sys_w + msg_w:
        color, char = YELLOW, '█'
    else:
        color, char = DIM, '░'
    ...

El carácter (U+254E, BOX DRAWINGS LIGHT DOUBLE DASH VERTICAL) en blanco brillante actúa como tick. Cuando el uso supera el 40%, el marcador queda “cubierto” por los bloques de color — el hecho de que desaparezca es en sí una señal visual.

Implementación final

Output de la statusline (dos líneas):

Sonnet 4.6 in sasha | ctx:9% i:1 w:880 r:17k o:8 hit:95% | $0.222
[██████████╎░░░░░░░░░░░░░░░░░░░░░░░░░░░] sys:~7k msgs:~20k free:171k (85%) turns:26

Colores:

Conclusiones

Referencias