同时打开三四个 AI 会话,是很多开发者的日常:左边窗格在跑代码审查,中间让本地模型生成摘要,右边开着 API agent 处理接口文档。你切回终端才发现,最早的任务早就输出完了,正停在一个等待输入的提示符上,白白浪费了几分钟。更麻烦的是,你开了好多窗格,每次都只能一个个敲回车看哪个“活过来”。
这个问题本质上是:tmux 只负责把你的多个会话保持在同一个终端里,它并不关心某个会话里的 AI 是“继续输出中”,还是“已经结束生成、正在等你回复”。所以这次我们来看一个实用方案:用 tmux 加轻量脚本,把“哪个 AI 正在等你”直接显示在状态栏上。
方案不依赖具体模型,不要求高显存,核心就是把tmux capture-pane做成一个状态轮询器,让每个 AI 窗口的“静默时长”可见。下面会先解释 tmux 为什么不够用,再给出可落地的 tmux 配置、监控脚本、状态栏接入方法,以及批量 AI 任务下怎么扩展。
1. 多 AI 会话管理的核心能力速览
先给结论,这套方案能解决什么、不能解决什么,看下面这张表。
| 能力项 | 说明 |
|---|---|
| 目标场景 | 同一台机器用 tmux 同时运行多个终端 AI 会话、本地模型推理或 API agent |
| 核心痛点 | 多个 AI 会话输出完成后,无法快速知道哪个会话处于等待输入状态 |
| 方案类型 | tmux 原生能力 + 少量 Bash 脚本,不需要新建重量级服务 |
| 基础依赖 | tmux、bash、md5sum/md5、系统date命令 |
| 显存/硬件要求 | 不参与 AI 推理的话基本无要求;参与推理则取决于具体模型 |
| 启动方式 | 先创建 tmux session 和 window,再启动各 AI 客户端 |
| 状态展示 | 在 tmux status bar 实时输出每个窗口的“静默秒数”或“WAIT”标记 |
| 批量任务 | 支持,按窗口命名 + 结束标记扩展,适合多任务并行 |
| 可观测粒度 | 秒级,可自行调整状态栏刷新间隔 |
| 主要局限 | 无法识别所有终端 UI 等待状态,依赖静默检测或客户端输出标记 |
这个方案适合已经习惯 tmux、同时跑多个 AI CLI 工具的开发者。如果你只是偶尔开一个对话,没必要搞这套。需要手动监控多个 AI 任务并且经常切错窗口的时候,这套脚本的收益才最明显。
2. tmux 的定位和“不够用”的原因
tmux 是一个终端复用器,它的核心能力是 session、window、pane 管理。它能让你在一台远程服务器上长时间保留多个终端环境,断线重连后任务还在,这很重要。但它不会理解某个 pane 里的程序在做什么。
tmux 默认的“活动提醒”机制,比如monitor-activity,是通过“窗口中是否有新输出”来判断你要不要切过去的。这对于持续打印日志的任务有效。可是 AI 会话有一个完全相反的规律:它生成完内容之后,恰恰是安静下来的那一刻需要你介入。比如 AI 输出完了,Shell 提示符停在那里,不会有新的字符滚出来。tmux 只会把它当成一个普通空闲窗口,不会响铃,不会亮红色,也不会把窗口名改成“该你回复了”。
另一个问题是窗口数量一多,人眼很难跟踪。你开了 5 个 AI 窗口,每个窗口都长得很像:都是滚动日志、都停在某个>或者$后面。仅仅靠不断切换窗口去看,已经低效。状态栏最多显示几个窗口名,不会告诉你哪个会话已经跑完。
所以这里的判断不是“tmux 没用”,而是“tmux 缺少业务状态感知”。如果你想等一批 AI 任务,关键信息不在于“哪个窗口有新活动”,而在于“哪个窗口超过一段时间没有活动,并且现在正处于应该给输入的状态”。这个信息必须从业务侧定义,然后用脚本补上去。
3. 多 AI 会话等待监控的总体设计
目标很容易描述:在 tmux 状态栏看到类似这样的输出:
AI0:3s AI1:80s AI2:WAIT含义是:
- AI0 刚刚还有内容输出,可能正在生成或你刚输入过;
- AI1 已经静默 80 秒,大概率已经结束输出,需要切过去看;
- AI2 通过 wrapper 打印了
[WAIT]标记,明确知道它在等待你输入。
实现这个效果需要四个步骤:
- 约定窗口命名规则,比如 AI0、AI1、AI2,每个窗口对应一个独立的 AI 会话。
- 配置每个 AI 客户端能运行在独立 tmux window 里。
- 写一个监控脚本,定期抓取每个窗口当前的可见屏幕内容,计算内容哈希。
- 用 tmux status bar 刷新机制展示结果。
如果内容哈希和上次一样,说明这个窗口从上次采集到现在没有新输出,于是“静默秒数”会持续增长。一旦超过阈值,就认为它可能正在等待你输入。更精确的做法是:如果 AI 客户端能在等待输入前输出一个固定标记,比如[WAIT],脚本一旦检测到标记,就直接把窗口标成 WAIT。
这套方案有个好处:它不修改 tmux 的工作方式,只是增加一个状态轮询。所有代码都在用户目录下,随时可以停掉。
4. 环境准备与基础 tmux 配置
建议在 Linux 服务器、macOS 或 WSL 里操作。Windows 原生环境也支持 tmux,但脚本用到的命令最好统一在 WSL 或 Git Bash 下验证。
先检查基础命令:
tmux -V bash --version如果 tmux 没有安装,用系统自带包管理器安装。Ubuntu 可用apt,macOS 可用brew,WSL 同样走 Linux 源。
# Ubuntu/Debian sudo apt update sudo apt install -y tmux # macOS brew install tmux接下来把 tmux 状态栏刷新间隔调小一点。默认间隔可能偏长,状态栏更新不够及时。建议 2 到 3 秒。
在~/.tmux.conf中加入:
set -g base-index 1 set -g pane-base-index 1 set -g status-interval 3 set -g status-left-length 200 set -g status-left "#(bash $HOME/bin/ai_status_bar.sh)" set -g status-right "#(whoami)@#H #[fg=blue]%H:%M#[default]"这里的重点是status-left调用一个外部脚本。tmux 会在每隔status-interval秒时执行这个脚本,并把脚本输出渲染到状态栏左侧。
配置完成后重新加载:
tmux source-file ~/.tmux.conf如果 status-left 里脚本路径写错了,状态栏会显示空内容或者报错内容。建议先用绝对路径,并且先给脚本添加执行权限。
5. 创建独立 AI 会话窗口
我们要把多个 AI 任务分别放到不同 tmux window 里。这里定义 session 名字为aiwork,初始建 3 个窗口:AI0、AI1、AI2。
手动创建的命令是:
SESSION=aiwork tmux new-session -d -s "$SESSION" -n AI0 tmux new-window -t "$SESSION" -n AI1 tmux new-window -t "$SESSION" -n AI2然后在对应窗口里启动 AI 客户端。不同客户端命令不一样,这里用下面的方式示意:
# 启动 AI0 窗口中的客户端 tmux send-keys -t "$SESSION:AI0" 'python3 my_ai_chat.py --name ai0' Enter # 启动 AI1 窗口中的客户端 tmux send-keys -t "$SESSION:AI1" 'ollama run qwen2.5' Enter # 启动 AI2 窗口中的客户端 tmux send-keys -t "$SESSION:AI2" 'aider --model gpt-4o' Enter tmux select-window -t "$SESSION:AI0" tmux attach-session -t "$SESSION"这些命令只是示例。你实际运行时,替换成自己正在用的 AI 客户端即可。重点是窗口命名要有规则,后续监控脚本才知道去遍历哪些窗口。
如果希望经常反复启动,可以把上面内容写成一个脚本~/bin/start_ai_session.sh:
#!/usr/bin/env bash set -euo pipefail SESSION="${1:-aiwork}" # 已存在则 attach if tmux has-session -t "$SESSION" 2>/dev/null; then tmux attach-session -t "$SESSION" exit 0 fi tmux new-session -d -s "$SESSION" -n AI0 tmux new-window -t "$SESSION" -n AI1 tmux new-window -t "$SESSION" -n AI2 tmux send-keys -t "$SESSION:AI0" 'bash ~/bin/ai_runner.sh ai0' Enter tmux send-keys -t "$SESSION:AI1" 'bash ~/bin/ai_runner.sh ai1' Enter tmux send-keys -t "$SESSION:AI2" 'bash ~/bin/ai_runner.sh ai2' Enter tmux select-window -t "$SESSION:AI0" tmux attach-session -t "$SESSION"给脚本加执行权限:
chmod +x ~/bin/start_ai_session.sh之后每次连接远程开发机,只需执行:
bash ~/bin/start_ai_session.sh aiwork已经在跑就直接 attach,不会重复创建窗口。
6. 状态监控脚本:计算每个窗口的静默时长
这一步是核心。脚本要做的事简单说就是:每隔几秒看一遍每个窗口的可见画面,如果画面文本没有变化,就认为这个窗口静止了。
保存脚本到~/bin/ai_status_bar.sh,内容如下:
#!/usr/bin/env bash # ai_status_bar.sh # 输出示例:AI0:3s AI1:80s AI2:WAIT export SESSION="${TMUX_SESSION:-}" WAIT_THRESHOLD=30 MARKER="[WAIT]" AI_WINDOWS=("AI0" "AI1" "AI2") if [ -z "$SESSION" ]; then echo "no-session" exit 0 fi now=$(date +%s) left="" for win in "${AI_WINDOWS[@]}"; do win_name="$SESSION:$win" if ! tmux list-windows -F '#{window_name}' -t "$SESSION" 2>/dev/null | grep -qx "$win"; then left="${left} ${win}:off" continue fi hash_file="/tmp/tmux_ai_hash_${SESSION}_${win}" time_file="/tmp/tmux_ai_time_${SESSION}_${win}" text=$(tmux capture-pane -t "$win_name" -p 2>/dev/null | md5sum | awk '{print $1}') if [ ! -f "$hash_file" ] || [ "$(cat "$hash_file")" != "$text" ]; then echo "$text" > "$hash_file" echo "$now" > "$time_file" fi last_change=$(cat "$time_file" 2>/dev/null || echo "$now") idle=$(( now - last_change )) if echo "$text" | grep -q "$MARKER"; then left="${left} #[fg=yellow]${win}:WAIT#[default]" elif [ "$idle" -gt "$WAIT_THRESHOLD" ]; then left="${left} #[fg=red]${win}:${idle}s#[default]" else left="${left} ${win}:${idle}s" fi done echo "${left# }"给脚本加上执行权限:
chmod +x ~/bin/ai_status_bar.sh脚本设计里有几个关键点:
- 使用
tmux capture-pane -p截取当前可见屏幕,不读取整个滚回 buffer,因此每次执行的开销很小。 - 用
md5sum计算内容指纹,内容只要变化,就代表该窗口有新输出。 - 内容变化时更新时间戳,内容长时间不变时,idle 值会越来越大。
- 超过
WAIT_THRESHOLD秒后用红色显示,提醒你切过去看。 - 如果 AI 客户端输出了
[WAIT],脚本会把该窗口标记为黄色 WAIT,比单纯的静默判断更精确。
需要注意:如果你的系统是 macOS,原生的md5sum不存在,需要改成md5 -q。把脚本里的一行替换为:
text=$(tmux capture-pane -t "$win_name" -p 2>/dev/null | md5 -q)Linux 上继续使用md5sum即可。
7. 验证效果:从启动到状态栏出结果
先启动一个 session:
bash ~/bin/start_ai_session.sh aiwork进入 tmux 后,第一件事是确认状态栏左侧已经出现内容。正常情况会看到类似:
AI0:0s AI1:0s AI2:0s然后手动向 AI0 窗口发一条长文本生成任务。可以看到 AI0 窗口持续输出,此时状态栏里AI0的秒数会不断重置为较小值。AI1 和 AI2 如果一直没动,秒数会慢慢增长。等 AI0 输出完成停在输入提示符后,它的秒数也会开始增长。
要验证静默告警,可以在某个窗口里故意不做任何操作,等超过 30 秒。状态栏就会变成类似:
AI0:12s #[fg=red]AI1:45s#[default] AI2:0s如果希望更明确地区分“等待输入”和“任务卡死”,可以让 AI 客户端在自身进入输入循环时打印一行[WAIT]。比如写一个简单的 wrapper:
#!/usr/bin/env bash # ai_runner.sh echo "[WAIT] start" python3 my_ai_chat.py echo "[WAIT] end"实际项目中,my_ai_chat.py内部会在每次收到模型完整输出后,打印一个[WAIT]再进入下一次输入。这样监控脚本就能直接识别。
输入验证样例:
# 向 AI2 发送内容 tmux send-keys -t "aiwork:AI2" '帮我解释一下这段代码' Enter等 AI2 输出完毕后,如果 wrapper 在界面里打印了[WAIT],状态栏该窗口会立刻变为黄色AI2:WAIT。
这套流程跑通后,你不需要再逐个切换窗口看有没有卡住。判断依据已经从“人眼扫描”变成了“状态栏红黄提示”。
8. 让等待检测结果更可靠:利用输出标记
纯静默检测有一个问题:某些 AI 终端工具在等待输入时,会一直显示转圈动画、光标闪烁或刷新进度条。每次状态栏刷新时内容都不同,静默秒数会被不断重置,于是脚本一直认为“这个窗口还在工作”。
解决方式就是前面提到的[WAIT]标记。它的思路是:不要在状态层面猜测,而是让 AI 客户端自己告诉你“我现在要向你提问了”。
在 wrapper 里增加标记时,建议输出一个不容易与业务内容混淆的字符串。例如:
# my_ai_chat.py 示例片段 while True: user_prompt = input(">>> ") if user_prompt.strip(): continue # 关键:进入输入等待前打印标记 print("[WAIT]", flush=True)这只是一个极简示范。实际终端客户端不一定允许你随意插入这种输出,常见做法有几种:
- 如果 AI 客户端本身提供 hook,比如每次生成结束后会执行某个 shell 命令,那是最理想的接入点。
- 如果客户端不是交互式终端,而是从 stdin 读取输入,输出到 stdout,wrapper 可以在一次完整交互结束时补打标记。
- 如果客户端支持自定义 prompt 格式,可以把
[WAIT]写进提示符,然后监控脚本去匹配该前缀。 - 如果客户端完全无法改,那就退回到静默检测方案,并把阈值设得更保守一些。
不管用哪种方式,核心是让监控脚本识别的是业务状态,而不是单纯看终端有没有动静。这样可以有效减少误报和漏报。
9. 接入批量任务:同一时间跑多个 AI 队列
除了在线聊天式 AI,批量任务场景也适配这套监控。比如你有job-a、job-b、job-c三个目录,分别要交给同一个大模型 API 去跑。你可以把每个 window 命名成任务名,然后用同样的状态栏监控。
举一个批量任务脚本结构:
#!/usr/bin/env bash # run_batch_job.sh # 用法:bash run_batch_job.sh job-a job=$1 echo "[START] $job" python3 batch_worker.py --job "$job" > "/tmp/logs/${job}.log" 2>&1 code=$? echo "[EXIT] $code" >> "/tmp/logs/${job}.log" echo "[WAIT] job finished"启动任务时,让每个任务跑在一个独立窗口:
SESSION=batch tmux new-session -d -s "$SESSION" -n job-a tmux send-keys -t "$SESSION:job-a" 'bash ~/bin/run_batch_job.sh job-a' Enter批量任务通常不需要人工干预,所以这里的[WAIT]更准确的语义是“这个任务已经跑完,可以看结果了”。你可以把MARKER改成[DONE],再让监控脚本区分颜色:
- 绿色表示正在运行,秒数小;
- 黄色表示命中
[DONE],可以去读日志; - 红色表示静默超过阈值,可能卡死或崩溃。
如果任务数量很大,把AI_WINDOWS=("job-a" "job-b" "job-c")改为动态发现即可。在 Bash 脚本里可以用tmux list-windows动态组装窗口列表。这里提供第三种代码变体:
#!/usr/bin/env bash # 动态发现 session 内所有窗口 mapfile -t AI_WINDOWS < <(tmux list-windows -F '#{window_name}' -t "$SESSION" 2>/dev/null)这样新增任务窗口时,不需要修改监控脚本和 tmux 配置。
批量任务建议每个任务都在独立窗口运行,同时把完整日志写到磁盘文件。日志是批次审计的依据,不能只依赖终端回显。
10. 资源占用与状态栏刷新性能
这套方案的资源占用主要集中在两个操作上:tmux capture-pane抓取屏幕内容和计算文本哈希。
每次抓取当前可见屏幕,并不抓整个回滚历史,所以耗时很短。状态栏默认 3 秒刷新一次,每个窗口执行一次capture-pane和一次md5sum。三个窗口的情况下,平均 CPU 占用可以忽略。如果你有 10 个窗口,也可以把status-interval调大到 5 秒或 8 秒,减少刷新频率。
需要注意的变量是:
- 窗口尺寸越大,
capture-pane返回的文本量越大,哈希计算耗时越长。窗口数量很多时,建议不要开超大终端窗口。 - 如果 AI 客户端处于持续刷新状态,比如转圈动画、实时 token 计数,那么每次抓去的文本都会变化,脚本就会频繁更新时间戳,idle 一直很小。
- 状态栏刷新越频繁,视觉上越灵敏,但 CPU 占用也会增加。对普通开发机来说,2 到 3 秒是比较舒适的区间。
- 使用
md5sum计算不会对 tmux 主服务造成明显压力,因为每次调用的只是外部进程。
临时文件方面,脚本会为每个窗口生成两个文件,存放在/tmp下。窗口关闭后这些文件不会自动清理。如果想清理:
rm -f /tmp/tmux_ai_hash_* /tmp/tmux_ai_time_*如果要长期运行,建议把这些 hash 文件放到一个固定目录,方便监控和清理。例如:
mkdir -p ~/.cache/tmux_ai_status hash_file="$HOME/.cache/tmux_ai_status/${SESSION}_${win}.hash" time_file="$HOME/.cache/tmux_ai_status/${SESSION}_${win}.time"11. 常见问题与排查方法
下面这张表列出了实际使用中最容易出现的问题和排查思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 状态栏一直显示 no session | TMUX_SESSION变量为空,脚本没有拿到当前 session 名 | 在 tmux 内执行echo $TMUX_SESSION | 脚本里 fallback:export SESSION="${TMUX_SESSION:-$(tmux display-message -p '#{session_name}')}" |
| 状态栏空白 | status-left 脚本路径错误或文件不可执行 | 检查~/.tmux.conf中的路径和权限 | 使用绝对路径,执行chmod +x |
| 窗口长时间不更新,状态显示 0s | 窗口内有动画或光标闪烁,capute 内容一直变化 | 打开该窗口观察输出节奏 | 改用手动[WAIT]标记方式判断 |
| 明明已经输出完成,但状态栏不红 | WAIT_THRESHOLD设置太大,还没到阈值 | 查看当前 idle 值 | 把阈值降到 15 到 30 秒之间 |
| 脚本频繁执行导致 CPU 较高 | status-interval太短或窗口数太多 | 用top查看 bash 进程 | 把status-interval调大到 5 秒以上 |
| 某些窗口不存在,状态栏有 off 标记 | 创建 session 时没创建对应窗口 | 检查 session 窗口列表 | 确认命名后再启动监控脚本 |
macOS 上md5sum找不到 | macOS 自带md5 | 执行which md5 | 脚本中将md5sum | awk替换为md5 -q |
| tmux source-file 后状态栏没变化 | 配置有语法错误或脚本输出包含换行 | 终端执行tmux source-file ~/.tmux.conf看报错 | 修正配置或脚本中的echo输出 |
| 状态栏显示颜色代码,而不是颜色 | status-left 用了#[fg=red],但 tmux 未开启颜色终端 | 检查TERM设置 | 使用screen-256color或tmux-256color |
用[WAIT]标记失败 | wrapper 输出内容被滚动屏幕冲掉,或 AI 客户端吞掉了标记 | 用日志文件查看 wrapper 输出 | 增加tee /tmp/ai.log记录完整输出 |
最常踩的坑是状态栏脚本执行时没有 session 上下文。在 tmux 外部直接测试ai_status_bar.sh时,TMUX_SESSION为空,脚本自然不知道遍历哪个 session。建议在脚本里加入 session 自动发现:
if [ -z "$SESSION" ]; then if tmux display-message -p '#{session_name}' >/dev/null 2>&1; then SESSION=$(tmux display-message -p '#{session_name}') else echo "no-session" exit 0 fi fi12. 最佳实践与使用建议
这套方法的价值不在于“监控脚本本身多聪明”,而在于给你的多 AI 工作流增加了一个统一状态层。以下几点可以让它在实际项目里更稳定。
第一,给每个 AI 客户端套一层统一的 runner wrapper。wrapper 负责做三件事:设置窗口名、记录日志、在关键节点输出[WAIT]或[DONE]。这样所有 AI 任务的输出格式就统一了,监控脚本只需要接收这个约定。不要嫌这层封装多余,等你要跑 5 个并发 AI 任务的时候,它给你的不只是状态栏提示,还有完整日志和退出码。
第二,窗口命名尽量固定成AI0、AI1、AI2,或者直接使用任务名。不要用随机生成的窗口名,否则动态监控脚本很难维护。
第三,对于涉及代码仓库、文档、私有数据的 AI 会话,必须先确认数据使用的授权边界。不要因为任务跑得太顺手,就把内部敏感代码随意发给没有授权的第三方 API。本地模型可以自行控制,外部 API 要看服务条款和权限。
第四,不要把WAIT_THRESHOLD设得太小。AI 思维链比较长的任务,中间可能几十秒没有输出,容易误报。第一次运行时先用 60 秒观察,再根据自己的习惯改成 30 秒或 20 秒。
第五,状态栏显示红色后,第一件事不是马上发消息,而是切到对应窗口看清楚是“等待输入”还是“任务卡死”。在 wrapper 设计阶段,尽量给“完成”和“异常”分别打标记。没有标记时,至少通过日志最后几行判断。
第六,如果 tmux 本身的状态栏不够用,可以考虑再开一个 dashboard window,用tmux select-layout把监控脚本放到一个独立 pane 里持续输出。这适合窗口特别多的情况。
13. 总结与下一步
回到开头的场景:以前你要频繁切换窗口,在多个 AI 会话里找哪个停了。现在只需要让 tmux 状态栏显示AI1:80s和AI2:WAIT,你就能决定下一步切到哪个窗口。
建议先做最小验证:按文章里的示例脚本建 3 个 AI 窗口,跑通状态栏显示,再慢慢加入[WAIT]标记和批量任务。最先要验证的是静默检测方案,这是不依赖具体客户端的通用底座。最容易踩的坑是 AI 客户端有动画刷新,导致静默检测失效,这种情况就用 wrapper 标记兜底。
后续如果你想继续扩展,可以考虑把状态栏输出转发到通知系统。比如心跳检测到某个窗口超过 5 分钟没有变化时,用系统通知提醒你。也可以把监控脚本从 Bash 换成 Python,加上更准确的终端交互状态判断。
这套方案不复杂,但它能让“多开 AI”真正变得可控。先跑