Tmux状态栏中的智能体状态

Lobsters Hottest 工具

摘要

本文探讨了在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 ```

相似文章

AgentPeek

Product Hunt

AgentPeek 是一款 macOS 实用工具,可在 Mac 的灵动岛区域显示 Claude Code 和 Codex 等 AI 编程代理的运行状态。

AgentOS

Product Hunt

AgentOS 提供了一个统一控制层,用于管理 AI 代理、任务和工作空间。