Last active
December 23, 2025 05:24
-
-
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
| #!/bin/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": "\"C:/Program Files/Git/bin/bash.exe\" ~/.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 === | |
| # | |
| # === CLI OPTIONS === | |
| # --update Download and install the latest version from gist | |
| # --debug Enable debug logging to ~/.claude/statusline.log | |
| # | |
| # 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 both new context_window API and legacy transcript fallback | |
| # | |
| # 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) | |
| # 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 | |
| } | |
| # === 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 | |
| # === 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" | |
| # 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 | |
| # Verify downloaded file is valid (has shebang) | |
| if head -1 "$TMP_FILE" | grep -q '^#!/bin/bash'; 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 | |
| # === EXTRACT DATA FROM JSON INPUT === | |
| session_id=$(echo "$input" | jq -r '.session_id // ""') | |
| project_dir=$(echo "$input" | jq -r '.workspace.project_dir // .workspace.current_dir // .cwd // ""') | |
| last_prompt="" | |
| # 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=$(echo "$input" | jq -r '.transcript_path // ""') | |
| 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 === | |
| 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 | |
| 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 | head -c "$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 | |
| version=$(echo "$input" | jq -r '.version // ""') | |
| output_style=$(echo "$input" | jq -r '.output_style.name // ""') | |
| git_branch="" | |
| # Get git branch if available | |
| current_dir=$(echo "$input" | jq -r '.workspace.current_dir // .cwd // ""') | |
| 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 | |
| git_status=$(cd "$current_dir" && git status --porcelain 2>/dev/null) | |
| if [ -z "$git_status" ]; then | |
| git_status_icon="β" # clean | |
| else | |
| git_status_icon="β" # dirty/uncommitted changes | |
| fi | |
| # Get ahead/behind counts | |
| 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) | |
| else | |
| git_behind=0 | |
| 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 | |
| if [ "$SHOW_PATH" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π ${COLOR_PATH}%s${RESET}" "$(echo "$input" | jq -r '.workspace.current_dir // .cwd // "unknown"' | sed "s|^$HOME|~|g")" | |
| 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 ]; then | |
| printf " ${COLOR_ADDED}β%s${RESET}" "$git_ahead" | |
| fi | |
| if [ "$git_behind" -gt 0 ]; 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 | |
| if [ "$SHOW_MODEL" = "true" ]; then | |
| [ -n "$need_sep" ] && printf " | " | |
| printf "${RESET}π€ ${COLOR_MODEL}%s${RESET}" "$(echo "$input" | jq -r '.model.display_name // "Claude"')" | |
| 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 | |
| # Try to get context window info from input JSON (new API in latest Claude Code) | |
| # Use current_usage for actual context window usage (not session totals) | |
| current_input=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0') | |
| cache_creation=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0') | |
| cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0') | |
| context_limit=$(echo "$input" | jq -r '.context_window.context_window_size // 0') | |
| # Log context window extraction if debugging | |
| if [ "$DEBUG_LOG" = true ]; then | |
| echo "CONTEXT_WINDOW_RAW: $(echo "$input" | jq -c '.context_window // "null"')" >> "$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 | |
| # Always display context remaining (show --% when no data available) | |
| if [ "$total_context" -gt 0 ]; then | |
| # Calculate remaining as percentage | |
| context_remaining_pct=$((100 - (total_context * 100 / context_limit))) | |
| # 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 (includes agent todos) | |
| todo_file=$(ls -t "$todos_dir/${session_id}"-*.json 2>/dev/null | head -1) | |
| if [ -n "$todo_file" ] && [ -f "$todo_file" ]; then | |
| todo_pending=$(jq '[.[] | select(.status == "pending")] | length' "$todo_file" 2>/dev/null || echo 0) | |
| todo_in_progress=$(jq '[.[] | select(.status == "in_progress")] | length' "$todo_file" 2>/dev/null || echo 0) | |
| [[ ! "$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 | |
| if [ "$SHOW_SESSION_DURATION" = "true" ]; then | |
| duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0') | |
| # 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 | |
| if [ "$SHOW_SESSION_COST" = "true" ]; then | |
| session_cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0') | |
| # 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) | |
| if [ "$SHOW_BURN_RATE" = "true" ]; then | |
| session_cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0') | |
| duration_ms=$(echo "$input" | jq -r '.cost.total_duration_ms // 0') | |
| # 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 Claude Haiku for fast, cost-effective summarization | |
| # Pass context via stdin to avoid shell escaping issues | |
| # Limit summary to terminal width minus space for icon (use global max_prompt_len) | |
| max_summary_len=$max_prompt_len | |
| summary=$(printf '%s' "$context" | timeout 10s claude --model haiku -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 (independent of transcript file) | |
| if [ "$SHOW_LINE_CHANGES" = "true" ]; then | |
| lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0') | |
| lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0') | |
| 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