Skip to main content

tmux-claude-panel

基于 example/tmux_ai_coder_panel_src 改造的本地 Web 面板: 左边是 tmux 会话里跑着的 Claude Code,右边是那段会话的 jsonl 历史 + 实时屏幕 + 键盘输入。

一个进程、一个端口、零前端构建。

┌────────────────┬──────────────────────────────────────────┐
│ tmux · claude  │  e389d12e  [tmux: main] [837 行]     ⟳   │
│ 6 会话         │  ────────────────────────────────────────│
│ ▾ main ●       │   14:22  · claude-sonnet                │
│   ● e389d12e   │   ⚙ Bash · npm test                     │
│     修复播放     │   测试全过,271 个用例                    │
│   ○ bc0dca7a   │                                          │
│ ▸ tmp          │   ↑ 上滑加载更早                          │
│ ▾ 其他会话     │  ── 实时屏幕 ────────────────────────────  │
│   ○ 9c81df58   │  $ claude ...                            │
│                │  [⏎ Esc ↑ ↓ 1 2 y n Ctrl-C]              │
│                │  [输入指令…………………]  [发送]             │
└────────────────┴──────────────────────────────────────────┘

快速开始

cd panel
pip install -r requirements.txt
python -m tmux_panel                 # 127.0.0.1:20016,被占用自动 +1
# python -m tmux_panel --host 0.0.0.0   # 手机/局域网访问

两种传输,同一套帧语义

模式 打开方式 适合
REST 轮询(默认) http://host:20016/?transport=rest&interval=5000 无长连接、最省服务端;代理/网关/小程序都友好
WebSocket 推送(可选) http://host:20016/?transport=ws 局域网内最跟手,变化即推;连不上自动降级回轮询

右上角小徽章显示当前是 REST 5s 还是 WS。默认就是 REST 轮询(无长连接,最省 服务端),?transport=ws 才显式开长连接;WS 连不上时前端自动降级为轮询,不需要手动切。 轮询走 GET /api/index + GET /api/state,都带 ETag: 内容没变回 304 不带 body,实测 1s × 3 客户端从 2.29 MB/min 降到 193 KB/min。

服务端省资源的机制(与协议无关,详见 ../spec/SBP-transport.md): 共享抓屏缓存、索引只在 tick 里重建一次、tick 周期自适应(5s 轮询就按 2.5s 抓屏)、 扇出只序列化一次。诊断数据看 GET /api/stats。

首次启动会在当前目录生成 .env(随机账号密码并打印),登录后即可用。 --no-auth 可关闭登录,只在纯本机调试时用。

左侧出现会话的前提:

  1. 有 tmux 会话:tmux new -s work -c ~/codes/foo
  2. 该会话的工作目录里跑过 claude(~/.claude/projects/<slug>/<uuid>.jsonl 存在)

侧栏的深色 / 灰色(活跃判定)

一个 tmux 组里只有「当前正被 agent 用着」的那个会话是深黑 + 绿点,其余一律灰掉并挂 历史 角标;这个 tmux 里没跑 agent 时整组全灰。判定在 tmux_panel/live.py,三级证据:

级别 证据 含义
held pane 进程树里有进程打开着 <sid>.jsonl(lsof / /proc) 最硬:claude 正挂着这个会话
recent pane 里跑着 agent,且最新 jsonl 在 180s 内被写过 agent 在跑、刚有动静
guess agent 在跑但文件已冷 仍认最新的那个(claude 默认续最近会话)
none 没有 agent 全灰

dsh / codex 这类没有会话文件的 pty agent 同一套规则,只是证据换成进程: 按 pane 维护一条 term:<tmux>(多 agent pane 时第二个起是 term:<tmux>#<pane_id>), 进程树里此刻还有 agent 进程 → 深黑 + 绿点,副标题取自屏幕上的 task: 行; agent 已退出(pane 只剩 shell)→ 灰色 + 未激活 角标,但仍可点开看屏幕。 纯 shell 的 pane 不进列表。判定都在 tmux_panel/live.py 的 running_agent()。

索引每 6s 重算一次,指纹(活跃 sid / 数量 / 标题)变了才向前端广播; 前端正在编辑注释时暂存推送,编辑结束再补渲染。见 panel/docs/ui_active_grey.png。

它到底做了什么

能力 实现
列出 tmux 会话 扫 ~/.tmux/tmux-<uid>/ 与 /tmp/tmux-<uid>/ 下所有 socket,逐个 tmux -L <name> list-sessions(systemd 不 source shell rc 也能找到)
会话 ↔ claude 映射 tmux pane 的 pane_current_path → ~/.claude/projects/<slug>/;slug = 路径非字母数字全替换成 -
侧栏标题 claude session id(前 8 位),副标题默认取 jsonl 里的 aiTitle / lastPrompt
自定义注释 点条目上的 ✎ 改副标题,存在 ~/.tmux-claude-panel/annotations.json
懒加载 打开会话才请求 session.history{limit:20};上滑按 before=<行号> 继续翻
倒序分页 首次访问给 jsonl 建字节偏移索引(837 行 / 2ms,单页 0.3ms),之后按行号 O(1) 定位,不整文件重读
实时增量 1.2s 轮询被 attach 的 jsonl,size/mtime 变化就追加推送 session.message
实时屏幕 tmux capture-pane -p -S -40,变化才推 output.delta
输入 tmux send-keys -l -- "<text>" + Enter,特殊键走键名映射(Esc/↑/Ctrl-C…)

协议(SBP v0.1 子集)

完整协议规范(角色 / 帧 / 消息表 / 懒加载 / 安全 / 演进 + 多 agent 适配器映射)见 sbp-spec 仓库。

信封与 spec/SBP-0.1.md 一致,WS /ws:

→ {"v":"sbp/0.1","id":"c1","type":"req","name":"sessions.index","payload":{"refresh":true}}
← {"v":"sbp/0.1","type":"res","ref":"c1","name":"sessions.index","ok":true,
   "payload":{"groups":[{"name":"main","cwd":"/Users/mac","live":true,"items":[...]}]}}

→ {"name":"session.history","sid":"e389…","payload":{"before":818,"limit":20}}
→ {"name":"session.attach","sid":"e389…"}          // 订阅增量 + 屏幕
→ {"name":"session.input","sid":"e389…","payload":{"kind":"text","text":"继续","submit":true}}
← evt session.message / output.delta / session.state
req 说明
sessions.index 只要索引:sid / 标题 / 副标题 / 条数 / 时间 / 是否 live
session.history before 为行号(不含),返回 items / has_more / next_before / total
session.attach 返回 last_seq 与 tmux 归属,决定前端能否输入
session.input kind=text 或 kind=key
session.subtitle 写注释后广播新索引
pane.snapshot 主动抓屏

从原项目改了什么

保留:

  • TmuxManager 的多 socket 解析、path_to_session_name、鉴权(HMAC cookie + ?token= 兜底)、 find_free_port / .env 自动生成 / 启动横幅 —— 这些是踩过坑的地方,直接沿用。

替换:

  • Vue3 打包产物(frontend_dist,改不动)→ 换成 web/ 下无构建的三件套(html/css/js)。
  • WebSocket 语义:原来是「键盘 LED 面板」事件(keypress/set_mode), 现在是「会话流」协议(索引/分页/订阅/输入),并按 SBP 的信封规范命名。
  • 新增数据层:claude.py 负责 jsonl 发现、索引、分页、增量;hub.py 负责分组与推送。

没有改 example/ 下的任何文件,两边可以并存对比。

已知边界

  • 只做了 Claude Code 的 jsonl(~/.claude/projects)。换别的 agent 只需换 claude.py 的解析层。
  • 子代理消息(isSidechain:true)默认不展示,避免刷屏;需要时改 normalize()。
  • 一个 tmux 会话里如果切过多个 claude 会话,侧栏会把该目录下的会话全列出来(按 mtime 倒序), 真正"当前在跑"的那个排最前并带绿点。
  • jsonl 超过 8MB 时侧栏不显示条数(避免每次列表都全量扫行);打开后仍可正常翻页。

目录

panel/
├── tmux_panel/
│   ├── tmux.py      # tmux 访问(多 socket + send-keys/capture-pane)
│   ├── claude.py    # jsonl 发现/索引/倒序分页/增量/注释
│   ├── hub.py       # 分组索引 + 1.2s 推送循环
│   ├── server.py    # FastAPI + WS 协议
│   └── __main__.py  # CLI:端口探测 / .env / 横幅
└── web/
    ├── index.html
    ├── style.css
    └── app.js

更新日志

  • 1.0.8 — 非 claude agent 也能翻历史了(qwen code 实测可用)。以前面板只认 ~/.claude/projects/<slug>/<uuid>.jsonl,qwen code 这类「其实也把对话落盘成 jsonl」的 CLI 被一律当成纯终端,页面上只有「没有历史文件,下方是实时屏幕」。 现在历史源由适配器声明驱动:adapters/*.yaml 加一段 history:,面板据此把 正在跑的进程绑到它的会话文件(qwen:--resume <uuid> / chats/*.runtime.json 里的 {pid, session_id} / cwd 目录下最新一个)。 同时修了两处:qwen 的正文在 message.parts(不是 content),已归一; 分页改为按消息条数(qwen 的 jsonl 里 system 快照占了绝大多数,按行翻会翻出半页空)。
  • 1.0.7 — 修复挂在 nginx 子路径下白屏:页面里的静态资源/API/WebSocket 以前是写死的 根绝对路径(/static/...、/api/...、/ws),挂在 https://host/xxx-panel/ 这类 反代子路径下时,浏览器会跑到站点根去找资源,结果就是 CSS/JS 全 404、页面全白。 现在面板自查 PANEL_BASE 环境变量或反代下发的 X-Forwarded-Prefix 请求头, 把前缀注入页面(window.__PANEL_BASE__),app.js / sidebar.html / qwapp.html 一律按前缀拼 URL。直接跑在站点根时行为不变。
  • 1.0.6 — 修复 Linux 下 /api/index 返回 500:进程树遍历里 stack.extend(kids.get(cur)) 在节点没有子进程时拿到 None 会抛 TypeError: 'NoneType' object is not iterable(live.py::_tree)。 已改为 extend(... or [])。
  • 1.0.5 — PyPI 描述与仓库互链(sbp-spec)。
  • 1.0.4 — 统一适配器声明(aider / opencode / qodercli),多 CLI 识别以 sbp-spec/spec/adapters/*.yaml 为唯一真相源;新增 CORS 与 SBP4AI 品牌。

多 CLI 的历史:适配器里声明 history

面板默认只认 claude 的 jsonl。要让别的 CLI 也能翻历史,在它的适配器声明里加一段:

history:
  root: "~/.qwen/projects"                 # 历史文件根
  glob: "*/chats/*.jsonl"                  # 相对 root 的匹配
  cwd_dir: "{slug}/chats"                  # 兜底:cwd 对应的目录({slug} 与 claude 同规则)
  id_from_cmdline: '--resume\s+([0-9a-fA-F-]{8,})'   # 从进程命令行抠 session id
  runtime: {glob: "*/chats/*.runtime.json", pid: pid, sid: session_id}  # pid -> sid 映射
  format: parts                            # 正文在 message.parts(claude 用 content)

没写这段的 agent 保持原样:只用实时屏幕(kind: term),不会误伤。

挂在反代子路径下(nginx)

面板本身始终提供根路径的服务;反代剥掉前缀后请求照样落在 /、/api/*、/ws、 /static/*。唯一要做的是告诉面板它对外叫什么前缀,它会据此生成页面里的 URL:

方式 用法
请求头(推荐,一个实例可同时服务根和多个前缀) proxy_set_header X-Forwarded-Prefix /ecs-coder-panel;
环境变量(固定前缀) PANEL_BASE=/ecs-coder-panel/

参考 nginx 配置见 sbp-deploy/nginx/sbp-panel-subpath.conf。不再需要早先那套 sub_filter 字符串替换(/src/、/node_modules/、'/api …)——那是给旧 vite 前端用的, 留着反而会改写坏新面板的输出。

Metadata

Release files for tmux-ai-coder-panel 1.0.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tmux-ai-coder-panel 1.0.8
File Size Uploaded Provenance Actions
tmux_ai_coder_panel-1.0.8.tar.gz 70.1 kB Without provenance Details for tmux_ai_coder_panel-1.0.8.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tmux-ai-coder-panel 1.0.8
File Interpreter ABI Platform Actions
tmux_ai_coder_panel-1.0.8-py3-none-any.whl Python 3 none any Details for tmux_ai_coder_panel-1.0.8-py3-none-any.whl

Total release size: 149.1 kB

Release files / tmux_ai_coder_panel-1.0.8.tar.gz

Download URL tmux_ai_coder_panel-1.0.8.tar.gz
Size 70.1 kB
Tags Source
SHA-256 checksum
How to use checksums
77c0b0f5ca6a69d285357894cdda5476a6a072178573a250259ca4db124272b8
BLAKE2b-256 checksum
How to use checksums
f23b1bd04da3325c2d2249dccefc138d47c2cc6feee6a4b326c6e7d26a4bdab4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / tmux_ai_coder_panel-1.0.8-py3-none-any.whl

Download URL tmux_ai_coder_panel-1.0.8-py3-none-any.whl
Size 79.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cd77bbbb77c5920b8f279567d447ae15c67de09f4b8b339ade67c621775eb16f
BLAKE2b-256 checksum
How to use checksums
df018ab7a910ddb1d38cedd90181fb41751fffed7f6d068a905cb9de6586a5bd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Security for tmux-ai-coder-panel 1.0.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page