
Claude Code Status Line:專業奴隸主,要有一套自己的監控系統
先說結論
- Status Line 是 Claude Code 8/8 推出的新功能(官方文件連結)
可以讓你自訂資訊在指令輸入框下面 - 不推薦用 ccuseage 的狀態列整合,搞的狀態列會有一堆錢 $ 的符號 XD
- 如果只能挑一個資訊顯示,我會選目前 context 使用量
- 我會提供我的完整 Status Line 的程式碼,你可以拿去再調整
先來看我的 Status Line 吧

應該馬上就懂了
雖然狀態列這個功能,對於提升 AI 能力在寫程式上沒有什麼幫助
一度讓我以為 Anthropic 是不是不知道要做啥功能了,我要這個狀態列幹嘛?
用了才知道…原來
傻瓜我們都一樣,被「AI 情」傷了又傷
肯定是很多前輩深受 AI 裝傻所苦
Claude Code 8月多了的新功能,像是 Status Line、/context 指令
都可以幫助我們做 Context 分析,可以滿足我無處安放的控制欲
欲練神功,必先多工
像我們這種訂閱 Max 20x 方案的盤子
肯定對於 token 的追求更加要求效率
不可能下完一個指令呆呆地看著他跑,肯定是又開一個新視窗
繼續把下一個任務丟出去
因此隨時有三四個 Claude Code 在跑,是很正常的
我自己會在桌上放一張白紙,隨時記下我要做的最大的 todo
有點像是任務指北針
最重要的事就是確保自己在做最重要的事
Todo任務清單已經實證對 AI 有效,對人類也一樣有效 XD
再來就是啟動多工模式,把工作都 assign 出去給 AI 處理
然後等語音通知回報任務結果,再回來驗收,然後繼續下一個循環
這裡我們來討論大腦的 context switch
如果同時開三個以上的 Claude Code
只能說叔叔有練過,小朋友不要學 XD
我們的大腦的工作記憶大概就是 5 格
處理完第三個視窗後,我已經忘記第一個在幹嘛了
然後就會往上滑找我的指令,順便回憶
但偏偏 Claude Code 裡面使用者指令的顏色是灰色的!
跟 AI 在思考顯示的思路文字一樣 XD
真的有夠難找,補個幹
因此我在 Status Line 第二行增加一個功能
顯示我最後的指令,最多三行
並且用舒服的大地綠色保護眼睛

不過我承認有時候我連自己上一個指令在寫啥都看不懂
備註:這裡我還會搭配 AI 摘要任務進度
讓我可以從終端機狀態欄看到現在在做的任務
搭配上一個指令,就沒有大腦切換當機問題
但這不是狀態列功能,要搭配使用,之後我會寫一篇介紹
保護你的 Context
再來是最重要的環節
如果我們都已經壓榨我們的精神力,同時多工好幾個視窗
結果只是加速產生了更多大便
那可不是浪費電這樣的小事了
因此 AI 的輸出品質絕對值得讓我們花精力去好好的優化
會影響 AI 輸出的三本柱
- 模型:現在好的模型就是貴
- Prompt:不說人話,表現就是爛
- Context:最近一直在講的 Context Engineering
(打 Context 很煩,接下來我用“上下文”表示 Context)
因為 Claude Code 會把每個對話紀錄存在本機
所以我們可以寫一個程式去查目前上下文使用量
然後顯示在狀態列上
我設定好後第一個發現就是
你能用的上下文其實是 160K
Opus4.1 和 Sonnet4 都有 200k 的上下文視窗
但如果你有開自動壓縮的話
你能用的上下文其實是 160K
大概是 80% 左右的上下文,他就會開始自動壓縮

那 20% 的空間應該是要保留用來壓縮上下文用的
如果你把自動壓縮功能關掉,然後把它用滿
你會發現你不能再做任何事
你只能回退讓出空間,或是重啟新的對話
再來是很多研究都表明 “長上下文” 會影響模型輸出的品質
簡單說就是你上下文越長
表現就會越不好,你就會越森77
狀態列就是一個可以用來監控上下文用量的好所在
我現在盡量保持上下文不超過 50%
超過之後我就會開始思考要不要手動壓縮(我有一個我自訂的壓縮指令)
或是結束對話,重新安排
有時候就是沒辦法,用到他自動壓縮
我覺得也是人之常情
如果結果沒有問題那就沒有問題
如果有問題,那可能就是這個問題(好像廢話)
總之
上下文是稀缺資源,需要控管
你今天對得起自己嗎?
最後簡單說明狀態列最旁邊的 “今天共使用時數”
簡單說他會記錄每個 session 運行時間
假設有一個 Claude Code在執行
跑了一小時,就會記錄一小時
同時有兩個執行一小時,就會記錄兩小時
每天0點重置
因此這可以表示你今天的工作強度
如果睡前發現沒有 10 小時,表示我太廢了 XDD
因為有記錄,所以可以跑統計時數

嗯...我是不是沒朋友...
沒關係,泰呈~
你還有 Claude Code啊~
我的 Status Line 設定(抄作業時間)
statusline.sh
#!/bin/bash
# 讀取輸入
input=$(cat)
# 快取檔案路徑
CACHE_DIR="$HOME/.claude/cache"
GIT_CACHE="$CACHE_DIR/git_branch"
mkdir -p "$CACHE_DIR"
# 基本資訊提取 - 使用單一 jq 調用
read -r MODEL SESSION_ID CURRENT_DIR TRANSCRIPT_PATH <<< $(echo "$input" | jq -r '
.model.display_name,
.session_id,
.workspace.current_dir,
(.transcript_path // "")
' | tr '\\n' ' ')
PROJECT_NAME=$(basename "$CURRENT_DIR")
# 根據模型設定顏色和圖標
case "$MODEL" in
*"Opus"*)
MODEL_COLOR="\\033[38;2;195;158;83m"
MODEL_ICON="💛"
;;
*"Sonnet"*)
MODEL_COLOR="\\033[38;2;118;170;185m"
MODEL_ICON="💠"
;;
*"Haiku"*)
MODEL_COLOR="\\033[38;2;255;182;193m"
MODEL_ICON="🌸"
;;
esac
COLOR_RESET="\\033[0m"
MESSAGE_COLOR="\\033[38;2;152;195;121m"
# Git 分支快取機制(5秒有效期)
BRANCH=""
if git rev-parse --git-dir > /dev/null 2>&1; then
current_time=$(date +%s)
# 檢查快取是否有效
if [ -f "$GIT_CACHE" ]; then
cache_time=$(stat -f %m "$GIT_CACHE" 2>/dev/null || stat -c %Y "$GIT_CACHE" 2>/dev/null)
if [ $((current_time - cache_time)) -lt 5 ]; then
BRANCH=$(cat "$GIT_CACHE")
fi
fi
# 快取過期或不存在,重新獲取
if [ -z "$BRANCH" ]; then
BRANCH_NAME=$(git branch --show-current 2>/dev/null)
if [ -n "$BRANCH_NAME" ]; then
BRANCH=" ⚡ $BRANCH_NAME"
fi
echo "$BRANCH" > "$GIT_CACHE"
fi
fi
# Session 追蹤目錄
TRACKER_DIR="$HOME/.claude/session-tracker"
SESSIONS_DIR="$TRACKER_DIR/sessions"
mkdir -p "$SESSIONS_DIR"
# 當前時間
CURRENT_TIME=$(date +%s)
TODAY=$(date +%Y-%m-%d)
# 優化的 session 更新函數
update_session() {
local session_file="$SESSIONS_DIR/$SESSION_ID.json"
if [ ! -f "$session_file" ]; then
# 新 session - 直接寫入
cat > "$session_file" <<EOF
{
"id": "$SESSION_ID",
"date": "$TODAY",
"start": $CURRENT_TIME,
"last_heartbeat": $CURRENT_TIME,
"total_seconds": 0,
"intervals": [{"start": $CURRENT_TIME, "end": null}]
}
EOF
else
# 使用單一 jq 調用更新現有 session
jq --argjson now "$CURRENT_TIME" '
. as $orig |
($now - .last_heartbeat) as $gap |
.last_heartbeat = $now |
if $gap < 600 then
.intervals[-1].end = $now
else
.intervals += [{"start": $now, "end": $now}]
end |
.total_seconds = ([.intervals[] | if .end != null then (.end - .start) else 0 end] | add // 0)
' "$session_file" > "$session_file.tmp" && mv "$session_file.tmp" "$session_file"
fi
}
# 計算所有 session 總時數(優化版)
calculate_total_hours() {
local total_seconds=0
local active_sessions=0
# 使用 find 批次處理檔案
while IFS= read -r -d '' session_file; do
# 使用單一 jq 調用獲取所需資訊
read -r session_date session_seconds last_heartbeat <<< $(jq -r '
.date // "",
(.total_seconds // 0),
(.last_heartbeat // 0)
' "$session_file" 2>/dev/null | tr '\\n' ' ')
# 只計算今日的 session
if [ "$session_date" = "$TODAY" ] && [ -n "$session_seconds" ]; then
total_seconds=$((total_seconds + session_seconds))
# 檢查是否活躍
if [ $((CURRENT_TIME - last_heartbeat)) -lt 600 ]; then
active_sessions=$((active_sessions + 1))
fi
fi
done < <(find "$SESSIONS_DIR" -name "*.json" -print0 2>/dev/null)
# 格式化輸出(使用 bash 內建運算)
local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
local time_str=""
if [ $hours -gt 0 ]; then
time_str="${hours}h"
[ $minutes -gt 0 ] && time_str="${time_str}${minutes}m"
else
time_str="${minutes}m"
fi
# 如果有多個活躍 session,顯示數量
[ $active_sessions -gt 1 ] && echo "$time_str [$active_sessions sessions]" || echo "$time_str"
}
# 歸檔舊 session(批次處理)
archive_old_sessions() {
find "$SESSIONS_DIR" -name "*.json" -exec sh -c '
for file; do
session_date=$(jq -r ".date // \\"\\"" "$file" 2>/dev/null)
if [ "$session_date" != "'"$TODAY"'" ] && [ -n "$session_date" ]; then
archive_dir="'"$TRACKER_DIR"'/archive/$session_date"
mkdir -p "$archive_dir"
mv "$file" "$archive_dir/"
fi
done
' sh {} +
}
# 優化的 context 使用量計算
calculate_context_usage() {
local transcript_path="$1"
[ ! -f "$transcript_path" ] && { echo "0"; return; }
# 使用單次 tail + awk 處理,避免臨時檔案
tail -100 "$transcript_path" 2>/dev/null | awk '
{
if (match($0, /"isSidechain":[[:space:]]*false/) &&
match($0, /"usage":[[:space:]]*\\{/)) {
# 提取 usage 資料
input_tokens = 0
cache_read = 0
cache_creation = 0
if (match($0, /"input_tokens":[[:space:]]*([0-9]+)/, arr))
input_tokens = arr[1]
if (match($0, /"cache_read_input_tokens":[[:space:]]*([0-9]+)/, arr))
cache_read = arr[1]
if (match($0, /"cache_creation_input_tokens":[[:space:]]*([0-9]+)/, arr))
cache_creation = arr[1]
context_length = input_tokens + cache_read + cache_creation
if (context_length > 0) {
print context_length
exit
}
}
}
END { if (NR == 0 || context_length == 0) print "0" }
'
}
# 優化的使用者訊息提取
extract_last_user_message() {
local transcript_path="$1"
local current_session_id="$2"
[ ! -f "$transcript_path" ] && return
# 使用 awk 一次處理,提升效能
tail -200 "$transcript_path" 2>/dev/null | tac | awk -v session_id="$current_session_id" '
/^$/ { next }
{
# 基本 JSON 格式檢查
if (!match($0, /^\\{.*\\}$/)) next
# 提取關鍵欄位
is_sidechain = match($0, /"isSidechain":[[:space:]]*true/)
session_match = match($0, /"sessionId":[[:space:]]*"'"'"'"$current_session_id"'"'"'"/)
is_user = match($0, /"role":[[:space:]]*"user"/) && match($0, /"type":[[:space:]]*"user"/)
if (!is_sidechain && session_match && is_user) {
# 提取訊息內容
if (match($0, /"content":[[:space:]]*"([^"]*)"/, arr)) {
content = arr[1]
# 過濾無效內容
if (match(content, /^[\\[\\{].*[\\]\\}]$/) ||
match(content, /<(local-command-stdout|command-name|command-message|command-args)>/) ||
match(content, /^Caveat:/) ||
content == "" || content == "null") {
next
}
# 清理並輸出
gsub(/^[[:space:]]+|[[:space:]]+$/, "", content)
if (length(content) > 0) {
print content
exit
}
}
}
}
'
}
# 格式化使用者訊息(優化版)
format_user_message() {
local message="$1"
[ -z "$message" ] && return
# 使用 awk 進行格式化
echo "$message" | awk '
BEGIN { max_lines = 3; line_width = 80; line_count = 0 }
line_count < max_lines {
line_count++
if (length($0) > line_width) {
$0 = substr($0, 1, 77) "..."
}
print $0
}
END {
if (NR > max_lines) {
print "... (還有 " (NR - max_lines) " 行)"
}
}
'
}
# 數字格式化函數(優化版)
format_number() {
local num="$1"
[ -z "$num" ] || [ "$num" = "0" ] && { echo "--"; return; }
# 使用 bash 內建運算
if [ "$num" -ge 1000000 ]; then
echo "$((num / 1000000))M"
elif [ "$num" -ge 1000 ]; then
echo "$((num / 1000))k"
else
echo "$num"
fi
}
# 進度條生成(優化版)
generate_progress_bar() {
local percentage="$1"
local width=10
# 使用 bash 內建運算
local filled=$(( percentage * width / 100 ))
[ "$filled" -lt 0 ] && filled=0
[ "$filled" -gt "$width" ] && filled=$width
local empty=$((width - filled))
# 獲取顏色
local bar_color=$(get_context_color "$percentage")
local gray_color="\\033[38;2;64;64;64m"
# 生成進度條
local bar=""
# 填充部分
if [ $filled -gt 0 ]; then
bar="${bar}${bar_color}"
for ((i=0; i<filled; i++)); do
bar="${bar}█"
done
bar="${bar}${COLOR_RESET}"
fi
# 未填充部分
if [ $empty -gt 0 ]; then
bar="${bar}${gray_color}"
for ((i=0; i<empty; i++)); do
bar="${bar}░"
done
bar="${bar}${COLOR_RESET}"
fi
echo "$bar"
}
# Context 顏色設定(優化版)
get_context_color() {
local percentage="$1"
# 處理空值
[ -z "$percentage" ] && { echo "\\033[38;2;192;192;192m"; return; }
# 使用 bash 內建比較
if [ "$percentage" -lt 60 ]; then
echo "\\033[38;2;108;167;108m" # 綠色
elif [ "$percentage" -lt 80 ]; then
echo "\\033[38;2;188;155;83m" # 金色
else
echo "\\033[38;2;185;102;82m" # 紅色
fi
}
# 執行主要邏輯
update_session
archive_old_sessions
TOTAL_HOURS=$(calculate_total_hours)
# Context 使用量計算
CONTEXT_USAGE=""
USER_MESSAGE_DISPLAY=""
if [ -n "$TRANSCRIPT_PATH" ] && [ "$TRANSCRIPT_PATH" != "null" ] && [ "$TRANSCRIPT_PATH" != "" ]; then
CONTEXT_LENGTH=$(calculate_context_usage "$TRANSCRIPT_PATH")
if [ -n "$CONTEXT_LENGTH" ] && [ "$CONTEXT_LENGTH" != "0" ]; then
# 使用 bash 內建運算計算百分比
CONTEXT_PERCENTAGE=$((CONTEXT_LENGTH * 100 / 200000))
# 限制百分比最大為 100%
[ "$CONTEXT_PERCENTAGE" -gt 100 ] && CONTEXT_PERCENTAGE=100
# 生成顯示元件
PROGRESS_BAR=$(generate_progress_bar "$CONTEXT_PERCENTAGE")
FORMATTED_NUM=$(format_number "$CONTEXT_LENGTH")
CONTEXT_COLOR=$(get_context_color "$CONTEXT_PERCENTAGE")
CONTEXT_USAGE=" | ${PROGRESS_BAR} ${CONTEXT_COLOR}${CONTEXT_PERCENTAGE}% ${FORMATTED_NUM}${COLOR_RESET}"
fi
# 提取並格式化使用者訊息
LAST_USER_MESSAGE=$(extract_last_user_message "$TRANSCRIPT_PATH" "$SESSION_ID")
if [ -n "$LAST_USER_MESSAGE" ]; then
FORMATTED_USER_MESSAGE=$(format_user_message "$LAST_USER_MESSAGE")
if [ -n "$FORMATTED_USER_MESSAGE" ]; then
USER_MESSAGE_DISPLAY=$(echo "$FORMATTED_USER_MESSAGE" | while IFS= read -r line; do
echo "${COLOR_RESET}|${MESSAGE_COLOR}${line}${COLOR_RESET}"
done)
fi
fi
fi
# 輸出狀態列
echo -e "${COLOR_RESET}[${MODEL_COLOR}${MODEL_ICON} ${MODEL}${COLOR_RESET}] 📂 $PROJECT_NAME$BRANCH$CONTEXT_USAGE | $TOTAL_HOURS"
# 輸出使用者訊息
[ -n "$USER_MESSAGE_DISPLAY" ] && echo -e "$USER_MESSAGE_DISPLAY"
claude-stats.sh 統計時數腳本
#!/bin/bash
# Claude 時間統計工具
# 功能:統計和管理 Claude Code session 使用時間
#
# 用法:
# claude-stats.sh # 今日統計
# claude-stats.sh 2025-08-08 # 指定日期統計
# claude-stats.sh week # 本週統計
# claude-stats.sh month # 本月統計
# claude-stats.sh all # 所有歷史統計
# claude-stats.sh archive # 歸檔舊 session 檔案
#
# 說明:
# - 統計功能會同時檢查 sessions 和 archive 目錄
# - 歸檔功能會將非今日的 session 檔案移動到按日期分類的 archive 目錄
# - 支援中文星期顯示和時間格式化
TRACKER_DIR="$HOME/.claude/session-tracker"
SESSIONS_DIR="$TRACKER_DIR/sessions"
ARCHIVE_DIR="$TRACKER_DIR/archive"
# 將英文星期轉換為中文
get_chinese_weekday() {
local date_str=$1
local weekday
# 根據平台使用不同的 date 命令
if date --version >/dev/null 2>&1; then
# GNU date (Linux)
weekday=$(date -d "$date_str" +%a 2>/dev/null)
else
# BSD date (macOS)
weekday=$(date -j -f "%Y-%m-%d" "$date_str" +%a 2>/dev/null)
fi
case "$weekday" in
Mon) echo "(一)" ;;
Tue) echo "(二)" ;;
Wed) echo "(三)" ;;
Thu) echo "(四)" ;;
Fri) echo "(五)" ;;
Sat) echo "(六)" ;;
Sun) echo "(日)" ;;
*) echo "" ;;
esac
}
# 計算指定日期的總時數
calculate_date_total() {
local date=$1
local total_seconds=0
local session_count=0
# 檢查活躍目錄(移除日期限制)
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local session_date=$(jq -r '.date' "$session_file" 2>/dev/null)
if [ "$session_date" = "$date" ]; then
local session_seconds=$(jq '.total_seconds // 0' "$session_file")
total_seconds=$((total_seconds + session_seconds))
session_count=$((session_count + 1))
fi
fi
done
# 檢查歸檔目錄
if [ -d "$ARCHIVE_DIR/$date" ]; then
for session_file in "$ARCHIVE_DIR/$date"/*.json; do
if [ -f "$session_file" ]; then
local session_seconds=$(jq '.total_seconds // 0' "$session_file")
total_seconds=$((total_seconds + session_seconds))
session_count=$((session_count + 1))
fi
done
fi
# 格式化輸出
local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
# 取得星期幾
local weekday=$(get_chinese_weekday "$date")
if [ $total_seconds -eq 0 ]; then
echo "$date $weekday: 無記錄"
else
printf "%s %s: %dh %dm (%d sessions)\\n" "$date" "$weekday" "$hours" "$minutes" "$session_count"
fi
}
# 計算日期範圍統計
calculate_range_total() {
local start_date=$1
local end_date=$2
local grand_total=0
local total_sessions=0
echo "統計範圍: $start_date 至 $end_date"
echo "----------------------------------------"
# 遍歷日期範圍
current_date="$start_date"
while [ "$current_date" != "$end_date" ] || [ "$current_date" = "$end_date" ]; do
# 檢查是否已超過結束日期
if [[ "$current_date" > "$end_date" ]]; then
break
fi
local total_seconds=0
local session_count=0
# 檢查活躍目錄(移除日期限制)
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local session_date=$(jq -r '.date' "$session_file" 2>/dev/null)
if [ "$session_date" = "$current_date" ]; then
local session_seconds=$(jq '.total_seconds // 0' "$session_file")
total_seconds=$((total_seconds + session_seconds))
session_count=$((session_count + 1))
fi
fi
done
# 檢查歸檔目錄
if [ -d "$ARCHIVE_DIR/$current_date" ]; then
for session_file in "$ARCHIVE_DIR/$current_date"/*.json; do
if [ -f "$session_file" ]; then
local session_seconds=$(jq '.total_seconds // 0' "$session_file")
total_seconds=$((total_seconds + session_seconds))
session_count=$((session_count + 1))
fi
done
fi
# 只顯示有記錄的日期
if [ $total_seconds -gt 0 ]; then
local hours=$((total_seconds / 3600))
local minutes=$(((total_seconds % 3600) / 60))
local weekday=$(get_chinese_weekday "$current_date")
printf " %s %s: %2dh %2dm (%d sessions)\\n" "$current_date" "$weekday" "$hours" "$minutes" "$session_count"
grand_total=$((grand_total + total_seconds))
total_sessions=$((total_sessions + session_count))
fi
# 如果已經是結束日期,跳出循環
if [ "$current_date" = "$end_date" ]; then
break
fi
# 下一天
if date --version >/dev/null 2>&1; then
# GNU date
current_date=$(date -d "$current_date + 1 day" +%Y-%m-%d)
else
# BSD date (macOS)
current_date=$(date -j -v+1d -f "%Y-%m-%d" "$current_date" +%Y-%m-%d)
fi
done
# 總計
echo "----------------------------------------"
local total_hours=$((grand_total / 3600))
local total_minutes=$(((grand_total % 3600) / 60))
printf "總計: %dh %dm (%d sessions)\\n" "$total_hours" "$total_minutes" "$total_sessions"
}
# 歸檔舊 session 檔案
archive_old_sessions() {
local today=$(date +%Y-%m-%d)
local moved_count=0
local date_count=0
echo "開始歸檔舊 session 檔案..."
# 收集需要歸檔的日期
local dates_to_archive=()
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local session_date=$(jq -r '.date' "$session_file" 2>/dev/null)
if [ -n "$session_date" ] && [ "$session_date" != "$today" ]; then
# 檢查是否已在陣列中
local found=0
for existing_date in "${dates_to_archive[@]}"; do
if [ "$existing_date" = "$session_date" ]; then
found=1
break
fi
done
if [ $found -eq 0 ]; then
dates_to_archive+=("$session_date")
fi
fi
fi
done
# 歸檔每個日期的檔案
for date in "${dates_to_archive[@]}"; do
local archive_path="$ARCHIVE_DIR/$date"
mkdir -p "$archive_path"
date_count=$((date_count + 1))
# 移動該日期的所有檔案
local files_moved=0
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
local session_date=$(jq -r '.date' "$session_file" 2>/dev/null)
if [ "$session_date" = "$date" ]; then
mv "$session_file" "$archive_path/"
moved_count=$((moved_count + 1))
files_moved=$((files_moved + 1))
fi
fi
done
echo " 已歸檔 $date 的 $files_moved 個檔案"
done
if [ $moved_count -eq 0 ]; then
echo "沒有需要歸檔的檔案"
else
echo "歸檔完成:移動了 $moved_count 個檔案到 $date_count 個日期目錄"
fi
}
# 主程式
case "${1:-today}" in
today)
echo "=== 今日統計 ==="
calculate_date_total "$(date +%Y-%m-%d)"
;;
week)
echo "=== 本週統計 ==="
if date --version >/dev/null 2>&1; then
# GNU date
start_date=$(date -d "last monday" +%Y-%m-%d)
else
# BSD date (macOS)
start_date=$(date -v-monday +%Y-%m-%d)
fi
end_date=$(date +%Y-%m-%d)
calculate_range_total "$start_date" "$end_date"
;;
month)
echo "=== 本月統計 ==="
start_date=$(date +%Y-%m-01)
end_date=$(date +%Y-%m-%d)
calculate_range_total "$start_date" "$end_date"
;;
all)
echo "=== 所有歷史統計 ==="
# 找出最早的日期
earliest_date=""
# 檢查歸檔目錄
if [ -d "$ARCHIVE_DIR" ]; then
for dir in "$ARCHIVE_DIR"/*/; do
if [ -d "$dir" ]; then
date_dir=$(basename "$dir")
if [ -z "$earliest_date" ] || [[ "$date_dir" < "$earliest_date" ]]; then
earliest_date="$date_dir"
fi
fi
done
fi
# 檢查當前 sessions
for session_file in "$SESSIONS_DIR"/*.json; do
if [ -f "$session_file" ]; then
session_date=$(jq -r '.date' "$session_file" 2>/dev/null)
if [ -n "$session_date" ]; then
if [ -z "$earliest_date" ] || [[ "$session_date" < "$earliest_date" ]]; then
earliest_date="$session_date"
fi
fi
fi
done
if [ -n "$earliest_date" ]; then
calculate_range_total "$earliest_date" "$(date +%Y-%m-%d)"
else
echo "沒有找到任何記錄"
fi
;;
archive)
echo "=== 歸檔舊 Session ==="
archive_old_sessions
;;
20[0-9][0-9]-[0-1][0-9]-[0-3][0-9])
# 指定日期格式
echo "=== 指定日期統計 ==="
calculate_date_total "$1"
;;
*)
echo "用法: $0 [today|week|month|all|archive|YYYY-MM-DD]"
exit 1
;;
esac
statusline.go
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
// ANSI 顏色定義
const (
ColorReset = "\\033[0m"
ColorGold = "\\033[38;2;195;158;83m"
ColorCyan = "\\033[38;2;118;170;185m"
ColorPink = "\\033[38;2;255;182;193m"
ColorGreen = "\\033[38;2;152;195;121m"
ColorGray = "\\033[38;2;64;64;64m"
ColorSilver = "\\033[38;2;192;192;192m"
ColorCtxGreen = "\\033[38;2;108;167;108m"
ColorCtxGold = "\\033[38;2;188;155;83m"
ColorCtxRed = "\\033[38;2;185;102;82m"
)
// 模型圖示和顏色
var modelConfig = map[string][2]string{
"Opus": {ColorGold, "💛"},
"Sonnet": {ColorCyan, "💠"},
"Haiku": {ColorPink, "🌸"},
}
// 輸入資料結構
type Input struct {
Model struct {
DisplayName string `json:"display_name"`
} `json:"model"`
SessionID string `json:"session_id"`
Workspace struct {
CurrentDir string `json:"current_dir"`
} `json:"workspace"`
TranscriptPath string `json:"transcript_path,omitempty"`
}
// Session 資料結構
type Session struct {
ID string `json:"id"`
Date string `json:"date"`
Start int64 `json:"start"`
LastHeartbeat int64 `json:"last_heartbeat"`
TotalSeconds int64 `json:"total_seconds"`
Intervals []Interval `json:"intervals"`
}
type Interval struct {
Start int64 `json:"start"`
End *int64 `json:"end"`
}
// 結果通道資料
type Result struct {
Type string
Data interface{}
}
// 簡單快取
var (
gitBranchCache string
gitBranchExpires time.Time
cacheMutex sync.RWMutex
)
func main() {
var input Input
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
fmt.Fprintf(os.Stderr, "Failed to decode input: %v\\n", err)
os.Exit(1)
}
// 建立結果通道
results := make(chan Result, 4)
var wg sync.WaitGroup
// 並行獲取各種資訊
wg.Add(4)
go func() {
defer wg.Done()
branch := getGitBranch()
results <- Result{"git", branch}
}()
go func() {
defer wg.Done()
totalHours := calculateTotalHours(input.SessionID)
results <- Result{"hours", totalHours}
}()
go func() {
defer wg.Done()
contextInfo := analyzeContext(input.TranscriptPath)
results <- Result{"context", contextInfo}
}()
go func() {
defer wg.Done()
userMsg := extractUserMessage(input.TranscriptPath, input.SessionID)
results <- Result{"message", userMsg}
}()
// 等待所有 goroutines 完成
go func() {
wg.Wait()
close(results)
}()
// 收集結果
var gitBranch, totalHours, contextUsage, userMessage string
for result := range results {
switch result.Type {
case "git":
gitBranch = result.Data.(string)
case "hours":
totalHours = result.Data.(string)
case "context":
contextUsage = result.Data.(string)
case "message":
userMessage = result.Data.(string)
}
}
// 更新 session(同步操作,避免競爭條件)
updateSession(input.SessionID)
// 格式化模型顯示
modelDisplay := formatModel(input.Model.DisplayName)
projectName := filepath.Base(input.Workspace.CurrentDir)
// 輸出狀態列
fmt.Printf("%s[%s] 📂 %s%s%s | %s%s\\n",
ColorReset, modelDisplay, projectName, gitBranch,
contextUsage, totalHours, ColorReset)
// 輸出使用者訊息
if userMessage != "" {
fmt.Print(userMessage)
}
}
// 格式化模型顯示
func formatModel(model string) string {
for key, config := range modelConfig {
if strings.Contains(model, key) {
color := config[0]
icon := config[1]
return fmt.Sprintf("%s%s %s%s", color, icon, model, ColorReset)
}
}
return model
}
// 獲取 Git 分支(帶快取)
func getGitBranch() string {
cacheMutex.RLock()
if time.Now().Before(gitBranchExpires) && gitBranchCache != "" {
result := gitBranchCache
cacheMutex.RUnlock()
return result
}
cacheMutex.RUnlock()
// 檢查是否為 Git 倉庫
if _, err := os.Stat(".git"); os.IsNotExist(err) {
// 嘗試找到 Git 根目錄
cmd := exec.Command("git", "rev-parse", "--git-dir")
if err := cmd.Run(); err != nil {
return ""
}
}
// 獲取當前分支
cmd := exec.Command("git", "branch", "--show-current")
output, err := cmd.Output()
if err != nil {
return ""
}
branch := strings.TrimSpace(string(output))
if branch == "" {
return ""
}
result := fmt.Sprintf(" ⚡ %s", branch)
// 更新快取
cacheMutex.Lock()
gitBranchCache = result
gitBranchExpires = time.Now().Add(5 * time.Second)
cacheMutex.Unlock()
return result
}
// 更新 Session
func updateSession(sessionID string) {
homeDir, err := os.UserHomeDir()
if err != nil {
return
}
sessionsDir := filepath.Join(homeDir, ".claude", "session-tracker", "sessions")
if err := os.MkdirAll(sessionsDir, 0755); err != nil {
return
}
sessionFile := filepath.Join(sessionsDir, sessionID+".json")
currentTime := time.Now().Unix()
today := time.Now().Format("2006-01-02")
var session Session
// 讀取現有 session
if data, err := os.ReadFile(sessionFile); err == nil {
json.Unmarshal(data, &session)
} else {
// 新 session
session = Session{
ID: sessionID,
Date: today,
Start: currentTime,
LastHeartbeat: currentTime,
TotalSeconds: 0,
Intervals: []Interval{{Start: currentTime, End: nil}},
}
}
// 更新心跳
gap := currentTime - session.LastHeartbeat
session.LastHeartbeat = currentTime
if gap < 600 { // 10分鐘內為連續
// 延伸當前區間
if len(session.Intervals) > 0 {
session.Intervals[len(session.Intervals)-1].End = ¤tTime
}
} else {
// 新增新區間
session.Intervals = append(session.Intervals, Interval{
Start: currentTime,
End: ¤tTime,
})
}
// 計算總時數
var total int64
for _, interval := range session.Intervals {
if interval.End != nil {
total += *interval.End - interval.Start
}
}
session.TotalSeconds = total
// 儲存
if data, err := json.Marshal(session); err == nil {
os.WriteFile(sessionFile, data, 0644)
}
}
// 計算總時數
func calculateTotalHours(currentSessionID string) string {
homeDir, err := os.UserHomeDir()
if err != nil {
return "0m"
}
sessionsDir := filepath.Join(homeDir, ".claude", "session-tracker", "sessions")
entries, err := os.ReadDir(sessionsDir)
if err != nil {
return "0m"
}
var totalSeconds int64
activeSessions := 0
today := time.Now().Format("2006-01-02")
currentTime := time.Now().Unix()
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
sessionFile := filepath.Join(sessionsDir, entry.Name())
data, err := os.ReadFile(sessionFile)
if err != nil {
continue
}
var session Session
if err := json.Unmarshal(data, &session); err != nil {
continue
}
// 只計算今日的 session
if session.Date == today {
totalSeconds += session.TotalSeconds
// 檢查是否活躍(10分鐘內有心跳)
if currentTime-session.LastHeartbeat < 600 {
activeSessions++
}
}
}
// 格式化輸出
hours := totalSeconds / 3600
minutes := (totalSeconds % 3600) / 60
var timeStr string
if hours > 0 {
timeStr = fmt.Sprintf("%dh", hours)
if minutes > 0 {
timeStr += fmt.Sprintf("%dm", minutes)
}
} else {
timeStr = fmt.Sprintf("%dm", minutes)
}
if activeSessions > 1 {
return fmt.Sprintf("%s [%d sessions]", timeStr, activeSessions)
}
return timeStr
}
// 分析 Context 使用量
func analyzeContext(transcriptPath string) string {
var contextLength int
if transcriptPath == "" {
// 當 transcriptPath 為空時(對話剛開始),顯示初始狀態
contextLength = 0
} else {
contextLength = calculateContextUsage(transcriptPath)
}
// 即使 contextLength 為 0 也顯示進度條
// 計算百分比(基於 200k tokens)
percentage := int(float64(contextLength) * 100.0 / 200000.0)
if percentage > 100 {
percentage = 100
}
// 生成進度條
progressBar := generateProgressBar(percentage)
formattedNum := formatNumber(contextLength)
color := getContextColor(percentage)
return fmt.Sprintf(" | %s %s%d%% %s%s",
progressBar, color, percentage, formattedNum, ColorReset)
}
// 計算 Context 使用量
func calculateContextUsage(transcriptPath string) int {
file, err := os.Open(transcriptPath)
if err != nil {
return 0
}
defer file.Close()
// 讀取最後100行
lines := make([]string, 0, 100)
scanner := bufio.NewScanner(file)
// 設定更大的 buffer(1MB)以處理長 JSON 行
const maxScanTokenSize = 1024 * 1024 // 1MB
buf := make([]byte, 0, maxScanTokenSize)
scanner.Buffer(buf, maxScanTokenSize)
// 先讀取所有行到切片
allLines := make([]string, 0)
for scanner.Scan() {
allLines = append(allLines, scanner.Text())
}
// 取最後100行
start := len(allLines) - 100
if start < 0 {
start = 0
}
lines = allLines[start:]
// 從後往前分析
for i := len(lines) - 1; i >= 0; i-- {
line := lines[i]
// 空行跳過
if strings.TrimSpace(line) == "" {
continue
}
// 先嘗試解析 JSON
var data map[string]interface{}
if err := json.Unmarshal([]byte(line), &data); err != nil {
continue
}
// 檢查 isSidechain 欄位(處理 bool 和可能的其他類型)
if sidechain, ok := data["isSidechain"]; ok {
// 如果是 sidechain,跳過
if isSide, ok := sidechain.(bool); ok && isSide {
continue
}
}
// 檢查並提取 usage 資料
if message, ok := data["message"].(map[string]interface{}); ok {
if usage, ok := message["usage"].(map[string]interface{}); ok {
var total float64
// 計算所有 token 類型
if input, ok := usage["input_tokens"].(float64); ok {
total += input
}
if cacheRead, ok := usage["cache_read_input_tokens"].(float64); ok {
total += cacheRead
}
if cacheCreation, ok := usage["cache_creation_input_tokens"].(float64); ok {
total += cacheCreation
}
// 如果找到有效的 token 數量,立即返回
if total > 0 {
return int(total)
}
}
}
}
return 0
}
// 生成進度條
func generateProgressBar(percentage int) string {
width := 10
filled := percentage * width / 100
if filled > width {
filled = width
}
empty := width - filled
color := getContextColor(percentage)
var bar strings.Builder
// 填充部分
if filled > 0 {
bar.WriteString(color)
bar.WriteString(strings.Repeat("█", filled))
bar.WriteString(ColorReset)
}
// 空白部分
if empty > 0 {
bar.WriteString(ColorGray)
bar.WriteString(strings.Repeat("░", empty))
bar.WriteString(ColorReset)
}
return bar.String()
}
// 獲取 Context 顏色
func getContextColor(percentage int) string {
if percentage < 60 {
return ColorCtxGreen
} else if percentage < 80 {
return ColorCtxGold
}
return ColorCtxRed
}
// 格式化數字
func formatNumber(num int) string {
if num == 0 {
return "--"
}
if num >= 1000000 {
return fmt.Sprintf("%dM", num/1000000)
} else if num >= 1000 {
return fmt.Sprintf("%dk", num/1000)
}
return strconv.Itoa(num)
}
// 提取使用者訊息
func extractUserMessage(transcriptPath, sessionID string) string {
if transcriptPath == "" {
return ""
}
file, err := os.Open(transcriptPath)
if err != nil {
return ""
}
defer file.Close()
// 讀取最後200行
lines := make([]string, 0, 200)
scanner := bufio.NewScanner(file)
allLines := make([]string, 0)
for scanner.Scan() {
allLines = append(allLines, scanner.Text())
}
start := len(allLines) - 200
if start < 0 {
start = 0
}
lines = allLines[start:]
// 從後往前搜尋使用者訊息
for i := len(lines) - 1; i >= 0; i-- {
line := lines[i]
if strings.TrimSpace(line) == "" {
continue
}
var data map[string]interface{}
if err := json.Unmarshal([]byte(line), &data); err != nil {
continue
}
// 檢查是否為當前 session 的使用者訊息
isSidechain, _ := data["isSidechain"].(bool)
sessionMatch := false
if sid, ok := data["sessionId"].(string); ok && sid == sessionID {
sessionMatch = true
}
if !isSidechain && sessionMatch {
if message, ok := data["message"].(map[string]interface{}); ok {
role, _ := message["role"].(string)
msgType, _ := data["type"].(string)
if role == "user" && msgType == "user" {
if content, ok := message["content"].(string); ok {
// 過濾系統訊息
if isSystemMessage(content) {
continue
}
// 格式化並返回
return formatUserMessage(content)
}
}
}
}
}
return ""
}
// 檢查是否為系統訊息
func isSystemMessage(content string) bool {
// 過濾 JSON 格式
if strings.HasPrefix(content, "[") && strings.HasSuffix(content, "]") {
return true
}
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
return true
}
// 過濾 XML 標籤
xmlTags := []string{
"<local-command-stdout>", "<command-name>",
"<command-message>", "<command-args>",
}
for _, tag := range xmlTags {
if strings.Contains(content, tag) {
return true
}
}
// 過濾 Caveat 訊息
if strings.HasPrefix(content, "Caveat:") {
return true
}
return false
}
// 格式化使用者訊息
func formatUserMessage(message string) string {
if message == "" {
return ""
}
maxLines := 3
lineWidth := 80
lines := strings.Split(message, "\\n")
var result []string
for i, line := range lines {
if i >= maxLines {
break
}
line = strings.TrimSpace(line)
if len(line) > lineWidth {
line = line[:lineWidth-3] + "..."
}
result = append(result, fmt.Sprintf("%s|%s%s%s",
ColorReset, ColorGreen, line, ColorReset))
}
if len(lines) > maxLines {
result = append(result, fmt.Sprintf("%s|... (還有 %d 行)%s",
ColorReset, len(lines)-maxLines, ColorReset))
}
if len(result) > 0 {
return strings.Join(result, "\\n") + "\\n"
}
return ""
}
settings.json
"statusLine": {
"type": "command",
"command": "~/.claude/statusline-go",
"padding": 0
},
備註:以上程式碼都是 Claude Code 生成的
我本人根本寫不出腳本和 GO 語言
因此如果你使用上有問題
我建議你問 Claude Code 修正比較好 XD
設定教學
直接給 Claude Code 上面腳本,附上官方文件連結
跟他說我也要,他就會幫你設定了
但是第一個腳本指令執行會有點慢
Claude Code 印象好像是 600 毫秒刷新一次
這個腳本明顯跟不上
你選 opus-plan-mode 就可以明顯感覺到
你切換 plan mode 時,狀態列的模型切換有點 lag
我讓 Claude Code 用 GO 改寫這個腳本
再設定執行 GO 執行檔,刷新反應變超快
第二段 GO 版本的就是了
你可以拿去讓 AI 幫你處理
錢很重要,上下文更重要
如果你搜尋 Claude Code Status Line
你會查到很多大家分享的狀態列設定和做法
很多都會把 ccuseage 的狀態列整合工具加進來
搞的狀態列會有一堆錢 $ 的符號 XD

這些費用資訊對開發來說是雜訊
大腦的上下文也是需要保護滴
而且不管是哪個訂閱方案(pro, max 5x, 20x)
你都只會 care 是不是要碰到限流
除非你費用計算方式是算 API 費用的
如果是這樣的話,那這樣設定就很合理 XD
我知道就是會忍不住想知道今天回本沒
好想知道已經花了多少 api 費用
小秘訣:在等 AI 生成的時候有的是時間去看
我自己是把 ccuseage 監視畫面開在另一個 iPad 螢幕上
iPad 再次又有了價值,不然都放在旁邊生灰

搭配語音通知
還記得上一篇的《Claude Code 語音通知:成為 AI 奴隸主的第一步》嗎
- 語音通知:用聽的知道狀況
- Status Line:用看的知道狀況
這樣我們就已經有了良好的腦機協作基礎配置
下一篇我會分享我的 Claude Code 自訂咒語
/command
讓你使用燒 token 禁術
招喚大量 Sub Agent 來完成批次任務