Tmux状态栏中的智能体状态
摘要
本文探讨了在tmux状态栏中显示AI智能体会话状态的方法,通过比较窗格标题与前台进程来创建指示器,以区分工作、阻塞或空闲状态。
<p><a href="https://lobste.rs/s/rwzf83/agent_state_tmux_status_line">评论</a></p>
查看缓存全文
缓存时间: 2026/09/16 07:23
# Tmux状态栏中的代理状态 | The Cloudlet 原文:https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/
## 问题所在
在tmux窗口中并行运行多个代理会话(Claude Code、Codex、PingMe (https://github.com/TheCloudlet/PingMe)),很容易混淆哪个窗口仍在工作、哪个卡在等待决策、哪个已完成。手动逐个检查窗口的方式无法扩展到三四个以上。
Herdr通常是该领域的推荐方案:它是一个代理优先的终端复用器,会将会话分类为工作中、阻塞、完成或空闲状态,并在侧边栏中显示。使用一周后发现了两个问题——它需要切换上下文来获取本应紧邻其描述窗口的信息;且它取代了tmux而非适配现有查看状态栏的习惯。
agenmux保留了tmux并解决了第二个问题,它将代理状态渲染到侧边栏窗格和状态栏片段中。两者都是独立于窗口列表的区域。接下来的方案将状态标记直接写在窗口条目上。
Herdr还跟踪第四个状态**完成**——指窗口不可见时完成的轮次,下次访问时清除。这需要每个窗格的焦点历史记录,而以下设置不保留该记录;完成状态被合并为空闲状态,最终保留三种状态。目标是在tmux窗口列表中实现相同的分类。
曾有两个信号看似可行实则失败。
## 尝试一:窗格标题
tmux通过`pane_title`跟踪每个窗格的标题,程序通过OSC转义序列更新标题。代理CLI是交互式TUI程序,因此标题成为承载其状态的首选候选。
```bash
set -g @agent-status \
'#{?#{m:*Action Required*,#{pane_title}},#[fg=colour196]#[bold]!,'\
'#{?#{m/r:(^| )[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏◐◓◑◒]( |$),#{pane_title}},#[fg=colour220]●,'\
'#{?#{m/r:(^|/)(codex|claude|pingme)$,#{pane_current_command}},#[fg=colour34]✓,}}}'
```
黄色状态从未出现。在完整工作轮次中每秒采样两次标题解释了原因:
```bash
$ for i in $(seq 1 40); do tmux list-panes -a -F '#{pane_id} [#{pane_title}]'; sleep 0.5; done | sort -u
%0 [✳ Tmux config article]
%1 [✳ Claude Code]
```
整个采样中只出现两个唯一值。Claude Code设置标题后就再未修改——它承载的是会话名称而非状态,对常量字符串使用任何正则表达式都无法产生状态机。该采样包含两个Claude窗格。
Grok在同一tmux服务器上:
```
t=1 title=[Review of tmux agent-status article - grok]
t=2 title=[⠋ - Waiting for response... - Review of tmux agent-status article - grok]
t=4 title=[⠋ - Thinking - Review of tmux agent-status article - grok]
t=7 title=[⠸ - Thinking - Review of tmux agent-status article - grok]
```
标题中的盲文旋转器(来自黄色规则匹配的字符类)。标题对Claude Code无效但对Grok有效,因此可按代理设置规则直接读取Grok的标题。屏幕内容无需依赖OSC标题更新即可承载两种代理的状态。
## 尝试二:前台进程
Claude的标题被排除后,下一个候选是`pane_current_command`——tmux认为的前台进程。运行工具的代理与停留在提示符的代理占用不同的前台进程。
```bash
$ tmux list-panes -a -F '#{pane_id} cmd=[#{pane_current_command}]'
%0 cmd=[2.1.267]
%1 cmd=[2.1.273]
```
不是`claude`。安装布局解释了原因:
```bash
$ ls -l ~/.local/bin/claude
... -> ~/.local/share/claude/versions/2.1.273
$ ls -l ~/.grok/bin/grok
... -> ../downloads/grok-1.0.30-macos-aarch64
```
两个启动器都是指向版本化二进制文件的符号链接,进程承载该二进制文件名。tmux报告:`claude`显示为`2.1.267`,grok显示为`grok-1.0.30-mac`(从`grok-1.0.30-macos-aarch64`截断)。该值标识的是发布产物而非程序本身。绿色图标也从未在此机器上亮起。其规则要求`pane_current_command`以`claude`结尾,而此值从不满足,因此第三个分支与其他两个一起落入空回退。
进程树中包含一个`comm`为`claude`的进程,无论窗格前台命令名称如何,因此对树执行`ps -o comm=`可回答存在性问题。状态则是另一回事:`pane_current_command`标识进程,而进程身份不编码"思考中"与"等待批准"的状态差异。
## 状态的实际承载者
agenmux (https://github.com/snirt/agenmux) 是一个开源tmux代理监控器,移植了Herdr的检测规则,其文档说明:
> 检测仅通过抓取实现:通过遍历每个窗格的进程树识别代理,状态从窗格可见屏幕和标题推断。
即渲染后的终端缓冲区,而非标题或进程。
Herdr将Claude Code和Codex的规则匹配应用于实时缓冲区底部的快照。tmux恰好通过`capture-pane`暴露了该内容。工作中的Claude Code窗格底部:
```
✽ Brewing... (2m 14s · ↓ 3.9k tokens)
─────────────────────────────────────────────────────
❯
─────────────────────────────────────────────────────
⏵⏵ auto mode on (shift+tab to cycle) · esc to interrupt · ← for agents
```
同一窗格空闲时:
```
─────────────────────────────────────────────────────
❯
─────────────────────────────────────────────────────
⏵⏵ auto mode on (shift+tab to cycle) · ← for agents
```
`esc to interrupt`在一种状态下存在而另一种不存在。该提示仅在存在可中断项时渲染,因此它成为状态标识。其上方活动行也携带旋转器(`✽ Brewing...`),通过字形`✳ ✽ ✶`——与首次尝试搜索的盲文和圆形字符集不同的字符类,位于不同表面。
相同配置在Linux机器上似乎也有效(其标题同样静态)。那里亮起绿色状态,仅需`pane_current_command`解析为`claude`——在此机器上成立但此处不成立。绿色仅报告窗格存在代理进程;从未报告该进程在做什么。
## 实现方案
两个信号各自回答能回答的问题:进程树回答"此处是哪个代理",屏幕缓冲区回答"它在做什么"。遍历返回代理名称而非是/否答案,因为屏幕规则因代理而异:
```bash
pane_agent() {
root="$1"
pids="$root"
queue="$root"
while [ -n "$queue" ]; do
pid="${queue%% *}"
queue="${queue#"$pid"}"
queue="${queue# }"
for c in $(pgrep -P "$pid" 2>/dev/null); do
case " $pids " in
*" $c "*) ;;
*) pids="$pids $c"; queue="$queue $c" ;;
esac
done
done
ps -o comm= -p $pids 2>/dev/null | sed -nE 's|.*/||; /^(claude|claude-code|codex|grok|pingme)$/p' | head -n 1
}
```
然后每个代理获得自己的模式集和顺序。伪代码形式——`match`代表对捕获屏幕的grep匹配,模式被省略;附录 (https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/#appendix-the-full-poller) 包含可运行版本:
```
claude, claude-code, pingme:
working if screen matches 'esc to interrupt|ctrl+c to interrupt'
idle if screen matches a bare '❯' prompt line
action if screen matches 'do you want to proceed?|waiting for permission|...'
idle otherwise
codex:
action if screen matches 'press enter to confirm or esc to cancel|...'
working if screen matches 'esc to interrupt'
idle otherwise
grok:
action if screen matches '/:select|Allow ...?|No, reject|dialog footer hints'
working if screen matches '[stop]|Ctrl+c:cancel'
idle otherwise
```
Claude和Codex都打印`esc to interrupt`,但规则顺序不同。Claude将该提示视为权威并先检查working状态,在blocked模式前有空闲提示规则,避免已回答的权限提示被误读为等待。Codex先检查其批准提示。任一顺序反转都会导致所有提示被读作忙碌,或所有忙碌轮次被读作阻塞。
关于来源的说明:herdr和agenmux对Codex先检查**标题**再检查屏幕(`CHECK_ORDER="bt wt bs ws"`)。此处省略标题规则,使检测独立于OSC序列到达tmux。Grok的标题承载状态而Claude的不承载,因此标题规则对一方有效对另一方无效。仅屏幕规则延续,上述顺序仅在屏幕规则间。`pingme`遵循Claude的规则,因其封装Claude Code并渲染其UI;此分组是假设而非独立观察。
Grok需要最多工作。agenmux未为其提供清单;herdr有(grok.toml (https://github.com/herdrdev/herdr/blob/master/src/detect/manifests/grok.toml)),但针对Grok Build 0.2.101,其页脚与本机安装的1.0.30不同——它期望`Ctrl\+.:shortcuts`而此构建打印`Ctrl\+x:shortcuts`。足以确认方法,但字符串必须从实时会话重新读取。普通对话建议`Esc:cancel`,该提示在轮次运行时位于页脚,结束时消失。此规则对聊天有效但对首个shell命令失败:长时间工具调用期间旋转器文本停止更新,取消提示变为`Ctrl\+c:cancel`。在`sleep`中停留二十秒的窗格在整个轮次中报告空闲。
活动行上的`\[stop\]`提示在两种情况下持续存在。仍先检查blocked,因为grok的批准提示保留引发该提示的工具调用中的`\[stop\]`,working优先顺序会将每个权限提示读作忙碌。herdr的清单将`ctrl\+c:cancel`分类为blocked信号,与`:select`和`ctrl\+o:yolo`配对,而非working后备。
如下文所示将其视为working,完全依赖blocked模式首先匹配;它们遗漏的权限页脚会被读作忙碌而非等待。两个失败都源于从单一观察场景中提取信号,当第二个场景运行时两者都暴露。规则覆盖实际监视的状态范围。以上列表为阅读而精简;完整脚本(包含所有明确拼写的模式)位于附录 (https://thecloudlet.github.io/technical/til/tmux-agent-status-indicator/#appendix-the-full-poller)。
此方案作为后台循环运行而非在格式字符串内。早期版本从`\#\(\.\.\.\)`调用脚本,以微妙方式失败:`\#\(\)`不是同步调用而是结果缓存到**下次**重绘的任务,因此该方式计算的状态仅在上次屏幕绘制时最新。分离会话完全不重绘。进程树和屏幕内容是机器的真实状态,无论是否有人观察都成立——因此它们应遵循自己的时钟,格式字符串简化为纯读取:
```bash
set -g @agent-status \
'#{?#{==:#{@agent-state},action},#[fg=colour196]#[bold]!,'\
'#{?#{==:#{@agent-state},working},#[fg=colour220]●,'\
'#{?#{==:#{@agent-state},idle},#[fg=colour34]✓,}}}'
set-window-option -g window-status-format '#{E:@agent-status}#[fg=colour18]#[nobold]#I:#W#F '
set-window-option -g window-status-current-format '#{E:@agent-status}#[fg=colour252]#[nobold]#I:#W#[fg=colour196]#[bold]* '
```
两者都需要:tmux通过自身格式渲染当前窗口,因此仅设置第一个会导致当前查看的窗口没有图标。`set-option -p`将每个答案限定到单个窗格。窗口列表每个窗口渲染一个图标,基于其活动窗格解析,因此运行两个代理的分屏会显示其中之一。循环每个服务器启动一次,通过PID文件而非`pgrep -f agent-status-poll.sh`守护:argv包含脚本名的包装shell会对任何名称子串守卫产生假阳性。
```bash
if-shell '! kill -0 "$(cat /tmp/tmux-agent-status-poll.pid 2>/dev/null)" 2>/dev/null' \
'run-shell -b "~/.config/tmux/agent-status-poll.sh"'
```
## 结果
窗口列表成为看板:红色`\!`需要决策,黄色`●`工作中,绿色`✓`空闲就绪,无标记为普通shell。无论客户端是否附加它都保持正确,因为检测不依赖于观察。
tmux状态栏在空闲Claude窗口显示绿色勾号,在工作grok窗口显示黄点。
在底行,`✓1:123444`是空闲Claude窗格,`●2:grok-1.0.30-mac`是grok工作轮次中,其窗口名是尝试二中的版本化二进制文件名。工作规则匹配该窗格的`Waiting for response... 6.8s`行及其`\[stop\]`标记。
标题和进程名是元数据,对某些代理有效对另一些无效。Claude的标题是固定会话名;Grok的标题承载实时旋转器。Grok的前台命令是其自身二进制文件;Claude的是版本字符串。屏幕是所有代理填充的表面,因为那是它们存在以绘制的表面。
三个代理需要三套规则和两种顺序,Grok的规则从实时会话重新读取,因为herdr的清单针对Build 0.2.101而本机安装的是1.0.30。规则匹配页脚字符串,当代理重新设计页脚时静默失效,无版本号可核对。
herdr在存在时偏好生命周期钩子:`full_lifecycle_hook_authority`命名pi、omp、mastraccode、opencode、kilo和kimi为自身报告覆盖屏幕检测的代理。Claude Code、Codex和Grok不在此列表中,herdr抓取它们。Claude Code的钩子可直接写入`@agent-state`,覆盖三个中的一个,安装到该代理配置中,但对运行其他程序的窗格报告无信息。屏幕是唯一对所有代理可用的输入。
## 附录:完整设置
### 轮询器
`~/.config/tmux/agent-status-poll.sh`,经claude 2.1.x和macOS上的grok 1.0.30验证。Codex的模式来自agenmux,未经测试沿用。两个已知问题:PID文件位于固定`/tmp`路径,一台机器上的两个tmux服务器会竞争它;Grok的规则固定于一个版本的页脚文本——该处重新设计会静默失效,这是屏幕抓取的固有成本。
```bash
#!/bin/sh
# 后台轮询器:每秒对每个窗格的代理状态进行分类,并写入
# 该窗格的@agent-state用户选项,供状态栏读取。
#
# 两个信号,两者都必要:
# - 进程树:此处运行哪个代理?这些CLI通过符号链接启动版本化二进制文件,
# 因此pane_current_command报告该文件名(claude -> "2.1.267",grok -> "grok-1.0.30-mac")
# 而非代理名称。遍历树并匹配`ps -o comm=`可找到它。
# - 可见屏幕:该代理在做什么?每个代理都在此绘制其状态,
# 而标题则不然:Claude的是固定会话名,Grok的承载实时旋转器。
#
# 屏幕模式和检查顺序因代理而异。Claude和Codex的来自
# agenmux的agents/*.conf(移植herdr清单),减去它们首先检查的标题规则
# ——有意省略,使检测从不依赖OSC序列到达tmux。它们不止措辞不同:
# Claude将中断提示视为权威并先检查working状态,
# 而Codex先检查其blocked提示。
# 经claude 2.1.x和grok 1.0.30验证;codex未经测试。
echo $$ >/tmp/tmux-agent-status-poll.pid
# 被终止时干净退出:tmux在状态栏报告任何非零run-shell退出,
# 被终止的守护进程是常规操作,不是值得显示的错误。
trap 'rm -f /tmp/tmux-agent-status-poll.pid; exit 0' EXIT HUP INT TERM
# 在窗格进程树中回显找到的代理二进制文件(如有)。
pane_agent() {
root="$1"
pids="$root"
queue="$root"
while [ -n "$queue" ]; do
pid="${queue%% *}"
queue="${queue#"$pid"}"
queue="${queue# }"
for c in $(pgrep -P "$pid" 2>/dev/null); do
case " $pids " in
*" $c "*) ;;
*) pids="$pids $c"; queue="$queue $c" ;;
esac
done
done
ps -o comm= -p $pids 2>/dev/null | sed -nE 's|.*/||; /^(claude|claude-code|codex|grok|pingme)$/p' | head -n 1
}
# 检查窗格屏幕是否包含任一模式
screen_match() {
# $1=窗格ID,$2=模式,$3=正则标志(-E或-F)
tmux capture-pane -p -t "$1" -S - | grep -${3:-E} "$2" >/dev/null 2>&1
}
# 确定代理状态
classify_state() {
pane="$1"
agent="$(pane_agent "$pane")"
state="idle"
case "$agent" in
claude|claude-code|pingme)
if screen_match "$pane" 'esc to interrupt|ctrl\+c to interrupt'; then
state="working"
elif screen_match "$pane" '❯'; then
state="idle"
elif screen_match "$pane" 'do you want to proceed\?|waiting for permission|waiting for approval'; then
state="action"
fi
;;
codex)
if screen_match "$pane" 'press enter to confirm or esc to cancel'; then
state="action"
elif screen_match "$pane" 'esc to interrupt'; then
state="working"
fi
;;
grok)
if screen_match "$pane" '/:select|Allow|No, reject|dialog footer'; then
state="action"
elif screen_match "$pane" '\[stop\]|Ctrl\+c:cancel'; then
state="working"
fi
;;
esac
echo "$state"
}
# 主循环:每秒检查每个窗格
while true; do
for pane in $(tmux list-panes -a -F '#{pane_id}'); do
state="$(classify_state "$pane")"
tmux set-option -p -t "$pane" "@agent-state" "$state"
done
sleep 1
done
```
相似文章
Agent-Manager:一款用于运行Claude Code、Codex和OpenCode的Tmux TUI
Agent-Manager 是一款基于 Tmux 的 TUI,可让您在单独的 tmux 会话中并排运行和监控多个 AI 编码代理(Claude Code、Codex、OpenCode 等),支持实时状态、快速提示和差异审查。
@DavidOndrej1: 你的AI代理在关闭终端的瞬间就会消失。tmux解决了这个问题... 分离,走开,几小时后回来,它还在运行…
Tmux允许AI代理在关闭终端后持久运行,支持分离和稍后重新连接。
AgentPeek
AgentPeek 是一款 macOS 实用工具,可在 Mac 的灵动岛区域显示 Claude Code 和 Codex 等 AI 编程代理的运行状态。
AgentOS
AgentOS 提供了一个统一控制层,用于管理 AI 代理、任务和工作空间。
终端代理:命令行环境中AI代理的综述
本文综述探讨了在命令行环境中运行的AI代理,提出一个框架,通过终端介导执行的视角,统一软件工程和其他领域的研究。