====== Status line pro Claude Code ====== //Vytvořeno: **** | Aktualizováno: **~~LASTMOD~~**// [[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 ===== * Hlavní status line ukazuje pracovní adresář, model, session ID, využití context window, 5h a 7d limity a absolutní spotřebu tokenů. * 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ář. * 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. * Popisky subagentů jsou čitelné i při paralelním běhu více agentů. ===== Zapojení do konfigurace ===== V ''~/.claude/settings.json'' jsou zapojené dva skripty: "statusLine": { "type": "command", "command": "bash ~/.claude/statusline-command.sh" }, "subagentStatusLine": { "type": "command", "command": "bash ~/.claude/subagent-statusline.sh" } Skripty čtou JSON ze standardního vstupu, parsují ho přes ''jq'' a vrací barevný výstup přes ANSI sekvence. ===== Hlavní status line ===== 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''. Výstup hlavní status line vypadá zhruba takto: ~/project │ Sonnet 4.6 | sid: │ ctx:55% │ 5h:4% ↺ 00:50 │ 7d:45% ↺ Po 08:00 │ 220k/400k Barvy jsou rozdělené takto: * cwd → cyan + bold * model → magenta * sid → dim * ''ctx'', ''5h'' a ''7d'' → zelená pod 70 %, žlutá od 70 %, červená od 90 % * token counter ''XYZk/TOTALk'' → červeně a tučně při překročení 200k, jinak dim ==== Skript ''statusline-command.sh'' ==== #!/usr/bin/env bash # Claude Code status line # Order: cwd | model | context used | 5h limit | 7d limit input=$(cat) # ── helpers ──────────────────────────────────────────────────────────────────── 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' # ── 1) cwd ───────────────────────────────────────────────────────────────────── cwd=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""') # Show path relative to home dir home_dir="$HOME" if [[ "$cwd" == "$home_dir" ]]; then short_cwd="~" elif [[ "$cwd" == "$home_dir/"* ]]; then short_cwd="~/${cwd#$home_dir/}" else short_cwd="$cwd" fi [ "$short_cwd" = "/" ] && short_cwd="/" # ── 2) model ─────────────────────────────────────────────────────────────────── model_name=$(echo "$input" | jq -r '.model.display_name // .model.id // ""') session_id=$(echo "$input" | jq -r '.session_id // .conversation_id // empty') # ── rate limit helpers ───────────────────────────────────────────────────────── five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') five_rst=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty') week_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') week_rst=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty') fmt_reset_short() { local v="$1" [ -z "$v" ] && return # Try as epoch first, then as ISO-8601 string if [[ "$v" =~ ^[0-9]+$ ]]; then date -d "@$v" +%H:%M 2>/dev/null || date -r "$v" +%H:%M 2>/dev/null || echo "?" else date -d "$v" +%H:%M 2>/dev/null || echo "?" fi } fmt_reset_long() { local v="$1" [ -z "$v" ] && return if [[ "$v" =~ ^[0-9]+$ ]]; then date -d "@$v" +"%a %H:%M" 2>/dev/null || date -r "$v" +"%a %H:%M" 2>/dev/null || echo "?" else date -d "$v" +"%a %H:%M" 2>/dev/null || echo "?" fi } # Choose colour based on used % pct_color_used() { local used="$1" [ -z "$used" ] && echo "$WHITE" && return local u u=$(printf "%.0f" "$used") if [ "$u" -ge 90 ]; then echo "$RED" elif [ "$u" -ge 70 ]; then echo "$YELLOW" else echo "$GREEN" fi } # ── 3) context used ──────────────────────────────────────────────────────────── ctx_used_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // empty') ctx_window_size=$(echo "$input" | jq -r '.context_window.context_window_size // empty') ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') ctx_str="" if [ -n "$ctx_pct" ]; then ctx_int=$(printf "%.0f" "$ctx_pct") if [ "$ctx_int" -ge 90 ]; then ctx_str="$(printf "${RED}${BOLD}ctx:%s%%!${RESET}" "$ctx_int")" elif [ "$ctx_int" -ge 70 ]; then ctx_str="$(printf "${YELLOW}ctx:%s%%${RESET}" "$ctx_int")" else ctx_str="$(printf "${GREEN}ctx:%s%%${RESET}" "$ctx_int")" fi fi # ── assemble line ────────────────────────────────────────────────────────────── parts=() # 1) cwd parts+=("$(printf "${CYAN}${BOLD}%s${RESET}" "$short_cwd")") # 2) model + session id if [ -n "$model_name" ] || [ -n "$session_id" ]; then parts+=("$(printf "${MAGENTA}%s${RESET}" "$model_name")${session_id:+ $(printf "${DIM}| sid:%s${RESET}" "$session_id")}") fi # 3) context used (always shown when data available) [ -n "$ctx_str" ] && parts+=("$ctx_str") # 4) 5h rate limit (only when data is available) if [ -n "$five_pct" ]; then five_int=$(printf "%.0f" "$five_pct") col=$(pct_color_used "$five_pct") rst=$(fmt_reset_short "$five_rst") parts+=("$(printf "${col}5h:%s%%%s${RESET}" "$five_int" "${rst:+ ↺ $rst}")") fi # 5) 7d rate limit (only when data is available) if [ -n "$week_pct" ]; then week_int=$(printf "%.0f" "$week_pct") col=$(pct_color_used "$week_pct") rst=$(fmt_reset_long "$week_rst") parts+=("$(printf "${col}7d:%s%%%s${RESET}" "$week_int" "${rst:+ ↺ $rst}")") fi # 6) token counter — červeně od 200k if [ -n "$ctx_used_tokens" ] && [ -n "$ctx_window_size" ] && [ "$ctx_window_size" -gt 0 ]; then ctx_used_k=$(python3 -c "print(round($ctx_used_tokens / 1000))") ctx_total_k=$(python3 -c "print(round($ctx_window_size / 1000))") if [ "$ctx_used_tokens" -gt 200000 ]; then parts+=("$(printf "${RED}${BOLD}%sk/%sk${RESET}" "$ctx_used_k" "$ctx_total_k")") else parts+=("$(printf "${DIM}%sk/%sk${RESET}" "$ctx_used_k" "$ctx_total_k")") fi fi # ── print ────────────────────────────────────────────────────────────────────── # Join with separator " │ " sep="$(printf "${DIM} │ ${RESET}")" out="" for p in "${parts[@]}"; do [ -z "$out" ] && out="$p" || out="${out}${sep}${p}" done printf "%b\n" "$out" ===== Subagent status line ===== ''subagentStatusLine'' dostává pole ''tasks'' a pro každý task musí vypsat jeden JSON řádek ve tvaru: {"id":"","content":"<řádek s ANSI>"} 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'': · · Příklad: elite-prompt-architect · haiku · rešerše pricing Skript tento popisek rozparsuje a zobrazí ho jako: elite-prompt-architect │ haiku │ rešerše pricing │ running │ tok:8.1k ▁▄█ 579/s │ 14s ==== 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ěží. ## `description` naming convention — Claude Code only In **Claude Code**, format every dispatch `description` as: ``` · · ``` 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. - `` = whatever you pass in the `model` param (`haiku`/`sonnet`/`opus`/`fable`); the status line renders it as a colored chip. - Keep `` 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. ==== Skript ''subagent-statusline.sh'' ==== #!/usr/bin/env bash # Claude Code subagent status line # Renders one custom row per active subagent in the agent panel. # Row: │ tok: │ [cwd] │ # Input (stdin): { cwd, columns, tasks:[ {id,name,type,status,description,label,startTime,tokenCount,tokenSamples,cwd}, ... ] } # Output (stdout): one JSON line per task -> {"id":"","content":""} 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: "\t" 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 ===== Poznámky k chování ===== * Hlavní ''statusLine'' a ''subagentStatusLine'' jsou oddělené. Hlavní řádek zůstává pro top-level session, subagenti se zobrazují v agent panelu. * ''subagentStatusLine'' nedostává samostatně model ani skutečný ''subagent_type''. Proto je nutná konvence v ''description''. * ''tokenSamples'' se používá pro sparkline trendu tokenů. Když vzorky neobsahují čas, rychlost se dopočítá jako průměr od ''startTime''. * ''columns'' se používá pro ořez popisku, aby se řádky v úzkém terminálu nelámaly. * ''cwd'' se zobrazí jen tehdy, když se liší od hlavní session. * Skripty vyžadují ''jq'', ''python3'' a běžné unixové ''date''. ===== Zdroje ===== * [[https://code.claude.com/docs/en/statusline|Claude Code – Customize your status line (oficiální dokumentace)]]