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%.
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.
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:
cache_creation_input_tokens del primer mensaje
assistant con valor > 0. En el primer turno se cachea el system prompt + tools
msgs = (cache_read + cache_creation + fresh) - sysctx_size - total_usedLa 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.
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:
first_cache_creation.deque(maxlen=30) con las últimas 30 líneas crudas
durante el recorrido. Al terminar, parsear solo esas para obtener el
last_usage.{ 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.
Antes de cerrar, se hizo una revisión sistemática del script completo:
| # | Problema | Fix |
|---|---|---|
| 1 | GREEN y out_tok definidos pero nunca usados | Eliminados |
| 2 | echo "$input"= puede malinterpretar flags (-n=, -e) | Reemplazado por =printf ‘%s\n’ “$input”= |
| 3 | 7 llamadas separadas a jq (7 subprocesos) | Batched en una sola llamada con salida TSV |
| 4 | Sin límite de líneas: transcripts grandes agotan el timeout | Estrategia cabeza+cola+string matching |
| 5 | first_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"
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.
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:
█ — overhead de sistema (system prompt + tools + skills)█ — mensajes de la conversación░ — espacio libre╎ — marcador del 40% (umbral de advertencia)/context no está disponible en el JSON de la
statusline; hay que aproximarlo desde el transcriptfirst_cache_creation es útil y visualmente
informativa aunque no sea exacta