Claude Code Status Line:專業奴隸主,要有一套自己的監控系統

Claude Code Status Line:專業奴隸主,要有一套自己的監控系統

文章目錄(9 節)

先說結論

  • Status Line 是 Claude Code 8/8 推出的新功能(官方文件連結
    可以讓你自訂資訊在指令輸入框下面
  • 不推薦用 ccuseage 的狀態列整合,搞的狀態列會有一堆錢 $ 的符號 XD
  • 如果只能挑一個資訊顯示,我會選目前 context 使用量
  • 我會提供我的完整 Status Line 的程式碼,你可以拿去再調整

先來看我的 Status Line 吧

我的 Claude Code 狀態列,標出模型、專案、Git 分支、Context 使用量、今天共使用時數五個欄位
我的狀態列長這樣:一行塞了五種我真的會看的資訊

應該馬上就懂了

模型

這個視窗現在跑的是哪一顆,圖裡是 Sonnet 4

專案

目前所在的專案資料夾,圖裡是 Fortuna

Git 分支

現在站在哪一條分支上,圖裡是 main

Context 使用量

進度條加百分比,這篇的主角

今天共使用時數

圖裡是 3h9m [4 sessions]

雖然狀態列這個功能,對於提升 AI 能力在寫程式上沒有什麼幫助,一度讓我以為 Anthropic 是不是不知道要做啥功能了,我要這個狀態列幹嘛?

用了才知道…原來

傻瓜我們都一樣,被「AI 情」傷了又傷。

肯定是很多前輩深受 AI 裝傻所苦。Claude Code 8月多了的新功能,像是 Status Line、/context 指令,都可以幫助我們做 Context 分析,可以滿足我無處安放的控制欲。

欲練神功,必先多工

像我們這種訂閱 Max 20x 方案的盤子,肯定對於 token 的追求更加要求效率。不可能下完一個指令呆呆地看著他跑,肯定是又開一個新視窗,繼續把下一個任務丟出去。因此隨時有三四個 Claude Code 在跑,是很正常的。

  1. 任務指北針

    我自己會在桌上放一張白紙,隨時記下我要做的最大的 todo,有點像是任務指北針。最重要的事就是確保自己在做最重要的事。Todo任務清單已經實證對 AI 有效,對人類也一樣有效 XD

  2. 啟動多工模式

    再來就是啟動多工模式,把工作都 assign 出去給 AI 處理。

  3. 等回報,回來驗收

    然後等語音通知回報任務結果,再回來驗收,然後繼續下一個循環。

這裡我們來討論大腦的 context switch

沒有狀態列:往上滑找自己的指令

如果同時開三個以上的 Claude Code,只能說叔叔有練過,小朋友不要學 XD

我們的大腦的工作記憶大概就是 5 格,處理完第三個視窗後,我已經忘記第一個在幹嘛了,然後就會往上滑找我的指令,順便回憶。

但偏偏 Claude Code 裡面使用者指令的顏色是灰色的!跟 AI 在思考顯示的思路文字一樣 XD,真的有夠難找,補個幹。

有狀態列:第二行就是我的上一個指令

因此我在 Status Line 第二行增加一個功能,顯示我最後的指令,最多三行,並且用舒服的大地綠色保護眼睛。

狀態列第二行用綠色顯示我的上一個指令
第二行綠色的就是我的上一個指令,最多顯示三行

不過我承認有時候我連自己上一個指令在寫啥都看不懂

備註

這裡我還會搭配 AI 摘要任務進度,讓我可以從終端機狀態欄看到現在在做的任務,搭配上一個指令,就沒有大腦切換當機問題。但這不是狀態列功能,要搭配使用,之後我會寫一篇介紹。

保護你的 Context

再來是最重要的環節。如果我們都已經壓榨我們的精神力,同時多工好幾個視窗,結果只是加速產生了更多大便,那可不是浪費電這樣的小事了。因此 AI 的輸出品質絕對值得讓我們花精力去好好的優化。

會影響 AI 輸出的三本柱:

1.模型

現在好的模型就是貴

2.Prompt

不說人話,表現就是爛

3.Context

最近一直在講的 Context Engineering

(打 Context 很煩,接下來我用“上下文”表示 Context)

因為 Claude Code 會把每個對話紀錄存在本機,所以我們可以寫一個程式去查目前上下文使用量,然後顯示在狀態列上。

我設定好後第一個發現就是,你能用的上下文其實是 160K

Opus4.1 和 Sonnet4 都有 200k 的上下文視窗,但如果你有開自動壓縮的話,你能用的上下文其實是 160K,大概是 80% 左右的上下文,他就會開始自動壓縮。

200K 上下文視窗的三段預算 Opus 4.1 與 Sonnet 4 的上下文視窗是 200K。零到 50%(0 到 100K)是我盡量待著的安全區;50% 到 80%(100K 到 160K)是警戒區,會考慮手動壓縮或結束對話;80% 到 100%(160K 到 200K)保留給自動壓縮,所以實際能用的只有 160K。 0 0% 100K 50% 160K 80% 200K 100% 安全區 我現在盡量不超過 50% 警戒區 手動壓縮或收工 保留區 自動壓縮 實際能用的上下文只有 160K 20% 留給壓縮

左右滑動看完整內容

200K 視窗切成三段:安全區、警戒區,還有留給自動壓縮的 20%
狀態列顯示 70% 141k,右側寫著 Context left until auto-compact: 9%
用到 70%(141k)時,右邊已經在倒數「Context left until auto-compact: 9%」,可以看出 80% 時會啟動自動壓縮

那 20% 的空間應該是要保留用來壓縮上下文用的。如果你把自動壓縮功能關掉,然後把它用滿,你會發現你不能再做任何事,你只能回退讓出空間,或是重啟新的對話。

再來是很多研究都表明 “長上下文” 會影響模型輸出的品質。簡單說就是你上下文越長,表現就會越不好,你就會越森77。

狀態列就是一個可以用來監控上下文用量的好所在。

我現在盡量保持上下文不超過 50%,超過之後我就會開始思考要不要手動壓縮(我有一個我自訂的壓縮指令),或是結束對話,重新安排。

有時候就是沒辦法,用到他自動壓縮,我覺得也是人之常情。如果結果沒有問題那就沒有問題;如果有問題,那可能就是這個問題(好像廢話)。

總之

上下文是稀缺資源,需要控管。

你今天對得起自己嗎?

最後簡單說明狀態列最旁邊的 “今天共使用時數”,簡單說他會記錄每個 session 運行時間。

開一個視窗

假設有一個 Claude Code在執行,跑了一小時,就會記錄一小時

同時開兩個視窗

同時有兩個執行一小時,就會記錄兩小時

每天0點重置

隔天從零開始算,看到的一定是今天的量

因此這可以表示你今天的工作強度。如果睡前發現沒有 10 小時,表示我太廢了 XDD

因為有記錄,所以可以跑統計時數。

統計腳本輸出的所有歷史統計,逐日列出使用時數與 session 數
統計範圍 2025-08-09 至 2025-09-11,總計 496h 34m(1111 sessions)

嗯...我是不是沒朋友...
沒關係,泰呈~
你還有 Claude Code啊~

我的 Status Line 設定(抄作業時間)

四個檔案,展開就能整份複製走。前兩個是 shell 腳本,第三個是嫌 shell 太慢之後改寫的 GO 版本,最後一個是 Claude Code 的設定檔。

statusline.sh 狀態列本體,shell 版
#!/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 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 = &currentTime
		}
	} else {
		// 新增新區間
		session.Intervals = append(session.Intervals, Interval{
			Start: currentTime,
			End:   &currentTime,
		})
	}

	// 計算總時數
	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 把狀態列接上 Claude Code 的設定檔
"statusLine": {
    "type": "command",
    "command": "~/.claude/statusline-go",
    "padding": 0
},

備註

以上程式碼都是 Claude Code 生成的,我本人根本寫不出腳本和 GO 語言。因此如果你使用上有問題,我建議你問 Claude Code 修正比較好 XD

設定教學

直接給 Claude Code 上面腳本,附上官方文件連結,跟他說我也要,他就會幫你設定了。

第一版 shell 腳本:跟不上刷新

但是第一個腳本指令執行會有點慢。Claude Code 印象好像是 600 毫秒刷新一次,這個腳本明顯跟不上。

你選 opus-plan-mode 就可以明顯感覺到,你切換 plan mode 時,狀態列的模型切換有點 lag。

第二版 GO 執行檔:刷新變超快

我讓 Claude Code 用 GO 改寫這個腳本,再設定執行 GO 執行檔,刷新反應變超快。

第二段 GO 版本的就是了,你可以拿去讓 AI 幫你處理。

錢很重要,上下文更重要

如果你搜尋 Claude Code Status Line,你會查到很多大家分享的狀態列設定和做法,很多都會把 ccuseage 的狀態列整合工具加進來,搞的狀態列會有一堆錢 $ 的符號 XD

整合 ccusage 之後的狀態列,塞滿了金額資訊
整合之後的狀態列:$0.34 session/$69.65 today/$69.65 block/$25.20/hr,滿滿的錢字號

這些費用資訊對開發來說是雜訊

大腦的上下文也是需要保護滴。

而且不管是哪個訂閱方案(pro, max 5x, 20x),你都只會 care 是不是要碰到限流。除非你費用計算方式是算 API 費用的,如果是這樣的話,那這樣設定就很合理 XD

我知道就是會忍不住想知道今天回本沒,好想知道已經花了多少 api 費用。

小秘訣:在等 AI 生成的時候有的是時間去看。我自己是把 ccuseage 監視畫面開在另一個 iPad 螢幕上,iPad 再次又有了價值,不然都放在旁邊生灰。

工作桌照片:筆電、外接螢幕、直立螢幕,中間立著一台 iPad 開著 ccusage 監視畫面
iPad 專職當 ccuseage 監視器,主畫面留給開發

搭配語音通知

還記得上一篇的Claude Code 語音通知:成為 AI 奴隸主的第一步

語音通知

用聽的知道狀況

Status Line

用看的知道狀況

這樣我們就已經有了良好的腦機協作基礎配置。

下一篇我會分享我的 Claude Code 自訂咒語 /command,讓你使用燒 token 禁術,招喚大量 Sub Agent 來完成批次任務。

留言