Statusline 系统 — 3+1 行布局完全指南
无法度量就无法掌控。智能体开发在一次会话里消耗几十万 token,快速填满上下文窗口 (context window,模型一次能记住的对话总量),让多个智能体(自行工作的 AI 助手)并行运转,并直接影响提示缓存 (prompt cache,复用相同上下文来压低成本的技巧) 的命中与否。如果这一切在终端里看不见,就无法回答“为什么这次会话花了两倍的成本”。定制 statusline 系统正是从这个痛点出发的。token 经济学 (tokenomics,把 token 用得经济的方式) 始于度量,所以我们把上下文使用率、缓存命中率、rate limit 消耗率常驻显示在终端底部。
本文以入门书的深度梳理 statusline 显示什么、数据如何流动、以及上下文被填满时它给出什么信号。比起段落格式的细节,先讲清楚“为什么需要这些信息、怎么读”。
在智能体编程中决定成本与质量的变量有五个:用哪个模型、以哪个推理深度在跑、上下文窗口填了多少、rate limit 还剩多少、提示缓存是否正常生效。这五个变量彼此相连 —— 上下文满了会出现 SSE 停滞 (stream stall,流式传输停下来的现象),缓存不命中成本立刻上涨,rate limit 见底就得停下重活。
问题在于这些变量默认看不见。Claude Code 自带的状态栏够丰富,但装不下 MoAI 工作流要处理的信息 —— 活动 SPEC(需求规格书)、当前 PR 的评审状态、交接 (handoff,把会话接续起来的操作) 建议时机。所以 MoAI 把自己的状态栏以三行立在终端底部,多会话 run 时再附上第四行,让“现在 token 是怎么花的”和“现在在哪里做什么”一眼可读。
基本布局是三行,存在会话名·积压观测时,第四行(会话行)会按条件附加在最后。下面的示例是实际渲染输出的一个实例,连每个段落使用的字形 (glyph,小图形字符) 都原样搬来。
🤖 Opus | 🧠 xhigh·t | ♻️ 87% | 🔅 v2.1.212 | 🗿 v3.1.3 | ⏳ 4h 52m | 💬 MoAI
🪫 CW: ███████░░░ 72% (⚠️/clear) | 🔋 5H: █████░░░░░ 56% (46m) | 🔋 7D: █░░░░░░░░░ 13% (May 28)
📁 moai-adk-go | 📡 modu-ai/moai-adk, 12/3 | 🅱️ main +2 | 💾 +0 M1 ?1 | 💌 PR #1234 (⌥approved)
🏷️ run | 👤 manager-develop | 🔄 TODO: 1/3- 第一行 —— 会话“跑得怎么样”: 模型、推理深度、缓存命中率、Claude Code 版本、MoAI 版本、会话时长、输出样式放进一行。让人立刻知道“这个会话是以什么配置在跑”。
- 第二行 —— 预算“还剩多少”: 上下文窗口使用率 (CW) 和两条滚动 rate limit(5 小时·7 天)以量表条展示。这是判断“现在能不能直接跑重活”的依据。
- 第三行 —— 现在“在哪里、做什么”: 目录、仓库与分支、打开的 issue 与变更请求数、git 状态、活动 SPEC 任务、以及打开中 PR 的评审状态打成一包。在以 PR 为中心的工作流里,这是最常看到的一行。
- 第四行(按条件)—— 以什么身份、积了多少: 显示会话名 (🏷️)、智能体名 (👤)、积压现状 (🔄
TODO: 进行中/待办)。在看板伴随会话这类有名字的会话里自然出现;没有观测来源时对应段落会缩水,全部为空则整行省略。这里也是高亮显示会话名的位置 —— 开着多个终端、分不清哪个窗口是哪个角色时,它是第一个信号。
statusline 不是一个单一程序,而是一条短管道。Claude Code 在每个渲染周期把会话状态做成 JSON 传过来,MoAI 接住后加工成三行还给终端。
flowchart TD
A["Claude Code
(把会话状态作为 stdin JSON 传递)"] --> B[".moai/status_line.sh
(shell wrapper — settings.json statusLine.command)"]
B --> C["moai statusline
(Go 单一二进制)"]
C --> D1["internal/statusline
(stdin JSON 解析)"]
D1 --> D2["internal/statusline
(内存·指标·git 收集)"]
D2 --> D3["internal/statusline
(3-line 渲染)"]
D3 --> E["终端底部显示 3 行"]为什么 shell wrapper 要夹在中间?因为 Claude Code 的 statusLine.command 只接受一个命令字符串。于是 .moai/status_line.sh 充当最小化的 shell 包装去调用 moai statusline 二进制,重活(解析·收集·渲染)全部在编译出的 Go 二进制里快速完成。这样一来,每次渲染不必拉起多个进程,也能一次画出海量信息。
数据收集阶段还会补足 stdin 里没有的信息。git 状态直接解析本地 git status --porcelain,MoAI 版本从本地设置读取,活动任务从会话状态文件取得。这样即使 Claude Code 没传来的上下文,也能装进这一行里。
第一行读的是“这个会话的设置与状态”。不只模型名,还用 Claude Code v2.1.139 起加入 stdin 的 effort/thinking 值显示“以哪个推理深度在跑、扩展思考 (thinking) 有没有开”。等级后面带 ·t(如 xhigh·t)表示扩展思考已激活;有这个标记,就能一眼核查模型策略是否真的在生效。
其中缓存命中率是 token 经济学的核心指标。它等于 cache_read token 除以 (cache_read + cache_creation) —— 削减常驻加载的指引,这个数字会立刻上升;反过来,每轮新读大文件或指引树突变,它就会掉。命中率偏低时,它就是追踪“哪个变更在啃食缓存”的线索。
数据不足时不编造数值,而是安静地隐藏 (graceful degradation)。缓存创建 token 为 0、或两个值都是 0 时,命中率段落干脆不显示。这种谦逊的省略防止了“用不存在的数字给人虚假的确定”。
第二行由三条量表条组成,各有含义。
- CW(上下文窗口): 表示当前会话把窗口填了多少。条的颜色是从绿到黄、再到红的连续渐变,前面的电池字形在显示百分比超过 70% 时变为“弱电”标志。窗口填满时 SSE 停滞的风险加大,所以这条量表是“该什么时候换会话”的第一信号。
- 5H(5 小时滚动): 最近 5 小时的 rate limit 消耗率。同时显示重置时刻,告诉你“离限额恢复还要等多久”。
- 7D(7 天滚动): 最近 7 天的 rate limit 消耗率。帮你掂量周预算还剩多少。
对订阅制用户来说,5H/7D 两条就是实实在在的预算量表。看这两条,就能理性决定“现在直接跑重活,还是为了省成本转给 CG 模式的 GLM 工人”。CW 条满了、5H 条也高时,停下会话改用交接接续,对成本和稳定性都有利。
第三行把工作的上下文打成一包。目录、仓库与分支(含打开项目的计数和脏文件数)、git 状态、活动 SPEC 任务、以及打开中 PR 的评审状态,都在这一行里。
仓库与分支渲染为一段合并的段落。owner/name 部分来自 Claude Code v2.1.145 起加入 stdin 的 workspace.repo,其后跟着本仓库打开的 issue 数与变更请求数,以 , 12/3 这样的逗号加斜杠形式呈现。分支从本地 git 读取。仓库显示带 📡 字形,与分支之间用 ASCII 竖线 (|) 相连 —— 两个值合起来,“在哪个仓库的哪个分支上干活"一目了然。在 worktree(挂接的独立工作目录)里工作时,分支前会加 [WT] 标记,与普通检出区分开。
仓库名后面紧跟着本仓库打开的 issue 数与变更请求数,以 , 12/3 这样的斜杠数对呈现。前一个数字是打开的 issue,后一个是打开的变更请求。这些值以前单独放在第四行,现在挂在所属仓库的同一个位置,用位置来回答"这个数字是哪个仓库的”。
一行里只放一个斜杠数对。这个位置以前放的是相对远程的领先/落后提交数,当打开项目的计数作为第二个数对挨着它出现后,确实有人把分支的 59/0 读成了"issue 对 变更请求"。于是这个位置让给了在状态栏上真正会被看的值 —— 打开的 issue 与变更请求 —— 而领先/落后不再在任何地方渲染。数据仍在采集,只是栏上没有它的位置了。
数对有四种状态。
| 显示 | 含义 |
|---|---|
, 12/3 | 已取到计数 —— 12 个打开的 issue、3 个打开的变更请求 |
, 0/0 | 已取到计数,并且确实没有打开的项目 |
, -/- | 有可询问的 forge,但此刻计数未知 —— 缓存缺失或无法读取,或 CLI 未能作答(速率限制、认证、网络)。这个状态在下一次刷新时可能自行解除 |
| 既无数对也无逗号 | 没有可询问的对象,或你要求不要询问 —— 五种原因就在下面 |
有五种原因会让数对整个消失。
segments.github: false—— 段落已关闭。statusline.forge: none(或off) —— 你已明确要求不统计。statusline.forge写了无法识别的取值(拼写错误)。不会打印任何警告:消失的数对本身就是那个能追回你刚写下之值的症状。origin远程不在可自动判定的公开主机上 —— 自建实例,或根本没有 origin。显式写下下面的forge键即可解决。- 对应的 CLI(
gh或glab)不在 PATH 上。
照实打印 0,以及数据缺失时拒绝打印 0/0,都是刻意的。打印出 0 让数对形状恒定,安静的仓库也能看出计数是活的;反过来,取数失败时打印 0/0,会把一次无声的查询失败报告成"没有打开的项目" —— 这是唯一绝不能发生的误读,所以"未知"有自己的形状 -/-。没有数据不等于数据为零。
而 -/- 只留给还能自行解除的状态。这个标记承诺的是"有可询问的 forge,答案正在路上"。若把同一个标记用在根本不会有答案的状态上,就会让使用者去追一个再怎么等也不会消失的故障。选择关闭的人并没有在等待什么 —— 因此 statusline.forge: none 不再像过去那样一直显示 -/-,而是把数对整个放下。段落被关闭时连 -/- 都不打印,也是同一个道理。
这种抑制不会固化。刷新子进程每次运行(TTL 10 分钟)都会从头重新判定,所以装上 gh 或改正 forge 的取值后,数对会自行回来 —— 不必改配置,也不必重启会话。
有一处时序差别值得知道。缺少 CLI 与主机无法识别这两种情况由刷新子进程判定,因此在第一个子进程写入缓存之前的若干次渲染里可能看到 -/-,等它跑过之后的下一次渲染即会消解(数秒)。而显式写下 forge: none 则没有这段窗口:数对在第一次渲染时就已消失。
打开项目的计数从哪里取,由 statusline.yaml 的 statusline.forge 键决定。
statusline:
forge: gitlab # github | gitlab | none| 值 | 行为 |
|---|---|
| 未指定(默认) | 按 origin 远程的主机判别 —— github.com 选 gh,gitlab.com 选 glab |
github | 始终用 gh 计数 |
gitlab | 始终用 glab 计数 |
none(或 off) | 不统计 —— 数对整个消失(不是 -/-) |
| 其他任何取值 | 不统计,且不退回自动判定 —— 数对整个消失 |
最后一行很重要。拼错时若按主机名暗示的一侧悄悄统计,错误的数字看起来就像是对的。所以无法识别的取值既不呈现为数字,也不呈现为 -/-,而是呈现为消失的数对。不会打印警告;这份缺席就是症状,能直接追回刚刚写下的配置值。
自托管实例仅凭名字无法区分 —— 公司内网 GitLab git.example.com 与公司内网 GitHub Enterprise 的地址形状相同。因此只自动判别两个公开主机,其他情况不做猜测,等待这个键。
对应的 CLI(gh 或 glab)不在 PATH 上时,只是没有计数,并不是错误 —— 数对会整个从这一行消失,最后一次缓存的值则保留在磁盘上以备之后恢复,这一行的其余部分照常渲染。之后装上 CLI,数对会在下一次刷新时自行回来。GitHub 一侧一次询问总数;GitLab 一侧只枚举一页(最多 100 条),因此打开项目多于此数时报告的是该页的数量而非真实总数。
git 状态在任何状态下都带同一个 💾 字形,后面跟着 +暂存 M修改 ?未跟踪 的计数。以前每种状态用不同的信箱字形,但细分状态后面的数字已经说清楚了,于是前面的字形统一成了一个。
PR 段落用颜色区分评审状态。approved 绿色、pending 黄色、changes_requested 红色、draft 灰色 —— 光看颜色就能掌握等待评审的 PR 处于什么状态。MoAI 工作流里每个 SPEC 都会走 plan-PR → run-PR → sync-PR 周期,把 PR 状态常驻显示,对决定下一步有直接帮助。
贴在 CW 条旁边的标记,是 statusline 给出的最重要建议。上下文用量超过按模型而定的阈值后,它会分两个阶段点亮。soft 阶段是“可以的话换会话”的建议,hard 阶段是“现在立刻换”的强信号。
flowchart TD
A["测量上下文使用率
(以 raw 用量为准)"] --> B{"窗口大小等级"}
B -- "1M 上下文
(Opus 5, GLM-5.3)" --> C{"使用率 ≥50%?"}
B -- "200K / 256K 标准
(Sonnet, Haiku, Fable)" --> D{"使用率 ≥90%?"}
C -- "否" --> N["无标记
(安全区间)"]
D -- "否" --> N
C -- "是" --> S["soft 标记 (⚠️/clear)
建议"]
D -- "是" --> S
S --> H{"到达 auto-compact 感知
天花板?"}
H -- "否" --> KEEP["维持 soft"]
H -- "是" --> HD["hard 标记 (🛑/clear!)
强信号"]
HD --> CLR["保存进度 →
paste-ready resume → /clear"]
S --> CLR阈值按模型等级各不相同,因为窗口越大,越早换会话对预防 SSE 停滞越有利。1M 上下文模型在装满一半 (50%) 时点亮 soft 标记,200K/256K 模型在 90% 时点亮。hard 标记是预先计入 auto-compact 动作时点的天花板。不过运行时的 auto-compact 常常先于这个天花板出手,所以 hard 阶段实际上是较少点亮的强信号。
标记点亮后,按既定顺序执行即可:把进行中的工作保存进 progress.md,拿到编排器生成的 paste-ready resume 消息,用 /clear 清空会话,再把那条消息粘进新会话接续。这个流程与会话交接规则一致。
有一点要注意。GLM-5.3 实际上是 1M 上下文模型,但 Claude Code 不管提供方是谁,都按 Claude 槽位标准 (Opus=1M, Sonnet/Haiku=200K) 报告 context_window_size。于是 GLM 会话里原始观测值可能错误地显示为约 180K。MoAI 在两处把它纠正 —— 启动器用 CLAUDE_CODE_MAX_CONTEXT_TOKENS 环境变量向会话声明 1M 窗口,状态栏用 internal/statusline/memory.go 的 ResolveGLMContextWindow 校正观测值。glm-5.3 和 glm-5.3-flash(默认模型)各自通过自己的表项映射为 1,000,000,也可以用 MOAI_STATUSLINE_CONTEXT_SIZE 环境变量直接覆盖,或用 llm.glm.context_windows 表设置。GLM 会话里请信任 MoAI 状态栏的 CW%,而不是原始值。
状态栏每次渲染时,还会把观测值记录到 .moai/state/context-usage/<session-id>.json。每个会话一份,以该会话命名,所以同一项目里多个会话同时运行也不会相互覆盖。这份记录会在下一个会话开始时作为“刚才窗口填了多少”的读取依据。raw_pct(原始使用率)和 stage(none/soft/hard)是核心字段,该会话实际运行的模型与 effort 也会一并写入。session_id、writer_pid、captured_at 同样保留。
为什么需要区分会话?当多个会话共用一个工作目录时,一个会话不能接过另一个会话的用量、误判“窗口已经满了”。所以要核对写记录的会话身份,不一致或过期的记录直接忽略,回退到原始观测值。目的是保守行事,而不是用缺失的数值制造虚假的确定。
段落开关在 .moai/config/sections/statusline.yaml 里配置。每行是一个段落开关。
statusline:
theme: catppuccin-mocha # 配色主题
forge: gitlab # github | gitlab | none (未指定 = 按 origin 主机判别)
segments:
# 第一行
model: true
effort_thinking: true
cache_hit: true
claude_version: true
moai_version: true
session_time: true
output_style: true
# 第二行
context: true
usage_5h: true
usage_7d: true
# 第三行
directory: true
git_branch: true # 仓库+分支合并
git_status: true
task: true
pr: true
worktree: false # 默认开启 —— 此行是显式关闭
github: true # 打开的 issue/变更请求数对 (仓库段落后缀)
# 第四行 —— 会话行 (默认开启; 不写也会渲染)
session: true # 🏷️ 会话名 + 👤 智能体
backlog: true # 🔄 TODO: 进行中/待办十六个键是正式的设置 schema。表示仓库的 owner/name 部分作为第十七个元素在 git_branch 段落里一并渲染,位于 schema 之外,没有单独开关。github 键名字照旧,但现在开关的是第三行仓库段落的 issue/变更请求 数对,而向哪个托管服务询问由上面的 forge 键决定。上面例子里写出的十九个开关中,十六个属于这套 schema,其余三个(github·session·backlog)在 schema 之外。这三个键不写进设置也默认开启渲染 —— 没有观测来源(会话名、积压队列、forge 缓存)时对应段落安静省略。过去那些带名字的预设 (full/compact/minimal) 已废弃,想要的组合按段落逐个开关即可。
刷新周期由 settings.json 的 statusLine.refreshInterval(单位:秒,默认 10)决定。这不是状态栏设置文件,而是 Claude Code 运行时设置。周期太短 CPU 负担加大,太长则上下文使用率的变化反映滞后。一般默认值就够用。
PR 不显示时确认三件事。Claude Code 必须是 v2.1.145 以上,stdin 才会带 pr 字段。用 gh pr view 确认当前分支有没有打开的 PR。再看设置里是否显式写了 pr: false。
交接标记不显示时多半正常。1M 模型在 50% 以下、200K/256K 模型在 90% 以下,就是还没到阈值。如果超过了阈值还不出现,确认模型的窗口大小是否正确映射(尤其 GLM 校正)。
颜色不显示时确认终端是否支持 ANSI 256-color、有没有设置 NO_COLOR=1、主题是否适配环境。
想看实际输出时可以把样例 stdin 用管道传入,画一次状态栏。给 moai statusline 命令的标准输入喂一段装着会话状态的 JSON 字符串,终端上会打出的三行就原样出现。用这个办法可以不进入渲染就检查设置变更对输出的影响。
Claude Code 2.1.169 以上提供了 /cd <path> 命令,保住提示缓存的同时更换会话的工作目录。状态栏的目录显示会更新到新路径,但至今积累的推理上下文不必重攒。可以把它理解为“不开新终端会话、把缓存留下”的办法。想在会话中途不丢上下文只挪工作目录时(例如工作中途切去 worktree),这是最省事的选择。与 resume 模式的联动见会话交接。
- Settings JSON — Claude Code
statusLine字段设置