#!/bin/bash # statusline.sh โ€” Copilot CLI custom status line. # # Layout (segments joined by โ”‚): # ๐Ÿ“ dir ๐ŸŒฟ branch โ”‚ โ–ฐโ–ฐโ–ฑ ctx% โ”‚ tokens cache cost diff โ”‚ time โ”‚ Copilot vโ€ฆ โ”‚ effort yolo โ”‚ โœฆ session # # Each section below owns one segment: it reads the already-extracted raw values # and builds a formatted *_SEG string (sometimes empty). The final line just # concatenates them. All payload fields are pulled up front in a single jq pass. input=$(cat) # โ”€โ”€ Colors / styles โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ RST=$'\033[0m'; BLD=$'\033[1m'; DIM=$'\033[2m' CYAN=$'\033[36m'; YEL=$'\033[33m'; GREEN=$'\033[32m'; RED=$'\033[31m' DYEL=$'\033[2;33m'; LMAG=$'\033[2;95m' LIBLUE=$'\033[38;5;39m' # LinkedIn blue (~#0077B5) SEP="${DIM}โ”‚${RST}" # โ”€โ”€ Extract every payload field in ONE jq pass โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # jq @sh shell-quotes each value so the eval is injection-safe; this replaces # ~16 separate jq subprocesses. PCT is floored to an integer here (was a # trailing `cut -d. -f1`). current_context_used_percentage tracks the live # window; used_percentage is the last-call / max-window fallback. eval "$(echo "$input" | jq -r ' [ "CWD=\(.cwd // .workspace.current_dir // "" | @sh)", "SESSION=\(.session_name // "" | @sh)", "BRANCH=\(.worktree.branch // "" | @sh)", "VERSION=\(.version // "" | @sh)", "PCT=\((.context_window.current_context_used_percentage // .context_window.used_percentage // 0) | floor | tostring | @sh)", "IN=\(.context_window.total_input_tokens // 0 | tostring | @sh)", "OUT=\(.context_window.total_output_tokens // 0 | tostring | @sh)", "CACHE_READ=\(.context_window.total_cache_read_tokens // 0 | tostring | @sh)", "DUR_MS=\(.cost.total_duration_ms // 0 | tostring | @sh)", "API_MS=\(.cost.total_api_duration_ms // 0 | tostring | @sh)", "ADDED=\(.cost.total_lines_added // 0 | tostring | @sh)", "REMOVED=\(.cost.total_lines_removed // 0 | tostring | @sh)", "AIU=\(.ai_used.formatted // "0" | @sh)", "DISPLAY_NAME=\(.model.display_name // "" | @sh)" ] | .[]')" DIR=$(basename "$CWD") # โ”€โ”€ Directory + git branch โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Branch comes from the payload when present; otherwise shell out to git. The # dirty dot (โ—) needs git either way. if [ -z "$BRANCH" ] && [ -n "$CWD" ]; then BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null) fi GIT="" if [ -n "$BRANCH" ]; then DIRTY="" [ -n "$(git -C "$CWD" status --porcelain 2>/dev/null)" ] && DIRTY=" ${YEL}โ—${RST}" GIT=" ${GREEN}๐ŸŒฟ ${BRANCH}${RST}${DIRTY}" fi DIR_SEG="${CYAN}๐Ÿ“ ${DIR}${RST}${GIT}" # โ”€โ”€ Context-window bar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ BAR_WIDTH=20 FILLED=$((PCT * BAR_WIDTH / 100)) EMPTY=$((BAR_WIDTH - FILLED)) BF='โ–ฐ'; BE='โ–ฑ' BAR=$(printf "$BF%.0s" $(seq 1 $FILLED 2>/dev/null))$(printf "$BE%.0s" $(seq 1 $EMPTY 2>/dev/null)) if [ "$PCT" -ge 80 ]; then BC=$RED elif [ "$PCT" -ge 50 ]; then BC=$YEL else BC=$GREEN; fi CONTEXT_SEG=" ${SEP} ${BC}${BAR} ${PCT}%${RST}" # โ”€โ”€ Token / cache / cost / diff info โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ TOKENS=$(awk "BEGIN{printf \"%.1fk\", ($IN+$OUT)/1000}") # Cache hit %: cache reads as a share of total input. total_input_tokens already # includes the cache-read and cache-write tokens (the API reports input as # uncached + cache_read + cache_write), so the denominator is just IN โ€” adding # the cache fields again would double-count and roughly halve the real rate. CACHE="" if [ "$IN" -gt 0 ]; then HIT=$((CACHE_READ * 100 / IN)) [ "$HIT" -gt 100 ] && HIT=100 CACHE=" ${DYEL}cache:${HIT}%${RST}" fi # Cost slot: prefer AIU when nonzero, else fall back to API time (free/internal). COST_PART="" if [ "$AIU" != "0" ]; then COST_PART=" ${DYEL}aiu:${AIU}${RST}" elif [ "$API_MS" -gt 0 ]; then API_S=$((API_MS / 1000)) if [ "$API_S" -ge 60 ]; then API_FMT="$((API_S/60))m$((API_S%60))s"; else API_FMT="${API_S}s"; fi COST_PART=" ${DYEL}api:${API_FMT}${RST}" fi # Line-change badge, only when nonzero. DIFF_PART="" if [ "$ADDED" -gt 0 ] || [ "$REMOVED" -gt 0 ]; then DIFF_PART=" ${DIM}+${ADDED}/-${REMOVED}${RST}" fi INFO_SEG=" ${SEP} ${DYEL}tokens:${TOKENS}${RST}${CACHE}${COST_PART}${DIFF_PART}" # โ”€โ”€ Elapsed session time โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ TOTAL_S=$((DUR_MS / 1000)) TIME="" [ $((TOTAL_S / 604800)) -gt 0 ] && TIME+="$((TOTAL_S / 604800))w " [ $(((TOTAL_S % 604800) / 86400)) -gt 0 ] && TIME+="$(((TOTAL_S % 604800) / 86400))d " [ $(((TOTAL_S % 86400) / 3600)) -gt 0 ] && TIME+="$(((TOTAL_S % 86400) / 3600))h " [ $(((TOTAL_S % 3600) / 60)) -gt 0 ] && TIME+="$(((TOTAL_S % 3600) / 60))m " TIME+="$((TOTAL_S % 60))s" TIME_SEG=" ${SEP} ${DIM}${TIME}${RST}" # โ”€โ”€ Version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ VERSION_SEG=" ${SEP} ${DIM}Copilot v${VERSION:-?}${RST}" # โ”€โ”€ Reasoning effort + YOLO emoji โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # Effort lives in model.display_name (e.g. "claude-opus-4.8 ยท xhigh ยท 1M # context"), the same string the footer shows; take the "ยท"-delimited segment # matching a known level and map it to a brainpower-ramp emoji. Absent token # (non-reasoning model) โ†’ no icon. EFFORT=$(echo "$DISPLAY_NAME" | tr 'ยท' '\n' \ | sed -E 's/^[[:space:]]+|[[:space:]]+$//g' \ | grep -ixE 'none|low|medium|high|xhigh|max' | head -1 | tr 'A-Z' 'a-z') case "$EFFORT" in none) EFFORT_ICON="๐Ÿ˜ด" ;; low) EFFORT_ICON="โ˜น๏ธ" ;; medium) EFFORT_ICON="๐Ÿ˜" ;; high) EFFORT_ICON="๐Ÿ™‚" ;; xhigh) EFFORT_ICON="๐Ÿ˜ƒ" ;; max) EFFORT_ICON="๐Ÿคฏ" ;; *) EFFORT_ICON="" ;; esac EFFORT_PART="" [ -n "$EFFORT_ICON" ] && EFFORT_PART="${EFFORT_ICON} " # YOLO / permission indicator โ€” read from the parent copilot process's launch # args (the payload carries no permission state) and the COPILOT_ALLOW_ALL env # var. Full bypass โ†’ ๐Ÿ’ฃ; partial grants โ†’ a yellow โš ๏ธ listing which. PARENT_ARGS=$(ps -o args= -p "$PPID" 2>/dev/null) HAS_TOOLS=0; HAS_PATHS=0; HAS_URLS=0 [[ "$PARENT_ARGS" == *" --allow-all-tools"* || -n "$COPILOT_ALLOW_ALL" ]] && HAS_TOOLS=1 [[ "$PARENT_ARGS" == *" --allow-all-paths"* ]] && HAS_PATHS=1 [[ "$PARENT_ARGS" == *" --allow-all-urls"* ]] && HAS_URLS=1 YOLO="" if [[ "$PARENT_ARGS" == *" --yolo"* || "$PARENT_ARGS" == *" --allow-all"* ]] \ || { [ "$HAS_TOOLS" = 1 ] && [ "$HAS_PATHS" = 1 ] && [ "$HAS_URLS" = 1 ]; }; then YOLO="๐Ÿ’ฃ " elif [ "$HAS_TOOLS" = 1 ] || [ "$HAS_PATHS" = 1 ] || [ "$HAS_URLS" = 1 ]; then parts="" [ "$HAS_TOOLS" = 1 ] && parts+="tools " [ "$HAS_PATHS" = 1 ] && parts+="paths " [ "$HAS_URLS" = 1 ] && parts+="urls " YOLO="${YEL}โš ๏ธ (${parts% })${RST} " fi EMOJI_SEG="" [ -n "${EFFORT_PART}${YOLO}" ] && EMOJI_SEG=" ${SEP} ${EFFORT_PART}${YOLO}" # โ”€โ”€ Session name โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ SESSION_SEG="" [ -n "$SESSION" ] && SESSION_SEG=" ${SEP} ${LIBLUE}โœฆ ${SESSION}${RST}" # โ”€โ”€ Terminal tab/window title (session name, else directory) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # OSC 2 = window title, OSC 1 = tab/icon title โ€” emit both for broad support. TITLE="${SESSION:-$DIR}" [ -n "$TITLE" ] && printf '\033]2;%s\007\033]1;%s\007' "$TITLE" "$TITLE" > /dev/tty 2>/dev/null # โ”€โ”€ Assemble โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ echo "${DIR_SEG}${CONTEXT_SEG}${INFO_SEG}${TIME_SEG}${VERSION_SEG}${EMOJI_SEG}${SESSION_SEG}"