Last active
June 22, 2026 23:56
-
-
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 (v2.4.8) === | |
| # - Added GLM-5.2 to the 1M-context fallback (z.ai). GLM-5.1 and earlier stay at | |
| # 200K. Only affects the manual-calculation path when the harness doesn't supply | |
| # context_window percentages; compaction scaling still correctly skips GLM (non-Claude). | |
| # | |
| # === WHAT'S NEW (v2.4.7) === | |
| # - Fixed CR% hitting 0% far too early on Opus 4.8 / 1M-context sessions: | |
| # the compaction threshold now defaults to ~84% (the real Claude Code auto-compact | |
| # point) instead of a per-model 40%/50% magic number, and is the same for 200K and | |
| # 1M windows. CR: 0% now lines up with when auto-compact actually fires. | |
| # - Honors Claude Code's CLAUDE_AUTOCOMPACT_PCT_OVERRIDE env var automatically. | |
| # - COMPACT_THRESHOLD_PCT=0 now truly disables scaling (shows raw API %) as documented. | |
| # - Corrected stale comment: the API context percentages are RAW token math and do | |
| # NOT account for auto-compaction (per current Claude Code statusline JSON spec). | |
| # | |
| # === WHAT'S NEW (v2.3.0) === | |
| # - Added rate limit display (5-hour and 7-day usage for Claude.ai subscribers) | |
| # - Added worktree display (name + branch when in a git worktree) | |
| # - Added agent name display (when running with --agent flag) | |
| # - Added vim mode display (NORMAL/INSERT when vim mode enabled) | |
| # - Added exceeds_90pct context warning (model-aware) | |
| # - All new fields are configurable toggles with color support | |
| # | |
| # === WHAT'S NEW (v2.2.1) === | |
| # - 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 | |
| # - Rate limit tracking (5-hour and 7-day windows for Claude.ai subscribers) | |
| # - Git worktree awareness (name + branch display) | |
| # - Agent name display (--agent flag) | |
| # - Vim mode indicator (NORMAL/INSERT) | |
| # - Exceeds 90% context warning (model-aware) | |
| # - Session file export for Claude self-awareness (WRITE_SESSION_FILE) | |
| # | |
| # 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 | |
| # π rate limits (5h/7d) | π³ worktree | π€ agent | β¨οΈ vim mode | β οΈ >90% | |
| # Line 2: β Last user prompt | |
| # Line 3: π AI-powered conversation summary (via Claude Haiku) | |
| # Line 4: +lines/-lines (GitHub-style indicators) | |
| # | |
| # Session file output (WRITE_SESSION_FILE): | |
| # Writes .claude/statusline-<session_id>.local.json to the project directory | |
| # after each update, allowing Claude to read its own context utilization, | |
| # cost, rate limits, and other session metrics via the Read tool. | |
| SCRIPT_VERSION="2.4.8" | |
| # 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" | |
| # Rate limit color (bold yellow on black) | |
| COLOR_RATE_LIMIT="\033[01;33;${BG}m" | |
| # Worktree color (bold green on black) | |
| COLOR_WORKTREE="\033[01;32;${BG}m" | |
| # Warning color (bold red on black) | |
| COLOR_WARNING="\033[01;31;${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_SESSION_ID_FULL="false" # π Session ID (full UUID) | |
| 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 | |
| SHOW_RATE_LIMITS="true" # π Rate limit usage (5-hour and 7-day windows, Claude.ai subscribers) | |
| SHOW_WORKTREE="false" # π³ Git worktree name and branch (when in a worktree) | |
| SHOW_AGENT_NAME="false" # π€ Agent name (when running with --agent flag) | |
| SHOW_VIM_MODE="false" # β¨οΈ Vim mode indicator (NORMAL/INSERT) | |
| SHOW_EXCEEDS_90PCT="false" # β οΈ Warning when context usage exceeds 90% of model's window | |
| WRITE_SESSION_FILE="false" # πΎ Write session data to .claude/statusline-<session_id>.local.json for Claude to read | |
| # 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 === | |
| # Compaction threshold: the raw used_percentage at which Claude Code auto-compacts. | |
| # When non-zero, CR% is scaled so 0% = compaction about to fire, 100% = empty window. | |
| # This makes the display reflect usable context rather than raw token math. | |
| # | |
| # Claude Code auto-compacts at ~84% of the context window by default (the same | |
| # percentage for both 200K and 1M windows, including Opus 4.8 1M), and is | |
| # user-configurable via the CLAUDE_AUTOCOMPACT_PCT_OVERRIDE environment variable. | |
| # This script honors CLAUDE_AUTOCOMPACT_PCT_OVERRIDE automatically; the | |
| # COMPACT_THRESHOLD_PCT setting below takes precedence over it when set. | |
| # Set to 0 to disable scaling and show raw API percentages (CR ~16% at compaction). | |
| COMPACT_THRESHOLD_PCT="${COMPACT_THRESHOLD_PCT:-}" | |
| # === 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), | |
| "rate_5h_used=" + (.rate_limits.five_hour.used_percentage // "" | tostring | @sh), | |
| "rate_5h_resets=" + (.rate_limits.five_hour.resets_at // "" | tostring | @sh), | |
| "rate_7d_used=" + (.rate_limits.seven_day.used_percentage // "" | tostring | @sh), | |
| "rate_7d_resets=" + (.rate_limits.seven_day.resets_at // "" | tostring | @sh), | |
| "worktree_name=" + (.worktree.name // "" | @sh), | |
| "worktree_path=" + (.worktree.path // "" | @sh), | |
| "worktree_branch=" + (.worktree.branch // "" | @sh), | |
| "agent_name=" + (.agent.name // "" | @sh), | |
| "vim_mode=" + (.vim.mode // "" | @sh), | |
| "used_pct_raw=" + (.context_window.used_percentage // 0 | tostring | @sh) | |
| ' 2>/dev/null) || { | |
| printf "Error parsing input JSON" | |
| exit 0 | |
| } | |
| eval "$jq_bulk" | |
| # Strip ANSI escape sequences from string values that may contain them | |
| model_name=$(printf '%s' "$model_name" | sed 's/\x1b\[[0-9;]*[mGKHf]//g; s/\[[0-9;]*m\]//g; s/\[[0-9;]*m//g') | |
| model_name=$(printf '%s' "$model_name" | sed -E 's/ \(([0-9]+\.?[0-9]*[KMGT]?B?) context\)/ \1/g') | |
| # 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 | |
| # === WRITE SESSION FILE FOR CLAUDE ACCESS === | |
| # Writes key session metrics to a project-local JSON file that Claude can read | |
| if [ "$WRITE_SESSION_FILE" = "true" ] && [ -n "$session_id" ] && [ -n "$project_dir" ] && [ -d "$project_dir" ]; then | |
| session_file_dir="${project_dir}/.claude" | |
| session_file="${session_file_dir}/statusline-${session_id}.local.json" | |
| mkdir -p "$session_file_dir" 2>/dev/null | |
| # Write compact JSON with all key metrics | |
| cat > "$session_file" <<EOJSON | |
| { | |
| "updated_at": "$(date -u '+%Y-%m-%dT%H:%M:%SZ')", | |
| "session_id": "${session_id}", | |
| "model": "${model_name}", | |
| "context_window": { | |
| "used_percentage": ${used_pct:-null}, | |
| "remaining_percentage": ${remaining_pct:-null}, | |
| "context_window_size": ${ctx_limit:-0}, | |
| "current_input_tokens": ${ctx_current_input:-0}, | |
| "cache_creation_tokens": ${ctx_cache_creation:-0}, | |
| "cache_read_tokens": ${ctx_cache_read:-0} | |
| }, | |
| "cost": { | |
| "total_cost_usd": ${session_cost:-0}, | |
| "total_duration_ms": ${duration_ms:-0}, | |
| "lines_added": ${lines_added:-0}, | |
| "lines_removed": ${lines_removed:-0} | |
| }, | |
| "rate_limits": { | |
| "five_hour_used_pct": ${rate_5h_used:-null}, | |
| "five_hour_resets_at": ${rate_5h_resets:-null}, | |
| "seven_day_used_pct": ${rate_7d_used:-null}, | |
| "seven_day_resets_at": ${rate_7d_resets:-null} | |
| }, | |
| "worktree": { | |
| "name": $([ -n "$worktree_name" ] && echo "\"${worktree_name}\"" || echo "null"), | |
| "branch": $([ -n "$worktree_branch" ] && echo "\"${worktree_branch}\"" || echo "null") | |
| }, | |
| "agent_name": $([ -n "$agent_name" ] && echo "\"${agent_name}\"" || echo "null"), | |
| "vim_mode": $([ -n "$vim_mode" ] && echo "\"${vim_mode}\"" || echo "null"), | |
| "exceeds_90pct": ${exceeds_90pct:-false} | |
| } | |
| EOJSON | |
| 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 | |
| # Prefer pre-calculated remaining_percentage from Claude Code API when available. | |
| # NOTE: these API percentages are RAW token math (input + cache_creation + cache_read | |
| # over context_window_size) and do NOT account for auto-compaction. The compaction | |
| # threshold scaling further below adjusts the raw value into usable-context terms. | |
| if [[ "$remaining_pct" =~ ^[0-9]+\.?[0-9]*$ ]]; 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 Claude Code versions without API percentages | |
| # 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 based on model: Opus and GLM-5.2 default to 1M, others to 200k | |
| # (GLM-5.1 and earlier are 200k β handled by the default case.) | |
| if [ "$context_limit" -eq 0 ]; then | |
| case "$model_name" in | |
| *[Oo]pus*) context_limit=1000000 ;; | |
| *GLM-5.2*) context_limit=1000000 ;; | |
| *) context_limit=200000 ;; | |
| esac | |
| 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 | |
| # Apply compaction threshold scaling so CR% reflects usable context. | |
| # Only applies to Claude models where auto-compaction fires well before 100% usage. | |
| # Non-Claude providers (z.ai, etc.) report accurate percentages β skip scaling. | |
| _is_claude=false | |
| case "$model_name" in | |
| *[Cc]laude*|*[Oo]pus*|*[Ss]onnet*|*[Hh]aiku*) _is_claude=true ;; | |
| esac | |
| if [ "$_is_claude" = "true" ] && [[ -n "$context_remaining_pct" ]] && [[ "$context_remaining_pct" =~ ^[0-9]+$ ]]; then | |
| # Threshold precedence: explicit COMPACT_THRESHOLD_PCT (incl. 0 to disable) > | |
| # Claude Code's own CLAUDE_AUTOCOMPACT_PCT_OVERRIDE > built-in default (~84%, | |
| # the current Claude Code auto-compact default for both 200K and 1M windows). | |
| if [[ "$COMPACT_THRESHOLD_PCT" =~ ^[0-9]+$ ]]; then | |
| _threshold="$COMPACT_THRESHOLD_PCT" | |
| elif [[ "$CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" =~ ^[0-9]+$ ]] && [ "$CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" -gt 0 ]; then | |
| _threshold="$CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" | |
| else | |
| _threshold=84 | |
| fi | |
| # _threshold of 0 (or out of range) disables scaling β show raw API percentages. | |
| if [ "$_threshold" -gt 0 ] && [ "$_threshold" -le 100 ]; then | |
| _used=$((100 - context_remaining_pct)) | |
| if [ "$_used" -ge "$_threshold" ]; then | |
| context_remaining_pct=0 | |
| else | |
| context_remaining_pct=$((100 - (_used * 100 / _threshold))) | |
| fi | |
| 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 | |
| # Rate limits (5-hour and 7-day windows) | |
| if [ "$SHOW_RATE_LIMITS" = "true" ]; then | |
| if [[ "$rate_5h_used" =~ ^[0-9]+\.?[0-9]*$ ]]; then | |
| # Color based on usage percentage | |
| rate_5h_int=$(printf "%.0f" "$rate_5h_used") | |
| if [ "$rate_5h_int" -ge 90 ]; then | |
| rate_5h_color="${COLOR_REMOVED}" # red | |
| elif [ "$rate_5h_int" -ge 75 ]; then | |
| rate_5h_color="\033[38;5;208;${BG}m" # orange | |
| elif [ "$rate_5h_int" -ge 50 ]; then | |
| rate_5h_color="${COLOR_STYLE}" # yellow | |
| else | |
| rate_5h_color="${COLOR_ADDED}" # green | |
| fi | |
| printf " | ${RESET}π ${rate_5h_color}5h: %s%%${RESET}" "$rate_5h_int" | |
| need_sep="true" | |
| fi | |
| if [[ "$rate_7d_used" =~ ^[0-9]+\.?[0-9]*$ ]]; then | |
| rate_7d_int=$(printf "%.0f" "$rate_7d_used") | |
| if [ "$rate_7d_int" -ge 90 ]; then | |
| rate_7d_color="${COLOR_REMOVED}" # red | |
| elif [ "$rate_7d_int" -ge 75 ]; then | |
| rate_7d_color="\033[38;5;208;${BG}m" # orange | |
| elif [ "$rate_7d_int" -ge 50 ]; then | |
| rate_7d_color="${COLOR_STYLE}" # yellow | |
| else | |
| rate_7d_color="${COLOR_ADDED}" # green | |
| fi | |
| printf " ${rate_7d_color}7d: %s%%${RESET}" "$rate_7d_int" | |
| need_sep="true" | |
| fi | |
| fi | |
| # Worktree info | |
| if [ "$SHOW_WORKTREE" = "true" ] && [ -n "$worktree_name" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| if [ -n "$worktree_branch" ]; then | |
| printf "${RESET}π³ ${COLOR_WORKTREE}%s (%s)${RESET}" "$worktree_name" "$worktree_branch" | |
| else | |
| printf "${RESET}π³ ${COLOR_WORKTREE}%s${RESET}" "$worktree_name" | |
| fi | |
| need_sep="true" | |
| fi | |
| # Agent name | |
| if [ "$SHOW_AGENT_NAME" = "true" ] && [ -n "$agent_name" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π€ ${COLOR_MODEL}%s${RESET}" "$agent_name" | |
| need_sep="true" | |
| fi | |
| # Vim mode | |
| if [ "$SHOW_VIM_MODE" = "true" ] && [ -n "$vim_mode" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}β¨οΈ ${COLOR_CONTEXT}%s${RESET}" "$vim_mode" | |
| need_sep="true" | |
| fi | |
| # Exceeds 90% context warning (only for Claude models) | |
| if [ "$SHOW_EXCEEDS_90PCT" = "true" ] && [ "$_is_claude" = "true" ]; then | |
| # Use raw API used_percentage to avoid double-scaling from compaction threshold | |
| _ctx_used_pct="" | |
| if [[ "$used_pct_raw" =~ ^[0-9]+\.?[0-9]*$ ]]; then | |
| _ctx_used_pct=$(printf "%.0f" "$used_pct_raw") | |
| elif [ -n "$context_remaining_pct" ] && [[ "$context_remaining_pct" =~ ^[0-9]+$ ]]; then | |
| _ctx_used_pct=$((100 - context_remaining_pct)) | |
| fi | |
| if [ -n "$_ctx_used_pct" ] && [ "$_ctx_used_pct" -ge 90 ] 2>/dev/null; then | |
| exceeds_90pct=true | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${COLOR_WARNING}β >${_ctx_used_pct}%%${RESET}" | |
| need_sep="true" | |
| fi | |
| fi | |
| # === DISPLAY SESSION ID + LAST USER PROMPT ON LINE 2 === | |
| session_id_shown="" | |
| if [ "$SHOW_SESSION_ID_FULL" = "true" ] && [ -n "$session_id" ]; then | |
| printf "\n${RESET}π ${COLOR_SESSION}%s${RESET}" "$session_id" | |
| session_id_shown="true" | |
| fi | |
| if [ "$SHOW_LAST_PROMPT" = "true" ] && [ -n "$last_prompt" ]; then | |
| if [ -n "$session_id_shown" ]; then | |
| printf " ${COLOR_ARROW}β${RESET} %s" "$last_prompt" | |
| else | |
| printf "\n${COLOR_ARROW}β${RESET} %s" "$last_prompt" | |
| fi | |
| 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