Skip to content

Agent Monitor:基于 tmux 的移动端终端监控方案

Agent Monitor 是一个运行在开发机上的轻量 Web 工具,用来在手机上查看和接管本机正在运行的终端任务,重点场景是 Claude Code、Codex、测试命令和构建命令。

核心思路不是做远程桌面,也不是改造 Ghostty,而是把 tmux 当成终端会话的状态源。所有 AI agent 都跑在 tmux pane 里,服务端定期读取 pane 列表和最近输出,再通过手机端页面展示状态;需要干预时,再把输入通过 tmux send-keys 发回对应 pane。

这更像给现有终端工作流外挂一个 observer service,而不是侵入 Ghostty runtime。

目标

这个工具解决的是“人离开电脑,但 AI agent 还在工作”的状态可见性问题。

需要支持:

  • 在手机上看到当前有哪些 tmux session / pane。
  • 看到每个 pane 最近输出,判断 Codex / Claude 是否还在跑。
  • 识别常见状态:运行中、等待输入、失败、完成、空闲。
  • 在手机上回复一段文本并发送 Enter。
  • 提供少量快捷键,例如 Enter、Ctrl-C、Ctrl-D、Esc。
  • 对 stale session 提供 kill session 能力,避免已无价值的会话长期挂着。
  • 通过 token 做最小鉴权,再结合 Tailscale、局域网或 Cloudflare Tunnel 访问。

非目标:

  • 不做完整远程桌面。
  • 不做多用户账号系统。
  • 不保存长期历史。
  • 不直接依赖 Ghostty 内部 API。

产品形态

电脑端启动一个本地服务:

bash
AGENT_MONITOR_HOST=0.0.0.0 \
AGENT_MONITOR_PORT=8797 \
AGENT_MONITOR_TOKEN=<token> \
bun run start

手机打开:

text
http://<开发机 IP>:8797/?token=<token>

首页是一个 dashboard:

text
Waiting  1
Running  3
Failed   0
Done     2
Idle     4

下面是 pane 列表,每一项展示:

  • session / window 名称
  • 当前命令,例如 codexclaudenodezsh
  • 推断出的状态和原因
  • 最近几行输出

点进详情页后展示:

  • tmux target,例如 work:0.1
  • cwd
  • pid
  • 最近 300 行输出
  • 输入框
  • 快捷键按钮
  • Kill tmux session 危险操作按钮

架构

当前实现位于:

text
/Users/not/projects/devs/opensource/agent-monitor

目录结构:

text
agent-monitor
├── src/server.ts
├── public/index.html
├── public/app.js
├── public/styles.css
├── package.json
└── README.md

整体链路:

text
手机浏览器
  |
  | HTTP / WebSocket
  v
Bun server
  |
  | tmux list-panes / capture-pane / send-keys / kill-session
  v
tmux sessions
  |
  | panes
  v
Claude Code / Codex / tests / shell

这里的 Bun server 类似一个 Hono/NestJS handler 层,只是数据源不是 PostgreSQL 或 ClickHouse,而是 tmux 的命令行接口。

服务端职责

src/server.ts 做几件事:

发现 pane

通过:

bash
tmux list-panes -a -F '<format>'

读取所有 session、window、pane 信息。

当前采集字段包括:

  • session_name
  • window_index
  • window_name
  • pane_index
  • pane_id
  • pane_current_command
  • pane_current_path
  • pane_active
  • pane_pid
  • pane_title

服务端把它们整理成统一 Pane 对象。

抓取最近输出

通过:

bash
tmux capture-pane -p -J -S -300 -t <pane_id>

读取最近 300 行。-J 会 join wrapped lines,让手机上看到的文本更接近实际语义,而不是纯粹按终端宽度折行。

状态推断

当前是轻量规则,不引入模型:

  • 最近输出包含 Do you wantProceed?Continue?y/n 等,标记为 waiting
  • 最近输出包含 failederror:panic:traceback 等,标记为 failed
  • 最近输出包含 successcompleteddonetests passed 等,标记为 done
  • 当前命令是 claudecodexnodebunnpmzig 等,标记为 running
  • 当前命令是 zshbashfishnu 且没有明显输出,标记为 idle

这套规则不追求完美,只负责给手机端提供足够可用的 first signal。

发送输入

文本回复使用:

bash
tmux send-keys -t <pane_id> -l '<text>'
tmux send-keys -t <pane_id> Enter

快捷键使用:

bash
tmux send-keys -t <pane_id> C-c
tmux send-keys -t <pane_id> C-d
tmux send-keys -t <pane_id> C-[

Esc 按钮实际发送 C-[,比直接发送 Escape 更兼容 Claude Code / Codex 这类 TUI。

删除 stale session

详情页提供 Kill tmux session,后端调用:

bash
tmux kill-session -t <session>

前端必须先弹确认框,避免误删正在跑的任务。

API

GET /api/snapshot

返回当前所有 pane 的快照。

鉴权:

text
?token=<token>
Authorization: Bearer <token>

响应结构:

json
{
  "ok": true,
  "now": "2026-05-03T00:00:00.000Z",
  "panes": [
    {
      "id": "%4",
      "target": "work:0.1",
      "session": "work",
      "command": "codex",
      "path": "/Users/not/projects/devs/opensource/ghostty",
      "tail": "...",
      "status": "waiting",
      "reason": "looks like it needs input"
    }
  ]
}

POST /api/send

向 pane 输入文本。

json
{
  "paneId": "%4",
  "text": "继续",
  "enter": true
}

POST /api/key

发送快捷键。

text
/api/key?paneId=%254&key=C-c

允许的 key:

  • Enter
  • C-c
  • C-d
  • C-[
  • Escape
  • Up
  • Down

POST /api/session/kill

删除整个 tmux session。

json
{
  "session": "cx_ghostty_03a5805c"
}

cc / cx alias 改造

为了让 Codex 和 Claude Code 默认跑进 tmux,~/.zshrc 里的 cc / cx 从 alias 改成了 function。

目标行为:

  • 在当前目录执行 cc,进入或创建 cc_<目录名>_<路径hash> session。
  • 在当前目录执行 cx,进入或创建 cx_<目录名>_<路径hash> session。
  • 如果 session 已存在,直接进入。
  • 如果 session 不存在,先创建一个正常 shell,再用 tmux send-keys 发送启动命令。
  • 在 tmux 外执行时使用 attach-session
  • 在 tmux 内执行时使用 switch-client

示例:

bash
cd /Users/not/projects/devs/opensource/ghostty
cx

会创建或进入类似:

text
cx_ghostty_03a5805c

session 内启动:

bash
codex --yolo

cc 对应:

bash
claude --dangerously-skip-permissions

一个关键修正是命令拼接不能把整个命令整体转义成 codex\ --yolo,否则 zsh 会尝试寻找名字叫 codex --yolo 的命令。正确做法是逐参数 quote 后再 join。

为什么选择 tmux,而不是直接改 Ghostty

Ghostty 源码里确实有可以利用的事件:

  • start_command
  • stop_command
  • progress_report
  • desktop_notification
  • pwd_change
  • ring_bell

也能从终端模型里拿到当前 viewport 文本。理论上可以做 Ghostty-native remote status server。

但第一版选 tmux 更合适:

  • 不侵入 Ghostty,不需要维护 fork。
  • 对任何终端都有效,Ghostty、iTerm2、原生 Terminal 都能用。
  • 对 Claude Code / Codex 的真实运行环境更直接,因为 tmux 就是它们所在的 PTY。
  • 更容易通过手机发送输入和快捷键。
  • 更容易清理 stale session。

可以类比为:先在服务外层加一个观测和控制 sidecar,而不是直接改主框架 runtime。

访问与安全

推荐访问方式:

  1. 本机调试:127.0.0.1
  2. 同 Wi-Fi:开发机局域网 IP
  3. 跨网络:Tailscale
  4. 必要时:Cloudflare Tunnel

服务默认使用 token:

bash
AGENT_MONITOR_TOKEN="$(openssl rand -hex 18)"

手机 URL:

text
http://<ip>:8797/?token=<token>

注意:

  • 不建议无 token 暴露到公网。
  • send-keyskill-session 都是高权限操作,等价于远程控制你的 tmux。
  • 如果使用 Cloudflare Tunnel,应该再叠加 Access 或至少使用长随机 token。

当前限制

  • 状态识别是正则规则,不是严格状态机。
  • 没有持久化历史,刷新后只能看当前 tmux scrollback。
  • 没有多机器聚合。
  • 没有 push notification。
  • 没有 session 白名单,任何当前用户可见的 tmux session 都会展示。
  • kill-session 是整 session 删除,不是只关单个 pane。

后续方向

优先级较高的增强:

  • session/pane tag:给 pane 标记 ghostty-codexfrontend-claude 等名称。
  • waiting detector 增强:识别更多 Codex / Claude Code 的确认提示。
  • push 通知:等待输入、失败、完成时推到手机。
  • pane 级 kill:支持只关闭一个 pane,而不是整个 session。
  • 只读模式:手机端只观察,不允许 send-keys。
  • 多机器:多个开发机上报到一个统一 dashboard。

更长期可以考虑 Ghostty-native 集成,把 Ghostty 的 command lifecycle 和 viewport dump 作为更结构化的数据源。但在真实需求被验证前,tmux 方案已经足够覆盖主要工作流。

基于 MIT 许可证发布。