| Obě strany předchozí revize Předchozí verze | |
| ai:platformy:claude-code:statusline [08.05.2026 21:45] – Zjednodušení: ctx% čteno přímo z used_percentage. Doplněny poznatky z oficiální dokumentace (total_input_tokens zahrnuje cache, used_percentage existuje). Petr Nosek | ai:platformy:claude-code:statusline [07.07.2026 18:08] (aktuální) – Aktualizace článku o Claude Code status line a subagentStatusLine Petr Nosek |
|---|
| //Vytvořeno: **** | Aktualizováno: **~~LASTMOD~~**// | //Vytvořeno: **** | Aktualizováno: **~~LASTMOD~~**// |
| |
| [[ai:platformy:claude-code:start|Claude Code]] umí vykreslovat vlastní status line spuštěný jako externí příkaz. Tohle je moje varianta, která drží stabilní pořadí polí a na jeden pohled ukazuje pracovní adresář, model, session ID, využití kontextu, 5h a 7d limity i absolutní spotřebu tokenů context window se zvýrazněním nad hranicí 200k. | [[ai:platformy:claude-code:start|Claude Code]] umí podle [[https://code.claude.com/docs/en/statusline|oficiální dokumentace]] vykreslovat vlastní status line přes externí příkaz. Tohle je moje aktuální konfigurace: hlavní ''statusLine'' ukazuje stav top-level session a samostatná ''subagentStatusLine'' přidává přehled běžících subagentů v agent panelu. |
| |
| ===== Co z toho získám ===== | ===== Co z toho získám ===== |
| |
| * Aktuální pracovní adresář projektu (root, kde byl Claude Code spuštěn) zkrácený relativně k ''$HOME'', takže místo celé cesty vidím jen ''~'' nebo ''~/...''. Adresář se v průběhu session nemění, i když shell přejde do podadresáře. | * Hlavní status line ukazuje pracovní adresář, model, session ID, využití context window, 5h a 7d limity a absolutní spotřebu tokenů. |
| * Použitý model a session ID přímo ve status line. | * Subagent status line ukazuje každého subagenta jako vlastní řádek: agent, model, krátký popis, stav, tokeny, trend tokenů, rychlost tokenů za sekundu, běh od startu a případně pracovní adresář. |
| * Využití context window v procentech, čtené přímo z předpočítaného pole ''used_percentage''. | * Model subagenta se do panelu dostává přes konvenci v ''description'', protože Claude Code ho ve vstupních datech pro ''subagentStatusLine'' neposílá jako samostatné pole. |
| * 5hodinový a 7denní limit, včetně času resetu, pokud je Claude Code pošle v JSONu. | * Popisky subagentů jsou čitelné i při paralelním běhu více agentů. |
| * Absolutní spotřebu tokenů context window ve formátu ''XYZk/TOTALk''. Hodnota celkového okna se čte dynamicky, takže funguje i pro 1M context window. Červeně se rozsvítí při překročení 200k tokenů. | |
| * Barevné zvýraznění podle vytížení: zelená pod 70 %, žlutá od 70 %, červená od 90 %. | |
| |
| ===== Zapojení do konfigurace ===== | ===== Zapojení do konfigurace ===== |
| |
| V ''~/.claude/settings.json'' mám status line zapojený takto: | V ''~/.claude/settings.json'' jsou zapojené dva skripty: |
| |
| <code json> | <code json> |
| "statusLine": { | "statusLine": { |
| "type": "command", | "type": "command", |
| "command": "bash /home/ubuntu/.claude/statusline-command.sh" | "command": "bash ~/.claude/statusline-command.sh" |
| | }, |
| | "subagentStatusLine": { |
| | "type": "command", |
| | "command": "bash ~/.claude/subagent-statusline.sh" |
| } | } |
| </code> | </code> |
| |
| Skript čte JSON ze standardního vstupu, parsuje ho přes ''jq'' a složí z něj jednu barevnou řádku. | Skripty čtou JSON ze standardního vstupu, parsují ho přes ''jq'' a vrací barevný výstup přes ANSI sekvence. |
| |
| ===== Jak vypadá výstup ===== | ===== Hlavní status line ===== |
| |
| Když Claude Code neposílá data o ''rate_limits'', výstup vypadá zhruba takto: | Hlavní ''statusLine'' je vázaná na top-level Claude Code session. Nepřepíná se podle aktivního subagenta a nedostává data o tom, který subagent zrovna běží. K tomu slouží samostatná ''subagentStatusLine''. |
| | |
| <code> | |
| ~/scratch │ Sonnet 4.6 | sid:<id> │ ctx:23% │ 92k/400k | |
| </code> | |
| |
| Když jsou limity v JSONu k dispozici, řádka se rozšíří o 5h a 7d přehled: | Výstup hlavní status line vypadá zhruba takto: |
| |
| <code> | <code> |
| ~/project-procesniaudit-website │ Sonnet 4.6 │ ctx:55% │ 5h:4% ↺ 00:50 │ 7d:45% ↺ Po 08:00 │ 220k/400k | ~/project │ Sonnet 4.6 | sid:<id> │ ctx:55% │ 5h:4% ↺ 00:50 │ 7d:45% ↺ Po 08:00 │ 220k/400k |
| </code> | </code> |
| |
| * token counter ''XYZk/TOTALk'' → červeně a tučně při překročení 200k, jinak dim | * token counter ''XYZk/TOTALk'' → červeně a tučně při překročení 200k, jinak dim |
| |
| ===== Celý skript ===== | ==== Skript ''statusline-command.sh'' ==== |
| |
| <code bash> | <code bash> |
| fi | fi |
| } | } |
| |
| |
| # ── 3) context used ──────────────────────────────────────────────────────────── | # ── 3) context used ──────────────────────────────────────────────────────────── |
| |
| printf "%b\n" "$out" | printf "%b\n" "$out" |
| | </code> |
| | |
| | ===== Subagent status line ===== |
| | |
| | ''subagentStatusLine'' dostává pole ''tasks'' a pro každý task musí vypsat jeden JSON řádek ve tvaru: |
| | |
| | <code json> |
| | {"id":"<task id>","content":"<řádek s ANSI>"} |
| | </code> |
| | |
| | V reálných datech se ukázalo, že task obsahuje zejména pole ''cwd'', ''description'', ''id'', ''label'', ''startTime'', ''status'', ''tokenCount'', ''tokenSamples'' a ''type''. Pole ''type'' je jen obecná hodnota typu ''local_agent'', nikoli název použitého subagenta. Samostatné pole pro model ani název subagenta v datech není. |
| | |
| | Proto se používá konvence v ''description'': |
| | |
| | <code> |
| | <subagent_type> · <model> · <short description> |
| | </code> |
| | |
| | Příklad: |
| | |
| | <code> |
| | elite-prompt-architect · haiku · rešerše pricing |
| | </code> |
| | |
| | Skript tento popisek rozparsuje a zobrazí ho jako: |
| | |
| | <code> |
| | elite-prompt-architect │ haiku │ rešerše pricing │ running │ tok:8.1k ▁▄█ 579/s │ 14s |
| | </code> |
| | |
| | ==== Pravidlo do globálního ''CLAUDE.md'' ==== |
| | |
| | Aby skript fungoval dobře i v dalších session, je potřeba dát do globálního ''CLAUDE.md'' pravidlo pro pojmenování dispatchů v Claude Code. Bez toho se sice subagent zobrazí, ale nebude zřejmé, který agent a model běží. |
| | |
| | <code markdown> |
| | ## `description` naming convention — Claude Code only |
| | |
| | In **Claude Code**, format every dispatch `description` as: |
| | |
| | ``` |
| | <subagent_type> · <model> · <short description> |
| | ``` |
| | |
| | Example: `elite-prompt-architect · haiku · rešerše pricing` |
| | |
| | Why this exact shape: the `description` is the **only** human-readable string that |
| | reaches the Claude Code subagent status-line panel — the task data exposes no |
| | `name` or `model` field, and `subagent_type` isn't surfaced there either. Encoding |
| | them into `description` is the only way the panel can show *which agent, on which |
| | model, doing what*. That visibility matters most exactly when several subagents run |
| | in parallel and you need to tell them apart at a glance. |
| | |
| | - Separator is a spaced middot ` · ` — the status line splits the label on it. |
| | - `<model>` = whatever you pass in the `model` param (`haiku`/`sonnet`/`opus`/`fable`); |
| | the status line renders it as a colored chip. |
| | - Keep `<short description>` genuinely short — the panel truncates to terminal width. |
| | |
| | This is purely a Claude Code convenience: it relies on the `subagentStatusLine` |
| | setting parsing the ` · ` format. **OpenCode** has no such status line, so use a |
| | plain, natural `description` there — do **not** force this format. |
| | </code> |
| | |
| | ==== Skript ''subagent-statusline.sh'' ==== |
| | |
| | <code bash> |
| | #!/usr/bin/env bash |
| | # Claude Code subagent status line |
| | # Renders one custom row per active subagent in the agent panel. |
| | # Row: <type> │ <status> │ tok:<n> <sparkline> <rate> │ <elapsed> │ [cwd] │ <label|desc> |
| | # Input (stdin): { cwd, columns, tasks:[ {id,name,type,status,description,label,startTime,tokenCount,tokenSamples,cwd}, ... ] } |
| | # Output (stdout): one JSON line per task -> {"id":"<id>","content":"<row w/ ANSI>"} |
| | |
| | input=$(cat) |
| | |
| | # ── ANSI ──────────────────────────────────────────────────────────────────────── |
| | RESET='\033[0m'; BOLD='\033[1m'; DIM='\033[2m' |
| | RED='\033[31m'; YELLOW='\033[33m'; GREEN='\033[32m'; CYAN='\033[36m'; MAGENTA='\033[35m'; WHITE='\033[37m'; BLUE='\033[34m' |
| | |
| | now=$(date +%s) |
| | main_cwd=$(echo "$input" | jq -r '.cwd // .workspace.current_dir // empty') |
| | columns=$(echo "$input" | jq -r '.columns // 0') |
| | US=$'\x1f' # interní oddělovač pro rozklad "agent · model · popis" |
| | |
| | status_color() { |
| | case "$1" in |
| | running|in_progress|active) echo "$CYAN" ;; |
| | completed|done|success) echo "$GREEN" ;; |
| | failed|error) echo "$RED" ;; |
| | pending|queued|waiting) echo "$YELLOW" ;; |
| | *) echo "$WHITE" ;; |
| | esac |
| | } |
| | |
| | # Rozpozná model v prefixu popisku (konvence "model · popis" / "model | popis"). |
| | # Vrací barvu, nebo prázdno když prefix není známý model. |
| | model_color() { |
| | case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in |
| | haiku*) echo "$GREEN" ;; |
| | sonnet*) echo "$CYAN" ;; |
| | opus*) echo "$MAGENTA" ;; |
| | fable*) echo "$BLUE" ;; |
| | *) echo "" ;; |
| | esac |
| | } |
| | |
| | # elapsed seconds from startTime (epoch-s, epoch-ms, or ISO-8601) -> "2m10s" |
| | elapsed_str() { |
| | local v="$1" start="" |
| | { [ -z "$v" ] || [ "$v" = "null" ]; } && return |
| | if [[ "$v" =~ ^[0-9]+$ ]]; then |
| | if [ "${#v}" -ge 13 ]; then start=$(( v / 1000 )); else start="$v"; fi |
| | else |
| | start=$(date -d "$v" +%s 2>/dev/null) || return |
| | fi |
| | local d=$(( now - start )); [ "$d" -lt 0 ] && d=0 |
| | if [ "$d" -ge 3600 ]; then printf "%dh%dm" $(( d/3600 )) $(( (d%3600)/60 )) |
| | elif [ "$d" -ge 60 ]; then printf "%dm%02ds" $(( d/60 )) $(( d%60 )) |
| | else printf "%ds" "$d"; fi |
| | return |
| | } |
| | elapsed_secs() { |
| | local v="$1" start="" |
| | { [ -z "$v" ] || [ "$v" = "null" ]; } && { echo 0; return; } |
| | if [[ "$v" =~ ^[0-9]+$ ]]; then |
| | if [ "${#v}" -ge 13 ]; then start=$(( v / 1000 )); else start="$v"; fi |
| | else |
| | start=$(date -d "$v" +%s 2>/dev/null) || { echo 0; return; } |
| | fi |
| | local d=$(( now - start )); [ "$d" -lt 0 ] && d=0; echo "$d" |
| | } |
| | |
| | num_fmt() { # 39731 -> 39.7k |
| | local t="$1" |
| | [ "$t" -ge 1000 ] 2>/dev/null && python3 -c "print(f'{$t/1000:.1f}k')" 2>/dev/null || echo "$t" |
| | } |
| | |
| | # sparkline + rate from tokenSamples (defensive: čísla i objekty) |
| | # stdout: "<sparkline>\t<rate_per_s or empty>" |
| | spark_and_rate() { |
| | python3 - "$1" 2>/dev/null <<'PY' |
| | import sys, json |
| | raw = sys.argv[1] if len(sys.argv) > 1 else "[]" |
| | try: |
| | d = json.loads(raw) |
| | except Exception: |
| | print("\t"); sys.exit(0) |
| | vals, times = [], [] |
| | def num(x): |
| | if isinstance(x,(int,float)): return float(x) |
| | if isinstance(x,dict): |
| | for k in ('tokens','tokenCount','count','value','v','y'): |
| | if isinstance(x.get(k),(int,float)): return float(x[k]) |
| | return None |
| | def tim(x): |
| | if isinstance(x,dict): |
| | for k in ('t','time','ts','timestamp','at'): |
| | if isinstance(x.get(k),(int,float)): return float(x[k]) |
| | return None |
| | if isinstance(d, list): |
| | for x in d: |
| | v = num(x) |
| | if v is not None: |
| | vals.append(v); times.append(tim(x)) |
| | spark = "" |
| | if len(vals) >= 2: |
| | blocks = "▁▂▃▄▅▆▇█" |
| | w = vals[-14:] |
| | lo, hi = min(w), max(w); rng = (hi - lo) or 1 |
| | spark = "".join(blocks[min(7, int(round((v-lo)/rng*7)))] for v in w) |
| | rate = "" |
| | if len(vals) >= 2 and all(t is not None for t in times) and (times[-1]-times[0]) > 0: |
| | dt = times[-1]-times[0] |
| | if dt > 10_000_000_000: dt = dt/1000.0 # ms guard |
| | r = (vals[-1]-vals[0])/dt |
| | if r > 0: |
| | rate = f"{r/1000:.1f}k/s" if r >= 1000 else f"{r:.0f}/s" |
| | print(f"{spark}\t{rate}") |
| | PY |
| | } |
| | |
| | short_dir() { |
| | local d="$1" |
| | { [ -z "$d" ] || [ "$d" = "null" ]; } && return |
| | if [ "$d" = "$HOME" ]; then echo "~"; |
| | elif [[ "$d" == "$HOME/"* ]]; then echo "~/${d#$HOME/}"; |
| | else echo "$d"; fi |
| | } |
| | |
| | # ── build one row per task ─────────────────────────────────────────────────────── |
| | echo "$input" | jq -c '.tasks[]?' | while read -r task; do |
| | id=$(echo "$task" | jq -r '.id // empty'); [ -z "$id" ] && continue |
| | # Primární štítek = label|description (subagent_type ve schématu NENÍ). |
| | primary=$(echo "$task" | jq -r '(.label // "") as $l | (.description // "") as $d | (.name // "") as $n |
| | | if ($l|length)>0 then $l elif ($d|length)>0 then $d elif ($n|length)>0 then $n else "agent" end') |
| | # `type` je hrubá kategorie ("local_agent"); ukázat jen když nese info. |
| | type=$(echo "$task" | jq -r '.type // ""') |
| | status=$(echo "$task" | jq -r '.status // ""') |
| | tokens=$(echo "$task" | jq -r '.tokenCount // ""') |
| | start=$(echo "$task" | jq -r '.startTime // ""') |
| | tcwd=$(echo "$task" | jq -r '.cwd // ""') |
| | samples=$(echo "$task" | jq -c '.tokenSamples // []') |
| | |
| | sc=$(status_color "$status") |
| | |
| | # Konvence "agent · model · popis" (oddělovač " · " nebo " | ") → 3 barevné části. |
| | # Zpětně kompatibilní: 2 díly "model · popis", 1 díl jen popis. |
| | agentseg=""; modelseg="" |
| | _tmp="${primary// · /$US}"; _tmp="${_tmp// | /$US}" |
| | IFS="$US" read -ra _segs <<< "$_tmp" |
| | _agent=""; _model=""; _popis="" |
| | _n=${#_segs[@]} |
| | if [ "$_n" -ge 3 ]; then |
| | _agent="${_segs[0]}"; _model="${_segs[1]}"; _popis="${_segs[2]}" |
| | for ((i=3; i<_n; i++)); do _popis="$_popis · ${_segs[$i]}"; done |
| | elif [ "$_n" -eq 2 ]; then |
| | if [ -n "$(model_color "${_segs[1]}")" ]; then _agent="${_segs[0]}"; _model="${_segs[1]}" |
| | elif [ -n "$(model_color "${_segs[0]}")" ]; then _model="${_segs[0]}"; _popis="${_segs[1]}" |
| | else _agent="${_segs[0]}"; _popis="${_segs[1]}"; fi |
| | else |
| | _popis="$primary" |
| | fi |
| | # model chip jen když je to známý model; jinak segment vrať do popisu |
| | if [ -n "$_model" ]; then |
| | _mcol=$(model_color "$_model") |
| | if [ -n "$_mcol" ]; then |
| | modelseg=$(printf "%b%b%s%b" "$_mcol" "$BOLD" "$_model" "$RESET") |
| | else |
| | _popis="${_model}${_popis:+ · $_popis}" |
| | fi |
| | fi |
| | [ -n "$_agent" ] && agentseg=$(printf "%b%s%b" "$BOLD" "$_agent" "$RESET") |
| | primary="$_popis" |
| | |
| | # tokens: count + sparkline + rate |
| | tokseg="" |
| | if [ -n "$tokens" ] && [ "$tokens" != "null" ]; then |
| | disp=$(num_fmt "$tokens") |
| | if [ "$tokens" -ge 100000 ] 2>/dev/null; then tcol="${RED}${BOLD}"; else tcol="$DIM"; fi |
| | IFS=$'\t' read -r spark rate < <(spark_and_rate "$samples") |
| | # fallback rate: průměr od startu, když samples nenesou čas |
| | if [ -z "$rate" ]; then |
| | es=$(elapsed_secs "$start") |
| | [ "$es" -gt 0 ] 2>/dev/null && rate=$(python3 -c "r=$tokens/$es; print(f'{r/1000:.1f}k/s' if r>=1000 else f'{r:.0f}/s')" 2>/dev/null) |
| | fi |
| | tokseg=$(printf "%btok:%s%b" "$tcol" "$disp" "$RESET") |
| | [ -n "$spark" ] && tokseg="$tokseg $(printf "%b%s%b" "$CYAN" "$spark" "$RESET")" |
| | [ -n "$rate" ] && tokseg="$tokseg $(printf "%b%s%b" "$DIM" "$rate" "$RESET")" |
| | fi |
| | |
| | el=$(elapsed_str "$start") |
| | |
| | # cwd jen když se liší od hlavní session |
| | dirseg="" |
| | if [ -n "$tcwd" ] && [ "$tcwd" != "null" ] && [ "$tcwd" != "$main_cwd" ]; then |
| | dirseg=$(printf "%b%s%b" "$YELLOW" "$(short_dir "$tcwd")" "$RESET") |
| | fi |
| | |
| | # ořez primárního štítku podle columns (heuristika: fixní část ~ 44 sloupců) |
| | if [ -n "$primary" ] && [ "$columns" -gt 0 ] 2>/dev/null; then |
| | budget=$(( columns - 44 )); [ "$budget" -lt 12 ] && budget=12 |
| | if [ "${#primary}" -gt "$budget" ]; then primary="${primary:0:$((budget-1))}…"; fi |
| | fi |
| | |
| | # type badge jen když není běžná lokální kategorie |
| | typeseg="" |
| | case "$type" in |
| | ""|local_agent|agent) : ;; |
| | *) typeseg=$(printf "%b%s%b" "$DIM" "$type" "$RESET") ;; |
| | esac |
| | |
| | parts=() |
| | if [ -n "$agentseg" ]; then |
| | parts+=("$agentseg") |
| | [ -n "$modelseg" ] && parts+=("$modelseg") |
| | [ -n "$primary" ] && parts+=("$(printf "%b%s%b" "$DIM" "$primary" "$RESET")") |
| | else |
| | [ -n "$modelseg" ] && parts+=("$modelseg") |
| | parts+=("$(printf "%b%b%s%b" "$MAGENTA" "$BOLD" "$primary" "$RESET")") |
| | fi |
| | [ -n "$status" ] && parts+=("$(printf "%b%s%b" "$sc" "$status" "$RESET")") |
| | [ -n "$tokseg" ] && parts+=("$tokseg") |
| | [ -n "$el" ] && parts+=("$(printf "%b%s%b" "$DIM" "$el" "$RESET")") |
| | [ -n "$dirseg" ] && parts+=("$dirseg") |
| | [ -n "$typeseg" ] && parts+=("$typeseg") |
| | |
| | sep="$(printf "%b │ %b" "$DIM" "$RESET")" |
| | content="" |
| | for p in "${parts[@]}"; do |
| | [ -z "$content" ] && content="$p" || content="${content}${sep}${p}" |
| | done |
| | |
| | content_bytes=$(printf "%b" "$content") |
| | jq -cn --arg id "$id" --arg content "$content_bytes" '{id:$id, content:$content}' |
| | done |
| </code> | </code> |
| |
| ===== Poznámky k chování ===== | ===== Poznámky k chování ===== |
| |
| * Pole ''5h'' a ''7d'' se zobrazí jen když Claude Code skutečně pošle ''rate_limits''. | * Hlavní ''statusLine'' a ''subagentStatusLine'' jsou oddělené. Hlavní řádek zůstává pro top-level session, subagenti se zobrazují v agent panelu. |
| * Funkce ''fmt_reset_short()'' a ''fmt_reset_long()'' zvládnou jak unix epoch, tak ISO-8601 string. | * ''subagentStatusLine'' nedostává samostatně model ani skutečný ''subagent_type''. Proto je nutná konvence v ''description''. |
| * Pro cwd se primárně čte ''.workspace.project_dir'' — pole, které Claude Code nastavuje na root projektu (kde byl spuštěn) a nemění se v průběhu session, i když shell přejde do podadresáře. Pole ''.workspace.current_dir'' a ''.cwd'' jsou jen fallback. | * ''tokenSamples'' se používá pro sparkline trendu tokenů. Když vzorky neobsahují čas, rychlost se dopočítá jako průměr od ''startTime''. |
| * Procento využití context window se čte přímo z ''context_window.used_percentage'' — předpočítané pole, které Claude Code posílá od verze 2.1.132. Pokrývá vstupní tokeny včetně cache: ''used_percentage'' = (''input_tokens'' + ''cache_creation_input_tokens'' + ''cache_read_input_tokens'') / ''context_window_size''. Výstupní tokeny do procenta nezahrnuje, ale ani do ''XYZk'' v token counteru (tam se čte ''total_input_tokens'', které je součtem stejných tří složek). | * ''columns'' se používá pro ořez popisku, aby se řádky v úzkém terminálu nelámaly. |
| * ''total_input_tokens'' zahrnuje všechny vstupní tokeny včetně cache a od v2.1.132 reprezentuje aktuální stav context window, nikoli kumulativní součet přes celou session. | * ''cwd'' se zobrazí jen tehdy, když se liší od hlavní session. |
| * Skript záměrně nepoužívá příznak ''exceeds_200k_tokens'' — ten vychází z tokenů aktuálního API volání (input + output + cache) a má pevnou hranici 200k bez ohledu na skutečnou velikost okna. Místo toho skript ukazuje aktuální stav v segmentu ''XYZk/TOTALk''. | * Skripty vyžadují ''jq'', ''python3'' a běžné unixové ''date''. |
| * Když některé pole v JSONu chybí, skript se nerozbije a prostě ho přeskočí. | |
| * Pořadí polí je pevné: ''cwd'' → ''model + sid'' → ''ctx'' → ''5h'' → ''7d'' → ''XYZk/TOTALk''. | |
| |
| ===== Zdroje ===== | ===== Zdroje ===== |
| |
| * [[https://code.claude.com/docs/en/statusline|Claude Code – Customize your status line (oficiální dokumentace)]] | * [[https://code.claude.com/docs/en/statusline|Claude Code – Customize your status line (oficiální dokumentace)]] |