Last active
February 11, 2026 01:08
-
-
Save paulrobello/6777e2dae8945900328e18005f6032c5 to your computer and use it in GitHub Desktop.
Claude Code Status line uber script - customizable statusline with toggles, colors, git info, AI summary, and more
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Statusline Script for Claude Code | |
| # https://gist.github.com/paulrobello/6777e2dae8945900328e18005f6032c5 | |
| # | |
| # === INSTALLATION === | |
| # 1. Copy this script to ~/.claude/statusline.sh: | |
| # cp statusline.sh ~/.claude/statusline.sh | |
| # | |
| # 2. Make it executable: | |
| # chmod +x ~/.claude/statusline.sh | |
| # | |
| # 3. Ensure jq is installed: | |
| # # macOS | |
| # brew install jq | |
| # # Ubuntu/Debian | |
| # sudo apt install jq | |
| # # Fedora/RHEL | |
| # sudo dnf install jq | |
| # # Windows (via scoop) | |
| # scoop install jq | |
| # # Windows (manual) | |
| # Download https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe | |
| # Rename to jq.exe and place in C:\Program Files\Git\usr\bin | |
| # | |
| # 4. Add to ~/.claude/settings.json: | |
| # { | |
| # "statusLine": { | |
| # "type": "command", | |
| # "command": "~/.claude/statusline.sh" | |
| # } | |
| # } | |
| # | |
| # Windows users use this command format instead: | |
| # { | |
| # "statusLine": { | |
| # "type": "command", | |
| # "command": "bash ~/.claude/statusline.sh" | |
| # } | |
| # } | |
| # | |
| # Or merge with existing settings.json using jq: | |
| # jq '. + {"statusLine": {"type": "command", "command": "~/.claude/statusline.sh"}}' \ | |
| # ~/.claude/settings.json > /tmp/settings.json && mv /tmp/settings.json ~/.claude/settings.json | |
| # | |
| # === END INSTALLATION === | |
| # | |
| # === WHAT'S NEW === | |
| # - Fixed self-update shebang validation to accept #!/usr/bin/env bash | |
| # - Added macOS-compatible timeout fallback (no longer requires GNU coreutils) | |
| # - Consolidated JSON parsing into fewer jq calls for better performance | |
| # - Fixed character vs byte truncation for multi-byte text (emoji, CJK) | |
| # - Added configurable AI summary model (AI_SUMMARY_MODEL) and git options (GIT_SHOW_UNTRACKED) | |
| # - Added --help and --version flags | |
| # - Improved arithmetic safety for git ahead/behind counters | |
| # - Safer todo file discovery using glob with stat instead of ls parsing | |
| # | |
| # === CLI OPTIONS === | |
| # --help Show usage information | |
| # --version Show script version | |
| # --update Download and install the latest version from gist | |
| # --debug Enable debug logging to ~/.claude/statusline.log | |
| # | |
| # === USER CONFIGURATION === | |
| # Create ~/.claude/status_line.env to override default settings. | |
| # This file is sourced after defaults, so your preferences persist across updates. | |
| # | |
| # Example ~/.claude/status_line.env: | |
| # # Display toggles | |
| # SHOW_CURRENT_TIME="true" | |
| # SHOW_SESSION_COST="true" | |
| # SHOW_AI_SUMMARY="true" | |
| # | |
| # # Custom colors (format: \033[<style>;<fg>;<bg>m) | |
| # COLOR_PATH="\033[01;92;49m" # bright green path | |
| # | |
| # # Context calculation | |
| # USE_AUTO_COMPACT="true" | |
| # | |
| # # AI summary model (default: haiku) | |
| # AI_SUMMARY_MODEL="haiku" | |
| # | |
| # # Git status (set to false to skip untracked file scanning for faster rendering) | |
| # GIT_SHOW_UNTRACKED="true" | |
| # | |
| # Features: | |
| # - Customizable display components with toggles | |
| # - Debug logging with --debug flag | |
| # - Full color customization support | |
| # - Cross-platform compatible (Linux, macOS, Windows via Git Bash/MSYS/Cygwin) | |
| # - Context window tracking with optional auto-compact buffer adjustment | |
| # - Supports pre-calculated percentages (v2.1.6+: used_percentage, remaining_percentage) | |
| # - Falls back to manual calculation from current_usage or legacy transcript | |
| # | |
| # Display order: | |
| # Line 1: π time | π€ user@host | π path | πΏ git branch (β/β status, ββ ahead/behind) | |
| # π version | π€ model | π¨ style | π§ context % | π todos | π session_id | |
| # βοΈ messages | β±οΈ duration | π° cost | π₯ burn rate | session name | |
| # Line 2: β Last user prompt | |
| # Line 3: π AI-powered conversation summary (via Claude Haiku) | |
| # Line 4: +lines/-lines (GitHub-style indicators) | |
| SCRIPT_VERSION="2.2.0" | |
| # Force color output even when not in a TTY | |
| export TERM=xterm-256color | |
| # === PLATFORM DETECTION === | |
| # Detect if running on Windows (Git Bash, MSYS, Cygwin, WSL) | |
| IS_WINDOWS=false | |
| if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then | |
| IS_WINDOWS=true | |
| elif [[ -n "$WINDIR" ]] || [[ -n "$windir" ]]; then | |
| IS_WINDOWS=true | |
| fi | |
| # Cross-platform hostname function (Windows doesn't support -s flag) | |
| get_hostname() { | |
| if [ "$IS_WINDOWS" = true ]; then | |
| hostname | cut -d'.' -f1 | |
| else | |
| hostname -s 2>/dev/null || hostname | cut -d'.' -f1 | |
| fi | |
| } | |
| # Cross-platform timeout wrapper (macOS lacks GNU timeout) | |
| run_with_timeout() { | |
| local secs=$1; shift | |
| if command -v timeout &>/dev/null; then | |
| timeout "${secs}s" "$@" | |
| elif command -v perl &>/dev/null; then | |
| perl -e 'alarm shift; exec @ARGV' "$secs" "$@" | |
| else | |
| # No timeout mechanism available; run without timeout | |
| "$@" | |
| fi | |
| } | |
| # Get file modification time as epoch seconds (cross-platform) | |
| file_mtime() { | |
| stat -f %m "$1" 2>/dev/null || stat -c %Y "$1" 2>/dev/null || echo 0 | |
| } | |
| # === COLOR CONFIGURATION === | |
| # Customize these variables to change the statusline appearance | |
| # Format: \033[<style>;<fg>;<bg>m where style=01 is bold, fg=3X, bg=4X | |
| # Colors: 0=black, 1=red, 2=green, 3=yellow, 4=blue, 5=magenta, 6=cyan, 7=white | |
| # Background (40=black, 41=red, 42=green, 43=yellow, 44=blue, 45=magenta, 46=cyan, 47=white, 49=default/none) | |
| BG="49" | |
| # Reset code | |
| RESET="\033[00m" | |
| # Path color (light blue/cyan on black) | |
| COLOR_PATH="\033[01;94;${BG}m" | |
| # Git branch color (bold cyan on black) | |
| COLOR_BRANCH="\033[01;36;${BG}m" | |
| # Model badge color (bold white on black) | |
| COLOR_MODEL="\033[01;37;${BG}m" | |
| # Version color (bold magenta on black) | |
| COLOR_VERSION="\033[01;35;${BG}m" | |
| # Output style color (bold yellow on black) | |
| COLOR_STYLE="\033[01;33;${BG}m" | |
| # Prompt arrow color (bold yellow on black) | |
| COLOR_ARROW="\033[01;33;${BG}m" | |
| # Summary icon color (bold cyan on black) | |
| COLOR_SUMMARY="\033[01;36;${BG}m" | |
| # Lines added color (bold green on black) | |
| COLOR_ADDED="\033[01;32;${BG}m" | |
| # Lines removed color (bold red on black) | |
| COLOR_REMOVED="\033[01;31;${BG}m" | |
| # Username color (bold red on black) | |
| COLOR_USER="\033[01;31;${BG}m" | |
| # At sign color (bold yellow on black) | |
| COLOR_AT="\033[01;33;${BG}m" | |
| # Hostname color (bold green on black) | |
| COLOR_HOST="\033[01;32;${BG}m" | |
| # Context usage color (bold white on black) | |
| COLOR_CONTEXT="\033[01;37;${BG}m" | |
| # Session info color (bold cyan on black) | |
| COLOR_SESSION="\033[01;36;${BG}m" | |
| # === DISPLAY TOGGLES === | |
| # Set to "true" to show, "false" to hide | |
| # Ordered by display position in statusline | |
| # Line 1: Main status bar | |
| SHOW_CURRENT_TIME="false" # π Current time (HH:MM) | |
| SHOW_USERNAME="true" # π€ Username display | |
| SHOW_HOSTNAME="true" # π€ Hostname display (combined with username as user@host) | |
| SHOW_PATH="true" # π Current working directory path | |
| SHOW_GIT_BRANCH="true" # πΏ Git branch name with status indicator (β/β) | |
| SHOW_GIT_AHEAD_BEHIND="true" # ββ Git ahead/behind commit counts | |
| SHOW_VERSION="false" # π Claude Code version | |
| SHOW_MODEL="true" # π€ Claude model name | |
| SHOW_OUTPUT_STYLE="false" # π¨ Output style name | |
| SHOW_CONTEXT_REMAINING="true" # π§ Context remaining percentage (CR: X%) | |
| SHOW_TODO_COUNT="true" # π Pending/in-progress todo count | |
| SHOW_SESSION_ID="false" # π Session ID (first 8 chars) | |
| SHOW_MESSAGE_COUNT="false" # βοΈ Number of user messages | |
| SHOW_SESSION_DURATION="false" # β±οΈ Session duration (min:sec) | |
| SHOW_SESSION_COST="false" # π° Session cost in USD | |
| SHOW_BURN_RATE="false" # π₯ Burn rate in $/hr | |
| SHOW_SESSION_NAME="false" # Session slug/name from transcript | |
| # Line 2: Last user prompt | |
| SHOW_LAST_PROMPT="true" # β Last user prompt text | |
| # Line 3: AI summary | |
| SHOW_AI_SUMMARY="false" # π AI-generated conversation summary | |
| # Line 4: Line changes (optional) | |
| SHOW_LINE_CHANGES="false" # +/- GitHub-style lines added/removed | |
| # === ADDITIONAL SETTINGS === | |
| AI_SUMMARY_MODEL="haiku" # Model for AI summary (e.g., haiku, sonnet) | |
| GIT_SHOW_UNTRACKED="true" # Include untracked files in git dirty check (false = faster on large repos) | |
| # === CONTEXT CALCULATION SETTINGS === | |
| # Auto-compact buffer: tokens reserved for auto-compaction | |
| # Computed as CLAUDE_CODE_MAX_OUTPUT_TOKENS + 13000 (default 32000 + 13000 = 45000) | |
| # When USE_AUTO_COMPACT is true, this buffer is added to consumed tokens | |
| AUTO_COMPACT_BUFFER=$(( ${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-32000} + 13000 )) | |
| USE_AUTO_COMPACT="false" # Account for auto-compact reserved tokens in context calculation | |
| # === GIST URL FOR UPDATES === | |
| GIST_RAW_URL="https://gist.githubusercontent.com/paulrobello/6777e2dae8945900328e18005f6032c5/raw/statusline.sh" | |
| # === USER CONFIGURATION OVERRIDE === | |
| # Source user's config file to override defaults (preserves settings across updates) | |
| # Create ~/.claude/status_line.env with any variables you want to customize | |
| ENV_FILE="$HOME/.claude/status_line.env" | |
| if [ -f "$ENV_FILE" ]; then | |
| # shellcheck source=/dev/null | |
| source "$ENV_FILE" | |
| fi | |
| # === CLI ARGUMENT HANDLING === | |
| # --help | |
| if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then | |
| cat <<'HELP' | |
| Usage: statusline.sh [OPTIONS] | |
| Claude Code statusline script. Reads JSON from stdin and outputs | |
| a formatted status bar. | |
| Options: | |
| --help, -h Show this help message | |
| --version Show script version | |
| --update Download and install the latest version from gist | |
| --debug Enable debug logging to ~/.claude/statusline.log | |
| Configuration: | |
| Create ~/.claude/status_line.env to override default settings. | |
| See script header comments for all available options. | |
| HELP | |
| exit 0 | |
| fi | |
| # --version | |
| if [ "$1" = "--version" ]; then | |
| echo "statusline.sh v${SCRIPT_VERSION}" | |
| exit 0 | |
| fi | |
| # Parse command line arguments | |
| DEBUG_LOG=false | |
| # Self-update from gist | |
| if [ "$1" = "--update" ]; then | |
| SCRIPT_PATH="${BASH_SOURCE[0]}" | |
| # Resolve symlinks to get actual script location | |
| while [ -L "$SCRIPT_PATH" ]; do | |
| SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" | |
| SCRIPT_PATH="$(readlink "$SCRIPT_PATH")" | |
| [[ $SCRIPT_PATH != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH" | |
| done | |
| SCRIPT_PATH="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)/$(basename "$SCRIPT_PATH")" | |
| echo "Downloading latest version from gist..." | |
| TMP_FILE=$(mktemp) | |
| if curl -fsSL --connect-timeout 10 "$GIST_RAW_URL" -o "$TMP_FILE" 2>/dev/null; then | |
| # Accept both #!/bin/bash and #!/usr/bin/env bash | |
| if head -1 "$TMP_FILE" | grep -q '^#!/'; then | |
| cp "$TMP_FILE" "$SCRIPT_PATH" | |
| chmod +x "$SCRIPT_PATH" | |
| rm -f "$TMP_FILE" | |
| echo "Updated successfully: $SCRIPT_PATH" | |
| exit 0 | |
| else | |
| rm -f "$TMP_FILE" | |
| echo "Error: Downloaded file is not a valid bash script" >&2 | |
| exit 1 | |
| fi | |
| else | |
| rm -f "$TMP_FILE" | |
| echo "Error: Failed to download from gist" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ "$1" = "--debug" ]; then | |
| DEBUG_LOG=true | |
| shift | |
| fi | |
| input=$(cat) | |
| # === DEBUG LOGGING === | |
| # Log input for debugging if flag is set | |
| if [ "$DEBUG_LOG" = true ]; then | |
| LOG_FILE="$HOME/.claude/statusline.log" | |
| echo "=== $(date '+%Y-%m-%d %H:%M:%S') ===" >> "$LOG_FILE" | |
| echo "INPUT JSON: $input" >> "$LOG_FILE" | |
| fi | |
| # Exit early if no input | |
| if [ -z "$input" ]; then | |
| printf "No input received" | |
| exit 0 | |
| fi | |
| # === BULK JSON EXTRACTION === | |
| # Extract all needed values in a single jq call for performance (~18 values in 1 process | |
| # instead of ~18 separate jq invocations). Uses jq's @sh filter for safe shell escaping. | |
| jq_bulk=$(echo "$input" | jq -r ' | |
| "session_id=" + (.session_id // "" | @sh), | |
| "project_dir=" + (.workspace.project_dir // .workspace.current_dir // .cwd // "" | @sh), | |
| "transcript_path_input=" + (.transcript_path // "" | @sh), | |
| "version=" + (.version // "" | @sh), | |
| "output_style=" + (.output_style.name // "" | @sh), | |
| "current_dir=" + (.workspace.current_dir // .cwd // "" | @sh), | |
| "model_name=" + (.model.display_name // "Claude" | @sh), | |
| "display_path=" + (.workspace.current_dir // .cwd // "unknown" | @sh), | |
| "used_pct=" + (.context_window.used_percentage // "" | tostring | @sh), | |
| "remaining_pct=" + (.context_window.remaining_percentage // "" | tostring | @sh), | |
| "ctx_current_input=" + (.context_window.current_usage.input_tokens // 0 | tostring | @sh), | |
| "ctx_cache_creation=" + (.context_window.current_usage.cache_creation_input_tokens // 0 | tostring | @sh), | |
| "ctx_cache_read=" + (.context_window.current_usage.cache_read_input_tokens // 0 | tostring | @sh), | |
| "ctx_limit=" + (.context_window.context_window_size // 0 | tostring | @sh), | |
| "duration_ms=" + (.cost.total_duration_ms // 0 | tostring | @sh), | |
| "session_cost=" + (.cost.total_cost_usd // 0 | tostring | @sh), | |
| "lines_added=" + (.cost.total_lines_added // 0 | tostring | @sh), | |
| "lines_removed=" + (.cost.total_lines_removed // 0 | tostring | @sh) | |
| ' 2>/dev/null) || { | |
| printf "Error parsing input JSON" | |
| exit 0 | |
| } | |
| eval "$jq_bulk" | |
| # Log extracted variables if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "SESSION_ID: $session_id" >> "$LOG_FILE" | |
| echo "PROJECT_DIR: $project_dir" >> "$LOG_FILE" | |
| fi | |
| # === GET TRANSCRIPT FILE PATH === | |
| # Use transcript_path from input if available, otherwise compute it | |
| transcript_file="$transcript_path_input" | |
| if [ -z "$transcript_file" ] && [ -n "$session_id" ] && [ -n "$project_dir" ]; then | |
| # Fallback: Convert project path to encoded format used by Claude (/ becomes -) | |
| encoded_project=$(echo "$project_dir" | sed 's|/|-|g') | |
| transcript_file="$HOME/.claude/projects/$encoded_project/$session_id.jsonl" | |
| fi | |
| # Log transcript file path if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "TRANSCRIPT_FILE: $transcript_file" >> "$LOG_FILE" | |
| echo "FILE_EXISTS: $([ -f "$transcript_file" ] && echo 'yes' || echo 'no')" >> "$LOG_FILE" | |
| fi | |
| # === CALCULATE TERMINAL WIDTH === | |
| # Try tput first, fall back to COLUMNS env var, then default to 80 | |
| term_width=$(tput cols 2>/dev/null || echo "${COLUMNS:-80}") | |
| [[ ! "$term_width" =~ ^[0-9]+$ ]] && term_width=80 | |
| max_prompt_len=$((term_width - 10)) | |
| # === EXTRACT LAST USER PROMPT === | |
| last_prompt="" | |
| if [ -n "$transcript_file" ] && [ -f "$transcript_file" ]; then | |
| # Find the last user message from the JSONL file | |
| # Handle both string and array content formats, only show first line | |
| # Use bash substring expansion for character-safe truncation (not head -c which counts bytes) | |
| last_prompt=$(grep '"type":"user"' "$transcript_file" | grep -v 'tool_result' | tail -n 1 | jq -r 'if (.message.content | type) == "string" then .message.content else (.message.content // "") | tostring end' 2>/dev/null | head -n 1) | |
| last_prompt="${last_prompt:0:$max_prompt_len}" | |
| # Log extracted prompt if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "LAST_PROMPT: $last_prompt" >> "$LOG_FILE" | |
| fi | |
| fi | |
| # === DISPLAY MAIN STATUSLINE === | |
| # Format: π time | π€ user@host | π path | πΏ branch | π version | π€ model | π¨ style | π§ context | π todos | βοΈ messages | β±οΈ duration | π° cost | π₯ burn rate | session name | |
| git_branch="" | |
| # Get git branch if available | |
| if [ -n "$current_dir" ] && [ -d "$current_dir" ]; then | |
| git_branch=$(cd "$current_dir" && git rev-parse --git-dir >/dev/null 2>&1 && git branch --show-current 2>/dev/null) | |
| if [ -n "$git_branch" ]; then | |
| # Get git status for branch indicator | |
| if [ "$GIT_SHOW_UNTRACKED" = "true" ]; then | |
| git_status=$(cd "$current_dir" && git status --porcelain 2>/dev/null) | |
| else | |
| # Skip untracked files for faster rendering on large repos | |
| git_status=$(cd "$current_dir" && git status --porcelain --untracked-files=no 2>/dev/null) | |
| fi | |
| if [ -z "$git_status" ]; then | |
| git_status_icon="β" # clean | |
| else | |
| git_status_icon="β" # dirty/uncommitted changes | |
| fi | |
| # Get ahead/behind counts (initialize to 0 for arithmetic safety) | |
| git_ahead=0 | |
| git_behind=0 | |
| git_ahead_behind=$(cd "$current_dir" && git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null) | |
| if [ -n "$git_ahead_behind" ]; then | |
| git_behind=$(echo "$git_ahead_behind" | cut -f1) | |
| git_ahead=$(echo "$git_ahead_behind" | cut -f2) | |
| # Ensure numeric values after cut | |
| [[ ! "$git_behind" =~ ^[0-9]+$ ]] && git_behind=0 | |
| [[ ! "$git_ahead" =~ ^[0-9]+$ ]] && git_ahead=0 | |
| fi | |
| fi | |
| fi | |
| # Track if we need separator | |
| need_sep="" | |
| # Current time | |
| if [ "$SHOW_CURRENT_TIME" = "true" ]; then | |
| printf "${RESET}π ${COLOR_CONTEXT}%s${RESET}" "$(date '+%H:%M')" | |
| need_sep="true" | |
| fi | |
| # Username and/or Hostname | |
| if [ "$SHOW_USERNAME" = "true" ] && [ "$SHOW_HOSTNAME" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π€ ${COLOR_USER}%s${COLOR_AT}@${COLOR_HOST}%s${RESET}" "$(whoami)" "$(get_hostname)" | |
| need_sep="true" | |
| elif [ "$SHOW_USERNAME" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π€ ${COLOR_USER}%s${RESET}" "$(whoami)" | |
| need_sep="true" | |
| elif [ "$SHOW_HOSTNAME" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${COLOR_AT}@${COLOR_HOST}%s${RESET}" "$(get_hostname)" | |
| need_sep="true" | |
| fi | |
| # Path (use pre-extracted display_path with ~ substitution) | |
| if [ "$SHOW_PATH" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| path_short="$display_path" | |
| if [ -n "$HOME" ] && [[ "$path_short" == "$HOME"* ]]; then | |
| path_short="~${path_short:${#HOME}}" | |
| fi | |
| printf "${RESET}π ${COLOR_PATH}%s${RESET}" "$path_short" | |
| need_sep="true" | |
| fi | |
| # Git branch | |
| if [ "$SHOW_GIT_BRANCH" = "true" ] && [ -n "$git_branch" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${COLOR_BRANCH}πΏ %s %s${RESET}" "$git_branch" "$git_status_icon" | |
| # Ahead/behind indicators | |
| if [ "$SHOW_GIT_AHEAD_BEHIND" = "true" ]; then | |
| if [ "$git_ahead" -gt 0 ] 2>/dev/null; then | |
| printf " ${COLOR_ADDED}β%s${RESET}" "$git_ahead" | |
| fi | |
| if [ "$git_behind" -gt 0 ] 2>/dev/null; then | |
| printf " ${COLOR_REMOVED}β%s${RESET}" "$git_behind" | |
| fi | |
| fi | |
| need_sep="true" | |
| fi | |
| # Claude Code version | |
| if [ "$SHOW_VERSION" = "true" ] && [ -n "$version" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${COLOR_VERSION}π %s${RESET}" "$version" | |
| need_sep="true" | |
| fi | |
| # Model name (use pre-extracted model_name) | |
| if [ "$SHOW_MODEL" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π€ ${COLOR_MODEL}%s${RESET}" "$model_name" | |
| need_sep="true" | |
| fi | |
| # Output style | |
| if [ "$SHOW_OUTPUT_STYLE" = "true" ] && [ -n "$output_style" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${COLOR_STYLE}π¨ %s${RESET}" "$output_style" | |
| need_sep="true" | |
| fi | |
| # Context remaining (percentage based on context_window data from input) | |
| if [ "$SHOW_CONTEXT_REMAINING" = "true" ]; then | |
| # Log pre-calculated percentages if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "CONTEXT_WINDOW_RAW: $(echo "$input" | jq -c '.context_window // "null"')" >> "$LOG_FILE" | |
| echo "USED_PERCENTAGE: $used_pct" >> "$LOG_FILE" | |
| echo "REMAINING_PERCENTAGE: $remaining_pct" >> "$LOG_FILE" | |
| fi | |
| # Use pre-calculated remaining_percentage if available and auto-compact not enabled | |
| # (auto-compact requires manual calculation to add the buffer) | |
| if [[ "$remaining_pct" =~ ^[0-9]+\.?[0-9]*$ ]] && [ "$USE_AUTO_COMPACT" != "true" ]; then | |
| # Round to integer for display | |
| context_remaining_pct=$(printf "%.0f" "$remaining_pct") | |
| # Log if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "USING_PRE_CALCULATED: true" >> "$LOG_FILE" | |
| echo "CONTEXT_REMAINING_PCT: $context_remaining_pct" >> "$LOG_FILE" | |
| fi | |
| else | |
| # Fallback to manual calculation for older versions or when auto-compact is enabled | |
| # Use pre-extracted context values | |
| current_input="$ctx_current_input" | |
| cache_creation="$ctx_cache_creation" | |
| cache_read="$ctx_cache_read" | |
| context_limit="$ctx_limit" | |
| # Log manual extraction if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "USING_PRE_CALCULATED: false" >> "$LOG_FILE" | |
| echo "CURRENT_INPUT: $current_input" >> "$LOG_FILE" | |
| echo "CACHE_CREATION: $cache_creation" >> "$LOG_FILE" | |
| echo "CACHE_READ: $cache_read" >> "$LOG_FILE" | |
| echo "CONTEXT_LIMIT: $context_limit" >> "$LOG_FILE" | |
| fi | |
| # Ensure numeric values (strip any whitespace and validate) | |
| current_input=$(echo "$current_input" | tr -d '[:space:]') | |
| cache_creation=$(echo "$cache_creation" | tr -d '[:space:]') | |
| cache_read=$(echo "$cache_read" | tr -d '[:space:]') | |
| context_limit=$(echo "$context_limit" | tr -d '[:space:]') | |
| [[ ! "$current_input" =~ ^[0-9]+$ ]] && current_input=0 | |
| [[ ! "$cache_creation" =~ ^[0-9]+$ ]] && cache_creation=0 | |
| [[ ! "$cache_read" =~ ^[0-9]+$ ]] && cache_read=0 | |
| [[ ! "$context_limit" =~ ^[0-9]+$ ]] && context_limit=0 | |
| # Context usage = current input + cache tokens (not output tokens) | |
| total_context=$((current_input + cache_creation + cache_read)) | |
| # Fallback to transcript file if context_window data not available (older Claude Code versions) | |
| if [ "$total_context" -eq 0 ] && [ -n "$transcript_file" ] && [ -f "$transcript_file" ]; then | |
| last_usage=$(grep '"type":"assistant"' "$transcript_file" | tail -1 | jq -r '.message.usage // empty') | |
| if [ -n "$last_usage" ]; then | |
| input_tokens=$(echo "$last_usage" | jq -r '.input_tokens // 0') | |
| cache_creation=$(echo "$last_usage" | jq -r '.cache_creation_input_tokens // 0') | |
| cache_read=$(echo "$last_usage" | jq -r '.cache_read_input_tokens // 0') | |
| # Ensure numeric values | |
| input_tokens=$(echo "$input_tokens" | tr -d '[:space:]') | |
| cache_creation=$(echo "$cache_creation" | tr -d '[:space:]') | |
| cache_read=$(echo "$cache_read" | tr -d '[:space:]') | |
| [[ ! "$input_tokens" =~ ^[0-9]+$ ]] && input_tokens=0 | |
| [[ ! "$cache_creation" =~ ^[0-9]+$ ]] && cache_creation=0 | |
| [[ ! "$cache_read" =~ ^[0-9]+$ ]] && cache_read=0 | |
| total_context=$((input_tokens + cache_creation + cache_read)) | |
| fi | |
| fi | |
| # Fallback to 200k if context_window_size not available | |
| [ "$context_limit" -eq 0 ] && context_limit=200000 | |
| # Add auto-compact buffer to consumed tokens if enabled | |
| if [ "$USE_AUTO_COMPACT" = "true" ]; then | |
| total_context=$((total_context + AUTO_COMPACT_BUFFER)) | |
| fi | |
| # Log final calculation if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "TOTAL_CONTEXT: $total_context" >> "$LOG_FILE" | |
| echo "FINAL_CONTEXT_LIMIT: $context_limit" >> "$LOG_FILE" | |
| fi | |
| # Calculate remaining as percentage | |
| if [ "$total_context" -gt 0 ]; then | |
| context_remaining_pct=$((100 - (total_context * 100 / context_limit))) | |
| else | |
| context_remaining_pct="" | |
| fi | |
| fi | |
| # Display context remaining (show --% when no data available) | |
| if [ -n "$context_remaining_pct" ]; then | |
| # Clamp to valid range | |
| [ "$context_remaining_pct" -lt 0 ] && context_remaining_pct=0 | |
| [ "$context_remaining_pct" -gt 100 ] && context_remaining_pct=100 | |
| # Color based on remaining percentage | |
| if [ "$context_remaining_pct" -lt 10 ]; then | |
| context_color="${COLOR_REMOVED}" # red | |
| elif [ "$context_remaining_pct" -lt 25 ]; then | |
| context_color="${COLOR_STYLE}" # yellow | |
| else | |
| context_color="${COLOR_CONTEXT}" # white | |
| fi | |
| printf " | ${RESET}π§ ${context_color}CR: %s%%${RESET}" "$context_remaining_pct" | |
| else | |
| # No context data yet - show placeholder | |
| printf " | ${RESET}π§ ${COLOR_CONTEXT}CR: --%%${RESET}" | |
| fi | |
| need_sep="true" | |
| fi | |
| # Todo count (pending/in_progress from todos folder) | |
| if [ "$SHOW_TODO_COUNT" = "true" ] && [ -n "$session_id" ]; then | |
| todos_dir="$HOME/.claude/todos" | |
| if [ -d "$todos_dir" ]; then | |
| # Find most recent todo file for this session using glob + stat (safe for all filenames) | |
| todo_file="" | |
| newest_mtime=0 | |
| for f in "$todos_dir/${session_id}"-*.json; do | |
| [ -f "$f" ] || continue | |
| fmtime=$(file_mtime "$f") | |
| if [ "$fmtime" -gt "$newest_mtime" ]; then | |
| newest_mtime="$fmtime" | |
| todo_file="$f" | |
| fi | |
| done | |
| if [ -n "$todo_file" ] && [ -f "$todo_file" ]; then | |
| # Extract both counts in a single jq call | |
| read -r todo_pending todo_in_progress <<< "$(jq -r ' | |
| ([.[] | select(.status == "pending")] | length | tostring) + " " + | |
| ([.[] | select(.status == "in_progress")] | length | tostring) | |
| ' "$todo_file" 2>/dev/null)" | |
| [[ ! "$todo_pending" =~ ^[0-9]+$ ]] && todo_pending=0 | |
| [[ ! "$todo_in_progress" =~ ^[0-9]+$ ]] && todo_in_progress=0 | |
| todo_total=$((todo_pending + todo_in_progress)) | |
| if [ "$todo_total" -gt 0 ]; then | |
| printf " | ${RESET}π ${COLOR_STYLE}%s${RESET}" "$todo_total" | |
| fi | |
| fi | |
| fi | |
| fi | |
| # Session ID (first 8 characters) | |
| if [ "$SHOW_SESSION_ID" = "true" ] && [ -n "$session_id" ]; then | |
| session_id_short=$(echo "$session_id" | cut -c1-8) | |
| printf " | ${RESET}π ${COLOR_SESSION}%s${RESET}" "$session_id_short" | |
| fi | |
| # Message count | |
| if [ "$SHOW_MESSAGE_COUNT" = "true" ] && [ -n "$transcript_file" ] && [ -f "$transcript_file" ]; then | |
| msg_count=$(grep -c '"type":"user"' "$transcript_file" 2>/dev/null || echo 0) | |
| printf " | ${RESET}βοΈ ${COLOR_SESSION}%s${RESET}" "$msg_count" | |
| fi | |
| # Session duration (use pre-extracted duration_ms) | |
| if [ "$SHOW_SESSION_DURATION" = "true" ]; then | |
| # Ensure numeric value | |
| [[ ! "$duration_ms" =~ ^[0-9]+$ ]] && duration_ms=0 | |
| if [ "$duration_ms" -gt 0 ]; then | |
| # Convert to minutes:seconds | |
| duration_sec=$((duration_ms / 1000)) | |
| duration_min=$((duration_sec / 60)) | |
| duration_sec_rem=$((duration_sec % 60)) | |
| printf " | ${RESET}β±οΈ ${COLOR_SESSION}%d:%02d${RESET}" "$duration_min" "$duration_sec_rem" | |
| fi | |
| fi | |
| # Session cost (use pre-extracted session_cost) | |
| if [ "$SHOW_SESSION_COST" = "true" ]; then | |
| # Validate it's a number (integer or float) and greater than 0 | |
| if [[ "$session_cost" =~ ^[0-9]+\.?[0-9]*$ ]] && [ "$(awk "BEGIN {print ($session_cost > 0) ? 1 : 0}")" = "1" ]; then | |
| printf " | ${RESET}π° ${COLOR_SESSION}\$%.2f${RESET}" "$session_cost" | |
| fi | |
| fi | |
| # Burn rate (dollars per hour, use pre-extracted session_cost and duration_ms) | |
| if [ "$SHOW_BURN_RATE" = "true" ]; then | |
| # Validate both are positive numbers | |
| if [[ "$session_cost" =~ ^[0-9]+\.?[0-9]*$ ]] && [[ "$duration_ms" =~ ^[0-9]+$ ]] && [ "$duration_ms" -gt 0 ]; then | |
| # Calculate dollars per hour: (cost / duration_ms) * 3600000 | |
| burn_rate=$(awk "BEGIN {printf \"%.2f\", ($session_cost / $duration_ms) * 3600000}" 2>/dev/null) | |
| if [ -n "$burn_rate" ]; then | |
| printf " | ${RESET}π₯ ${COLOR_REMOVED}\$%s/hr${RESET}" "$burn_rate" | |
| fi | |
| fi | |
| fi | |
| # Session name (slug) | |
| if [ "$SHOW_SESSION_NAME" = "true" ] && [ -n "$transcript_file" ] && [ -f "$transcript_file" ]; then | |
| session_slug=$(grep -m1 '"slug"' "$transcript_file" | jq -r '.slug // empty') | |
| if [ -n "$session_slug" ]; then | |
| printf " | ${COLOR_SESSION}%s${RESET}" "$session_slug" | |
| fi | |
| fi | |
| # === DISPLAY LAST USER PROMPT === | |
| if [ "$SHOW_LAST_PROMPT" = "true" ] && [ -n "$last_prompt" ]; then | |
| printf "\n${COLOR_ARROW}β${RESET} %s" "$last_prompt" | |
| if [ ${#last_prompt} -ge "$max_prompt_len" ]; then | |
| printf '...' | |
| fi | |
| fi | |
| # === GENERATE AI-POWERED CONVERSATION SUMMARY === | |
| if [ "$SHOW_AI_SUMMARY" = "true" ] && [ -n "$session_id" ] && [ -n "$transcript_file" ] && [ -f "$transcript_file" ] && command -v claude >/dev/null 2>&1; then | |
| # Get last few messages, filtering out tool results and extracting clean text | |
| # Only get actual user prompts (strings, not tool_result arrays) and assistant text responses | |
| context=$(tail -n 30 "$transcript_file" | jq -r ' | |
| select(.type == "user" or .type == "assistant") | | |
| if .type == "user" then | |
| # Only extract if content is a plain string (not tool_result array) | |
| if (.message.content | type) == "string" then | |
| "User: " + (.message.content | split("\n")[0] | .[0:200]) | |
| else | |
| empty | |
| end | |
| else | |
| # For assistant, get first text block, skip if it starts with tool use indicators | |
| if (.message.content | type) == "array" then | |
| (.message.content[] | select(.type == "text") | .text | split("\n")[0] | .[0:200]) as $text | | |
| if ($text | test("^(Let me|I.ll|I will|```|<)")) then | |
| empty | |
| else | |
| "Assistant: " + $text | |
| end | |
| else | |
| empty | |
| end | |
| end | |
| ' 2>/dev/null | grep -v '^$' | grep -v '^Assistant: $' | tail -n 4 | tr '\n' ' ') | |
| # Strip whitespace and check if context has meaningful content (at least 20 chars) | |
| context_trimmed=$(echo "$context" | tr -d '[:space:]') | |
| if [ -n "$context" ] && [ ${#context_trimmed} -gt 20 ]; then | |
| # Use configurable model for fast, cost-effective summarization | |
| # Pass context via stdin to avoid shell escaping issues | |
| # Use run_with_timeout for macOS compatibility (no GNU timeout required) | |
| max_summary_len=$max_prompt_len | |
| summary=$(printf '%s' "$context" | run_with_timeout 10 claude --model "$AI_SUMMARY_MODEL" -p "Output ONLY a 3-10 word summary in parentheses. No preamble, no explanation, just the summary. Example: (Fixing statusline color variables)" 2>/dev/null | head -c "$max_summary_len") | |
| # Log summary processing if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "CONTEXT: $context" >> "$LOG_FILE" | |
| echo "SUMMARY: $summary" >> "$LOG_FILE" | |
| echo "---" >> "$LOG_FILE" | |
| fi | |
| if [ -n "$summary" ]; then | |
| printf "\n${RESET}π ${COLOR_SUMMARY}%s${RESET}" "$summary" | |
| else | |
| printf "\n${RESET}π ${COLOR_SUMMARY}...${RESET}" | |
| fi | |
| else | |
| printf "\n${RESET}π ${COLOR_SUMMARY}...${RESET}" | |
| fi | |
| fi | |
| # === DISPLAY LINE CHANGES === | |
| # Add GitHub-style line changes (use pre-extracted values) | |
| if [ "$SHOW_LINE_CHANGES" = "true" ]; then | |
| if [ "$lines_added" != "0" ] || [ "$lines_removed" != "0" ]; then | |
| printf '\n' | |
| if [ "$lines_added" != "0" ]; then | |
| printf "${COLOR_ADDED}+%s${RESET}" "$lines_added" | |
| fi | |
| if [ "$lines_removed" != "0" ]; then | |
| if [ "$lines_added" != "0" ]; then | |
| printf ' ' | |
| fi | |
| printf "${COLOR_REMOVED}-%s${RESET}" "$lines_removed" | |
| fi | |
| fi | |
| fi | |
| # Ensure output ends with newline | |
| printf '\n' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment