如果你也经常在终端里长时间挂着 Claude Code 做批量重构、文档生成或者大规模代码检查应该会遇到一个很实际的痛点对话窗口里的文本一直在滚后台到底有几个 Subagent 在跑、跑到哪一步了、还有多少任务排队、有没有已经失败的完全要靠自己去翻日志。翻日志倒也不难但每次切窗口、动鼠标思路就断了。我自己就是用这种方式折腾了一个下午之后决定做一个自定义状态栏工具把所有关键信息实时钉在终端底部一抬头就能看到。这篇文章不是介绍某个现成的大而全工具而是完整记录我自己从需求拆解、方案选型、安装配置到最终实现 Subagent 状态展示的整个动手过程。文章里会给出可直接抄作业的命令、配置文件和解析代码也会把踩过的坑和排查思路原原本本写出来。适合两类人看一类是刚接触 Claude Code、被长时间后台任务困扰的新手另一类是想把终端工作流组件化、数据化的老手。无论哪一类我保证你能从中找到可以直接用的东西。1. 自定义状态栏的思路为什么值得自己搭一个1.1 终端里跑 Claude Code 时最缺什么先说清楚我面对的具体场景。我习惯把 Claude Code 当成一个需要长时间“托管”的助手来用比如给它一个包含几十个模块的工程让它逐个分析并生成文档或者让它跑一轮跨文件的接口梳理通常一跑就是十几分钟甚至更久。在这个过程里Claude Code 自己会创建 Subagent 并行处理子任务主对话流会继续输出中间结果。问题就出在这里我必须知道后台到底在做什么否则根本不敢离开屏幕。默认情况下终端里能获得的信息就是流式的滚动日志。比如“开始处理模块A”“Subagent 已创建”“模块A处理完成”这类消息它们以线性文本的形式出现。你当然可以用 CtrlR 或者滚动回看去搜索但代价太大了。我算过一笔账每翻一次日志至少打断 10 秒的注意力如果一次长任务里翻上 20 次就是 3 分多钟的无效干扰更别提误操作 CtrlC 的风险。所以我的核心需求很清楚要有一个始终可见的区域聚合展示当前会话的总体进度、正在运行的 Subagent 数量与耗时、队列深度、失败数量。这不是锦上添花而是长时间任务的基本保障。只有把这些关键指标从日志里抽出来变成“仪表盘”我才敢放心去写别的代码。1.2 方案选型提示符、tmux 还是独立状态栏需求明确之后我列了三个候选方案也做了对比。第一个方案是改 Shell 提示符用 oh-my-zsh 或者 starship 加一个自定义 segment从日志里读取状态并拼接成字符串。优点是零依赖缺点是提示符只在命令执行完和回车前后刷新任务运行期间它基本不会主动更新完全不满足“实时监控”的要求。第二个方案是用 tmux 的 status-right 或 status-left。tmux 本身就支持每隔几秒刷新状态栏所以实时性问题解决了。但 tmux 的状态栏是给窗口和会话信息设计的它能显示固定文本可一旦 Subagent 数量多了状态栏会被撑得很乱而且想在底部做一个多行、结构化的表格也相当吃力。第三个方案就是写一个独立的小工具专门做状态渲染。它可以放在普通终端里也可以输出到 tmux 的 status 区域本质是一个独立的进程只做“读日志、解析结构化事件、渲染状态行”。它的成本和复杂度比前两个高一些但无论是信息量、刷新频率还是可扩展性都最符合我的场景。三个方案的对比如下维度Shell 提示符方案tmux status 方案独立状态栏工具实现成本低低中实时刷新能力弱中强Subagent 信息量少少多环境依赖无需要 tmux仅需 Python/Node扩展性低中高最终我选了第三个方案。如果你只是想偶尔看看任务跑完没有第一个方案就够了但如果你想在长任务里保持清晰判断第二个和第三个才是正确方向。我甚至建议背景是长期使用 tmux 的人可以把独立状态栏工具和 tmux 配合使用两个方案并不冲突。1.3 核心设计原则日志即数据源展示层独立动手之前我给自己定了两条设计原则。第一条是“日志即数据源只读不改”。状态栏工具只需要像跟踪日志一样随着文件增长去读取数据绝不写入 Claude Code 的配置目录或会话目录。这样做的好处是即使状态栏工具写得再烂也影响不到 Claude Code 本身我见过有的方案为了取状态去直接改进程行为这在生产环境里是不可接受的。第二条是“解析与渲染分离”。工具的职责分两层底层负责把原始日志解析成结构化的状态对象比如 Subagent 的 id、task、开始时间、状态、耗时上层负责把这些对象渲染成可视化的行。这样以后想换展示风格或者想把状态数据推送到网页面板、写入监控系统都不需要重写解析逻辑。我实际开发时底层维护了一个状态表上层只是做格式化输出后续扩展起来非常轻松。2. 安装与初始化从零跑起状态栏2.1 前置条件与目录结构这个方案不依赖任何花哨的运行时我推荐用 Python 3.8 以上版本因为解析 JSON 行、做正则匹配、处理 ANSI 转义都顺手。需要确认的只有一件事确认你的机器上有 python3 命令。Node 14 以上也可以但我自己的实现是 Python 单文件安装更简单。我建议把状态栏工具放在独立目录里比如~/cc-status/不要在具体项目目录里放。原因是状态栏是跨项目使用的放在 home 目录下切换项目时配置不丢失。我的目录结构长这样~/cc-status/ ├── cc_status.py # 主脚本核心逻辑都在这里 ├── config.yaml # 配置文件 ├── parse_jq.sh # 可选的 jq 解析辅助脚本 └── README.md # 记录自己的改动不管你是直接复制我的脚本还是自己动手写第一步都建议用 git 初始化这个目录。状态栏这种小工具看着简单改着改着就复杂了有版本控制在改坏了随时回滚。2.2 安装步骤四步完成整个过程我拆成四步。第一步确认环境第二步创建脚本第三步建立可执行命令第四步生成配置并验证。先看完整命令# 1. 确认 Python 版本 python3 --version # 2. 创建目录并进入 mkdir -p ~/cc-status cd ~/cc-status # 3. 把 cc_status.py 放进目录然后加上执行权限 chmod x cc_status.py # 4. 建立软链接让任何目录下都能直接运行 ln -sf ~/cc-status/cc_status.py /usr/local/bin/cc-status # 5. 验证命令可执行 cc-status --version这里有一个细节值得说明为什么用软链接而不是直接把脚本复制到/usr/local/bin因为软链接保证了脚本永远只有一份后续你改的是~/cc-status/cc_status.py系统里执行时自动读到新版本。否则每次改完都要再去拷贝一次很容易漏。如果你没有/usr/local/bin的写权限也可以把软链接放到~/.local/bin前提是这个目录在你的PATH里。2.3 验证安装第一次看到状态栏装好主程序之后执行初始化命令生成默认配置cc-status init这个命令会自动创建config.yaml并尝试定位 Claude Code 的会话日志目录。日志目录的具体位置我不写死因为不同版本、不同操作系统的路径可能不同你可以先用cc-status --check查看它找到的路径并手动确认该目录下能看到 会话 相关的日志文件。如果路径不对需要在配置里手动指定。验证无误后运行cc-status watch这时屏幕底部会出现一条状态栏内容大致是[main-session] 8/12 tasks | [gen-docs] 42s | [gen-api] 12s | 3 queued | 1 failed第一次跑出来可能是空的显示“等待日志事件”这很正常。只要没有立刻报错说明安装已经成功接下来就是配置和细节打磨的问题了。3. 配置详解让状态栏贴合你的工作流3.1 配置文件结构与关键字段我的config.yaml长这样下面逐项解释log_path: ~/.claude/logs # Claude Code 会话日志目录以你本机实际为准 session_filter: recent # 最近一个会话也可用 project 或 pid poll_interval: 2 # 状态栏刷新周期单位秒 layout: compact # compact / table / merged theme: background: dark status_running: yellow status_done: green status_failed: red show_elapsed: true subagent: show_queue: true queue_max: 20 # 状态栏最多同时展示多少条 Subagent show_truncated_tasks: false # 超出的部分是否折叠为 N morelog_path是最关键的一项。我见过有人把轮询间隔设成 0.2 秒想追求极致实时结果日志文件是 Claude Code 进程持续写入的状态栏高频读取不仅白白消耗磁盘 I/O还容易读出半个被截断的 JSON 行。实测下来2 秒的刷新间隔足够用了大部分 Subagent 任务的生命周期以秒为单位2 秒不会漏掉关键状态切换同时把 I/O 开销控制在可以忽略的水平。session_filter用于指定“我当前关心哪一个会话”常见取值有三个recent表示自动选择最近修改的日志文件project表示按项目名匹配pid表示直接指定 Claude Code 的进程 ID。前两种在日常使用中更方便第三种适合极端情况下精确定位。3.2 展示项定制你以为在写配置其实在搭驾驶舱layout字段决定了状态栏的呈现形态。compact是单行紧凑模式适合和 tmux 的 status 区域结合table是多行表格模式适合在独立终端窗口里使用。我最常用的是 table 模式它会显示成类似这样ID TASK STATUS START ELAPSED s-7f3a generate_docs running 10:00:01 42s s-81c2 refactor_utils running 10:00:03 12s s-90ab missing_depends failed 10:00:05 5s为什么把颜色配置单独拆出来因为我实际使用中发现长任务里最需要快速识别的不是“完成了多少”而是“有没有失败的”。我会把status_failed设成高亮的红色把status_running设成黄色这样即使状态栏离视线较远余光扫到红色也能立刻警觉。另一个实用技巧是开启show_elapsed这个字段会显示每个 Subagent 已运行多久超过某个阈值之后我再在外部做告警。使用过程中我强烈建议把配置里暂时用不到的字段注释掉而不是删掉。比如show_truncated_tasks我起初觉得没用后来同时跑 30 多个 Subagent 时才发现必须靠它折叠。配置保持完整以后需要时直接取消注释即可。3.3 多会话识别与进程隔离如果你和我一样经常同时在两个项目目录里各开一个 Claude Code状态栏工具很容易把两个会话的日志混在一起。解决办法就是前面提到的session_filter和附加参数。我一般这样用cc-status watch --project myapp cc-status watch --pid 12345从实现层面看多会话识别其实不复杂每个会话在日志目录里对应独立的文件或文件前缀工具按project或pid做匹配后只在对应文件上做增量读取。进程隔离的意义在于每个cc-status watch实例只属于一个会话互不干扰。如果你确实想在同一个终端里同时盯多个会话那就开多个终端窗口或者用 tmux 的分屏每个窗格跑一个实例。4. Subagent 状态展示的实现原理与细节4.1 Subagent 状态字段从哪来这一节是整篇文章的核心。Subagent 在运行过程中Claude Code 会在会话日志里留下结构化的事件记录本质上是 JSON Lines 格式每行一个 JSON 对象。一个典型的 Subagent 事件大概长这样字段名在不同版本里可能略有差异结构是类似的{event: subagent_start, id: s-7f3a, task: generate_project_docs, ts: 2025-01-01T10:00:01Z} {event: subagent_complete, id: s-7f3a, status: success, duration_ms: 42000, ts: 2025-01-01T10:00:43Z} {event: subagent_start, id: s-81c2, task: refactor_utils, ts: 2025-01-01T10:00:03Z}状态栏要做的就是把这两类事件关联起来合并成一张状态表。一个 Subagent 的身份是id它从subagent_start开始进入running状态遇到subagent_complete或者类似的结束事件时切换成success、failed或者cancelled。中间如果一直没有任何结束事件它就是“正在运行”。我在实际开发中遇到一个很重要的细节subagent_complete里给的duration_ms只能算最终耗时但如果你想在它还没结束时就显示“已运行 42 秒”就得自己记录start时间然后计算差值。所以状态表里的字段我会设计成下面这样state_by_id { s-7f3a: { id: s-7f3a, task: generate_project_docs, status: running, # running / success / failed / cancelled start_ts: 1756000001, elapsed_sec: 42, duration_ms: None, output_tail: ... # 最近一段输出方便排查失败原因 } }有了这张表所有展示都变得很自然列表长度就是当前监控到的 Subagent 总数状态等于running的数量就是正在运行中的数量状态等于failed的就是失败数量而队列深度可以近似为“已经启动但尚未结束”的数量加上配置里预期会启动的数量。4.2 日志解析实战从 JSON 行到状态表如果你只是临时排查问题可以用 jq 快速验证日志里是否有对应事件。我的parse_jq.sh里放了一个辅助命令tail -f ~/.claude/logs/session.log | jq -r select(.event | startswith(subagent)) | [.id, .event, .status] | tsv但状态栏工具本身不能依赖 jq因为它是持续运行、需要维护内存状态的。我直接用 Python 的逐行解析方式import json, time, os def parse_line(line, state_by_id): line line.strip() if not line: return try: obj json.loads(line) except json.JSONDecodeError: return event obj.get(event) if event subagent_start: state_by_id[obj[id]] { id: obj[id], task: obj.get(task, ), status: running, start_ts: obj.get(ts), duration_ms: None, output_tail: } elif event subagent_complete: sid obj[id] if sid in state_by_id: state_by_id[sid][status] obj.get(status, success) state_by_id[sid][duration_ms] obj.get(duration_ms)这段代码看起来简单但有一个实用技巧解析时永远要try/except json.JSONDecodeError因为状态栏的刷新周期和日志写入周期不同步极有可能读到半行 JSON。更稳的做法是使用增量读取时记录文件偏移量每次只从上次读到的地方继续解析。解析完成之后渲染循环每隔poll_interval秒扫描一次状态表更新界面。这里有一个取舍是每次全量重绘还是只更新变化的区域我选择全量重绘状态栏区域因为状态栏本身只有几行全量重绘的成本远小于逐个判断字段变更的复杂度而且代码清晰很多。4.3 渲染细节刷新频率、颜色与终端控制渲染层的关键是使用 ANSI 转义序列控制光标位置而不是用 curses 或 textual 这类交互式 TUI 库。原因有二ANSI 序列兼容性最好能直接嵌套进 tmux 的 status 区域而完整 TUI 库在刷新时会抢占终端控制权很容易和 Claude Code 本身正在输出的内容“打架”。我的核心渲染代码长这样def render(status_lines): # 隐藏光标避免刷新瞬间光标闪烁 sys.stdout.write(\033[?25l) # 把光标移动到状态栏起始行 sys.stdout.write(\033[20;1H) # 清空从当前位置到屏幕底部的内容 sys.stdout.write(\033[J) # 逐行写入 sys.stdout.write(\n.join(status_lines)) sys.stdout.flush()这里的20;1H只是一个示例位置实际高度要根据终端行数动态计算。我写完第一个版本后发现刷新时 Claude Code 的输出会被光标跳转干扰于是加了保存与恢复光标位置的逻辑sys.stdout.write(\033[s) # 保存光标位置 render(status_lines) sys.stdout.write(\033[u) # 恢复光标位置这套组合在普通终端和 tmux 里都能跑得很稳。关于颜色ANSI 的颜色代码很好拼比如把状态为 failed 的文本包裹成红色只需在输出前后加上\033[31m和\033[0m。我在实现时把颜色编码放进了配置具体映射如下状态默认颜色ANSI 编码running黄色\033[33msuccess绿色\033[32mfailed红色\033[31mcancelled灰色\033[90m4.4 越跑越长的任务show_queue 和截断策略Subagent 一多状态栏不可能全部展示。比如一个大型代码库生成文档时会同时启动十几个 Subagent如果全铺到屏幕上状态栏比正文还长那就本末倒置了。我的策略是默认最多展示 20 条超出部分折叠成一行N more。如果开启了show_truncated_tasks则把超出部分的 task 名用逗号拼接放在第二行。实测下来折叠是最舒服的方式因为大多数时候你要关心的只是“有没有失败”和“完成比例”少数需要盯细节的场合再用--verbose输出完整列表即可。另外展示顺序也要有讲究。我按start_ts从早到晚排列最老的任务放在最上面最新的放在底部。后来我发现按状态分组更实用running 的放在最前面紧接着是 failed最后才是 success。这样状态栏第一部分永远是最需要关注的“活跃中”和“异常”任务视觉不疲劳。5. 常见问题与排查实录5.1 状态栏不刷新如果你遇到状态栏一直不动首先要分清楚是“没有新数据”还是“根本读不到数据”。我的排查顺序如下# 1. 确认日志文件还在增长 tail -f ~/.claude/logs/session.log # 2. 确认状态栏进程是否存活 ps aux | grep cc-status # 3. 用调试模式看解析结果 cc-status watch --verbose最常见的坑是session_filter选错了会话。比如你开了两个 Claude Code状态栏匹配到了另一个已经结束的会话自然永远没有新事件。其次是权限问题日志目录如果属于其他用户状态栏进程没有读权限也会静默失败。我在设计工具时特意让--verbose模式打印出“当前监听的文件路径”和“文件偏移量”大多数不刷新问题一眼就能定位。5.2 Subagent 状态显示不全或错乱显示不全一般有两种原因。一种是真的没有启动那么多 Subagent另一种是状态栏的queue_max太小把后面的任务截断了。前者不用处理后者把queue_max调大即可。我自己遇到过一次“显示错乱”现象是同一个id出现了两条记录后来发现是清理状态表的逻辑没做全subagent_complete事件到达之后我没有删除旧记录导致重复渲染。修复办法也很简单根据id更新已有记录而不是盲目追加。如果字段名版本不对解析会失败。我会在parse_line里加入兜底逻辑比如obj.get(event) or obj.get(type)同时把未识别的 JSON 行在--verbose模式下打印出来方便快速判断是不是新版本改了字段。5.3 快捷键和光标冲突状态栏渲染时会移动光标如果你正在用终端自带的搜索、复制或者滚轮翻页可能会出现光标跳动或者内容被覆盖。我前面提到的\033[s和\033[u保存恢复方案能解决大部分普通终端的冲突。但如果你在 tmux 里跑建议把状态栏输出喂给status-right而不是直接在内容区渲染这样两个区域物理隔离完全不会互相干扰。另外一个经常被忽略的坑状态栏占用了终端底部一行回车换行时可能把最后一条输出顶到状态栏区域里。我的解决办法是把状态栏高度预留出来渲染时先输出一个空行再把状态栏固定在空行之下。代码逻辑略微复杂但效果稳定。5.4 日志轮转导致状态中断长时间开着状态栏可能会遇到日志被外部工具轮转rotate的情况。旧文件被改名或删除新文件重新开始但状态栏还守着旧的 inode结果就是状态永远停在轮转前的最后一屏。这个问题我也踩过排查了很久才发现。解决办法是每次刷新前检查当前文件的 inode 是否变更def detect_rotation(path, last_inode): try: stat os.stat(path) if stat.st_ino ! last_inode: return True except FileNotFoundError: return True return False如果检测到轮转就重新打开文件并把状态表里所有 running 的任务标记为“等待恢复”。这个功能平时用不上但一旦遇到日志轮转能救你一次。5.5 附问题速查表现象最可能原因解决办法状态栏一直没有内容session_filter 匹配错误用--verbose查看监听的日志路径能刷新但数据少日志文件只有单行输出检查 Claude Code 版本是否输出 JSON 行中文 task 名乱码终端编码与日志编码不一致终端切换 UTF-8或脚本里强制 UTF-8 重新编码刷新时终端闪烁没有隐藏光标渲染前加\033[?25l结束恢复状态栏和输入重叠没有预留底部高度把状态栏固定到最底行输出前加空行任务完成后状态不消失状态表没有清理逻辑使用subagent_complete事件更新原记录日志轮转后停更文件 inode 变更用st_ino检测轮转并重新打开文件最后分享一个我一直在用的小技巧整个工具跑通之后我把它和 Claude Code 的启动绑定在一起做成一个 shell 函数再也不需要手动开状态栏。函数长这样cc() { cc-status watch --project $(basename $PWD) CC_STATUS_PID$! claude_code ${} kill $CC_STATUS_PID 2/dev/null }这样每次进入项目并启动 Claude Code状态栏自动开始监听退出时自动结束完全不需要额外操作。在 tmux 里配合使用效果最好我可以同时开几个窗格跑不同项目的任务每个窗格底部都有自己独立的状态栏。这个工具从构思到跑通花了我大半天时间但带来的收益是持续的。它解决的不只是“看到 Subagent 状态”这个表面问题而是把长时间后台任务的掌控感找回来了。如果你也经常被这类问题困扰强烈建议从最小版本开始自己实现一版配置和渲染逻辑都可以按自己的习惯慢慢打磨。
阅读完成 · 觉得有帮助?