#!/bin/bash # tmux-cc-monitor.sh — 实时监控 tmux pane 中 cc (Claude Code) 的消息流 # # 用法: # tmux-cc-monitor.sh watch %1 [%2 ...] # 监控指定 pane # tmux-cc-monitor.sh watch-all # 监控当前 session 所有 pane # tmux-cc-monitor.sh help # # 工作原理: # 1. 每 200ms 轮询 tmux capture-pane 获取 pane 内容 # 2. 用内容 hash 跟踪已显示行,仅显示新增内容 # 3. 根据 ❯ 标记区分用户输入和模型输出 # 4. 彩色流式显示到当前终端 set -euo pipefail POLL_INTERVAL=0.2 # Colors C_INPUT='\033[1;32m' C_OUTPUT='\033[0;36m' C_SYSTEM='\033[0;33m' C_RESET='\033[0m' ts() { date +%H:%M:%S; } strip_ansi() { sed 's/\x1b\[[0-9;]*[a-zA-Z]//g; s/\x1b\[[0-9;]*m//g; s/\r$//g' } is_blank() { local s="$1" [[ -z "${s//[[:space:]]/}" ]] } # ─── 噪声过滤 ─── is_noise() { local line="$1" local trimmed trimmed=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') [[ -z "$trimmed" ]] && return 0 # 状态栏 [[ "$trimmed" =~ ^[─═]+$ ]] && return 0 [[ "$trimmed" =~ 版本.*模型 ]] && return 0 [[ "$trimmed" =~ bypass ]] && return 0 [[ "$trimmed" =~ ^─+.*──[[:space:]]*$ ]] && return 0 # cc 加载动画 (含省略号 … 的行) [[ "$trimmed" == *…* ]] && return 0 # cc 命令状态 [[ "$trimmed" =~ Running\ [0-9]+\ bash\ command ]] && return 0 [[ "$trimmed" =~ Ran\ [0-9]+\ bash\ command ]] && return 0 return 1 } # ─── 行级去重 ─── # 用 (行内容hash) 跟踪已显示的行,避免重复输出 # 维护一个固定大小的环状缓冲区 SEEN_CAPACITY=500 declare -A SEEN_LINES=() SEEN_ORDER=() SEEN_POS=0 # 记录一行已显示,返回 true 如果是新行 mark_seen() { local line="$1" local hash hash=$(echo "$line" | md5sum | cut -d' ' -f1) if [[ -n "${SEEN_LINES[$hash]+x}" ]]; then return 1 # 已存在 fi SEEN_LINES["$hash"]=1 SEEN_ORDER+=("$hash") # 裁剪缓冲区 if [[ ${#SEEN_ORDER[@]} -gt $SEEN_CAPACITY ]]; then local old_hash="${SEEN_ORDER[0]}" SEEN_ORDER=("${SEEN_ORDER[@]:1}") unset 'SEEN_LINES[$old_hash]' fi return 0 } # ─── 单 Pane 监控 ─── watch_pane() { local pane_id="$1" local state="idle" local last_input="" # 初始化: 将当前所有行标记为已读 local init_content init_content=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null || echo "") while IFS= read -r line; do is_blank "$line" || mark_seen "$line" || true done <<< "$(echo "$init_content" | strip_ansi)" echo -e "${C_SYSTEM}[$(ts)][${pane_id}] 开始监控${C_RESET}" local prev_content="$init_content" while true; do local content content=$(tmux capture-pane -t "$pane_id" -p 2>/dev/null) || { sleep 0.5 continue } # 快速检测: hash local prev_hash curr_hash prev_hash=$(echo "$prev_content" | md5sum) curr_hash=$(echo "$content" | md5sum) [[ "$curr_hash" == "$prev_hash" ]] && { sleep "$POLL_INTERVAL"; continue; } # 清洗内容 local clean_curr clean_curr=$(echo "$content" | strip_ansi) # ── 找出所有新行(基于行级去重)── while IFS= read -r line; do is_blank "$line" && continue is_noise "$line" && continue # 检查是否是新行 if ! mark_seen "$line"; then continue fi # ── 分类 ── if echo "$line" | grep -q '❯'; then local input_text input_text=$(echo "$line" | sed 's/.*❯//' | tr -d '\302\240' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') # 去重: 用 trim 后的文本比较,避免前导空格导致的重复 if [[ -n "$input_text" && "$input_text" != "$last_input" ]]; then echo -e "${C_INPUT}[$(ts)][${pane_id}][输入]${C_RESET} ${input_text}" last_input="$input_text" state="output" elif [[ -z "$input_text" ]]; then state="idle" last_input="" fi else if [[ "$state" == "output" ]]; then echo -e "${C_OUTPUT}[$(ts)][${pane_id}][输出]${C_RESET} ${line}" fi fi done <<< "$clean_curr" prev_content="$content" sleep "$POLL_INTERVAL" done } # ─── Main ─── cmd="${1:-help}" shift || true case "$cmd" in watch) [[ $# -eq 0 ]] && { echo "用法: tmux-cc-monitor.sh watch [pane_id ...]"; exit 1; } pids=() for pane_id in "$@"; do ( # 每个 pane 有独立的去重缓冲区 SEEN_LINES=() SEEN_ORDER=() watch_pane "$pane_id" ) & pids+=($!) done cleanup() { for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null || true done } trap cleanup INT TERM echo -e "${C_SYSTEM}[$(ts)] 监控已启动 (PIDs: ${pids[*]}),Ctrl+C 停止${C_RESET}" wait ;; watch-all) panes=$(tmux list-panes -F '#{pane_id}' 2>/dev/null || true) [[ -z "$panes" ]] && { echo "错误: 未找到 tmux panes"; exit 1; } exec "$0" watch $panes ;; help|--help|-h) echo "用法: tmux-cc-monitor.sh " echo "" echo "命令:" echo " watch 监控指定 pane 的 cc 消息流" echo " watch-all 监控当前 session 所有 pane" echo "" echo "输出:" echo -e " ${C_INPUT}[时间][pane][输入]${C_RESET} 用户提示词" echo -e " ${C_OUTPUT}[时间][pane][输出]${C_RESET} 模型响应(流式)" ;; *) echo "未知命令: $cmd" >&2 exit 1 ;; esac