keepane,AI 时代的终端多路复用
Rust终端AgentMCP终端复用器是给坐在键盘前的人写的,分屏、切窗口、断线重连,前提都是有个人在看着。现在 pane 里跑的常常是 agent,人不在跟前,要的东西就变了。
keepane 是按后面这个前提写的。每个 pane 有名字、有收件箱、有工作模式,pane 之间能互发消息,也能让对方直接执行一条命令;同一台机器上如此,配过对的两台电脑之间也一样。人、脚本和 agent 走同一套,agent 那侧还能走内置的 MCP 服务端。
终端断了 pane 里的程序照跑,跑完了手机、桌面或者 webhook 会告诉你;扫个二维码就能在手机上看每个 pane 在干什么、往里打字。agent 用了多少 token、花了多少钱、上下文还剩多少,也都摆在面上。tmux 的键、命令和配置在这里照用。
起因
一开始是想找个现成的用,找了一圈没有合预期的。能用的那些都还是按「有人看着」写的,pane 里跑的是什么它不关心,更不会让两个 pane 说上话。于是自己写,第一个提交的标题是「wmux: tmux-style terminal multiplexer for Windows」,那会儿还叫 wmux。
真用起来才发现分屏解决的只是摆放。几个窗口里各跑一个 agent,想让它们配合起来,眼下就两条路。一条是用各家 agent 自己的协议,一家一套,谁也不认谁,换个工具就得重接一遍。另一条是用 agent 内部那套派子任务,它跑在一轮之内,不占 pane、看不见、也接管不了,断了就断了。
那就往下挪一层。不管跑的是哪家 agent,还是一个普通 shell、一个脚本,它都坐在一个 pane 里,pane 是大家都有的那层东西。通信放在这一层,给每个 pane 一个名字和一个收件箱,里面跑的是谁就不重要了。
pane 于是从一块屏幕变成了一个 actor。改名也是这时候的事,wmux 这名字 GitHub、winget、crates.io 上都有人在用了,0.14.0 起改叫 keepane,keep 加 pane,终端没了 pane 里的程序还在。
消息
给 pane 起个名字,它就能收消息:
keepane rename-pane -t %3 builder
keepane set-work-mode -t %builder shell # 指向别的 pane,得在 keepane 外面或者 C-b : 里运行
keepane send-message -t %builder -w 30 "cargo test"
keepane trace-message 12 -w 600-w 是等送达,trace-message 是等做完,回来的是输出和成败。

发送方能填的只有 --to、--re、--task 三样,其余由服务端盖章:
[keepane id=12 task=12 from=$1:@1.%3 name=lead mode=ai to=$1:@2.%7 via=shell hop=0]from、name、mode、id、hop、via 都不许发送方给,给了报错。来源能伪造的话 hop 上限就拦不住循环,两个 agent 能一直对着回信,现在超过 8 跳拒收。
字段顺序固定、中间不留空格,同一条消息每次生成的字节完全一样,写测试和给程序解析都省事;值里不会出现空格和 ],按空格切开就读回来了。这行原本是 JSON,用了一阵实在看不下去,0.17 改成字段式,JSON 只留在事件日志里。
工作模式
投递时机由收件方决定,一共三种:
normal:不自动投递,程序自己用read-message取,窗口标记上显示@shell:keepane 的提示符钩子看到 shell 回到提示符,就把命令打进去回车ai:agent 的轮次结束 hook 调过pane-ready,就把正文当提示词打进去
投给 shell 的那条,信封写成不执行任何东西的形式放在命令前面,PowerShell 里是行内注释 <# … #> cargo test,bash 和 zsh 里是 : 的参数。这样来源同时留在屏幕、shell 自己的历史和 keepane 的历史日志三处。
消息按发送当时对方的模式投递,不按现在的。写给 agent 的一段文字,不会因为这个 pane 中途被切成 shell 就被当命令执行。
空闲判断
什么时候算「这个 pane 腾出手了」,只认两个信号:shell 模式认 keepane 自己装的提示符钩子(OSC 7777;keepane-prompt),ai 模式认 agent 每轮结束时调的 pane-ready。ssh 到远端的 shell 也会发 OSC 133,那个不算,免得把别人机器上的提示符当成本地空闲。
按键、粘贴、send-keys 一律转成忙,消息不会插进正在打的字中间。提前打字也照顾到了:keepane 投的命令还在跑、人先敲了半行,这条命令结束时的提示符不算空闲,状态机里另记一位「行上留着东西」,一直等到有人把那行运行掉或者清掉。
还有一层是 ConPTY 的时序。提示符标记是个 OSC,ConPTY 立刻转发,它前面那些文字要等下一帧才画出来,于是标记到达时命令最后几行输出还不在屏幕上。所以标记到了先判在提示符,当前消息再等 60 ms 才算结束、才截输出,这期间不投递新的。截输出也不靠推算行数,而是从送达那行往下找到命令末行,输出从下一行起。
跨机器
pane 里的 agent 不一定都在一台机器上。Mac 上挂着 agent,编译和测试在 Windows 那台跑,这之前想让两边搭上话,得自己 ssh 过去看一眼再回来转述。0.21 起收件箱能跨机器了,同一个局域网,或者 Tailscale 这种私有网络。
两台都开着 keepane web,拿另一台打印出来的那个地址(手机扫的就是它,带着密钥),在这台上配一次对:
keepane link add http://100.64.0.3:7681/#k=... # 配对,一次双向生效
keepane link list # 配过对的机器
keepane link panes 100.64.0.3:7681 # 对方的 pane
keepane send-message --to 100.64.0.3:7681/%worker "cargo test"跟 ssh-copy-id 是一回事。web 密钥只在这一下用来证明身份,之后每个请求走两边各自的 Ed25519 密钥签名,对方的 web 后来换了密钥也不影响。表里记的是公钥,地址只是顺带记着,所以同一台机器从局域网挪到 Tailscale 再来,验签过了照收,那一行的地址自己更新。
地址写成 主机:端口/ 再跟上它在那台机器上的写法,%worker 或者 $1:@3.%7 都行。信封到那边是 from=100.64.0.3:7681/$1:@1.%3,主机那一段由收件方按验过的公钥填,发件方写不了。对方 send-message -r 回过来,落在这边发起的那个 pane 上,归进同一个任务。-w 由对方那台等到投递,超时退出码非 0,和发给本机 pane 一样。
对方的机器情况、自己那条消息的下落,也都能问:
keepane link info 100.64.0.3:7681 # 主机名、系统、版本、开机多久、CPU、内存、pane 数
keepane trace-message 12 -w 600 # 发过去那条在那边成没成、输出了什么
keepane link capture -S 100 100.64.0.3:7681/%worker # 对方那个 pane 现在显示什么,要它授权 --screen配完对,对方的消息只进 ai 和 normal 的 pane。想让它在 shell 里当命令执行,这边得单独给它开一次:
keepane link allow 100.64.0.3:7681 --shell这条和 set-work-mode 同一个规矩,在 pane 里面运行会被拒,得在 keepane 外面的终端或者 C-b : 里。不然 pane 里的 agent 自己就把远程执行的权限开了。--screen 也一样。
剩下几条是照着「有人存心捣乱」定的:
签名签进去的内容里含收件方的公钥,一条请求截下来拿到别的机器上用不了,答复也签
时钟差过两分钟不收,同一个随机数 240 秒内再来一次也不收
对方送来的from、name、mode落地前要按本机的字段规则过一遍。少了这一关,一台改过的机器可以在信封里塞出一行,看着像是本机 pane 发来的
同一个地址一分钟只记一次被拒。局域网上有人灌垃圾请求的时候,状态栏不至于刷屏,当天的事件日志配额也写不满
连不上就当场报错,不排队也不等以后补发,隔了半小时才执行一条过时的命令比失败麻烦keepane web --read-only的机器什么远程消息都不收
对方再开一道 --panes 授权的话,还能直接在它那台机器上开 pane:
keepane link start 100.64.0.3:7681 -n helper -- claude
keepane send-message --to 100.64.0.3:7681/%helper "看一下这个挂掉的测试"
keepane link kill 100.64.0.3:7681/%helper程序得在对方的 agent-commands 白名单里,参数不限。这里有个要想清楚的地方:白名单里要是留着 shell,--panes 就等于顺带给了执行命令的权限,因为 shell 收到什么就跑什么。只想让对方开 agent 的,在那台上把白名单收窄成 set -g agent-commands "claude codex"。数量照样受对方的 agent-pane-limit 管,按发起方单独计数;开出来的 pane 收在以这台机器命名的 session 里,也只有这台机器关得掉。keepane web 现在会同时监听局域网地址和本机的 Tailscale 地址。每条决定为什么这么定,写在 link.md 里。
MCP
只想让 pane 之间通通气,不需要 MCP。agent 那侧装一个 hook 就够:每轮结束时跑一下 keepane pane-ready -q,这个 pane 在 ai 模式下就算腾出了手,排队的消息这时候投进去;它要回话,命令行的 send-message 拿来就用,-r 回给手上这条消息的发送方,--re 12 回给第 12 条的。这条路上 agent 只管自己这一个 pane。
MCP 是给另一件事准备的,让 agent 自己去编组。keepane mcp 是个 stdio 服务端,从 KEEPANE_PANE 认出自己在哪个 pane,之后它可以开新的 session、新的窗口、新的 pane,开的时候就把名字、工作模式和第一条消息一并给上,pane 就绪后自动投递,省掉「发早了」的时序问题。开完了还能看谁忙谁闲、翻任务链、给自己上报进度,整套 keepane 的 session 由它自己管着。
keepane setup # 认识哪几家、本机装没装、配好没有
keepane setup claude --install # 替你写进去,改之前先备份现在认五家:Claude Code、Codex、Gemini CLI、Cursor CLI、opencode。各家的 hook 和 MCP 配置落在各自的地方(~/.claude/settings.json、~/.codex/hooks.json、~/.gemini/settings.json、~/.cursor/hooks.json,opencode 是一个插件文件),setup 按各家文档写,只加 keepane 自己那几条,不是纯 JSON 的文件(带注释那种)不动,改为告诉你手工加什么。端到端实测过的只有 Claude Code。
自己手写 hook 记着程序路径别加引号:Claude Code 在 Windows 上可能用 PowerShell 跑 hook,带引号的路径后面跟参数是语法错误。hook 在 keepane 外面是静默的,不在 ai 模式的 pane 里也不管事。别家只要能在每轮结束时跑条命令、能用 stdio 的 MCP,照样能接。
还有一类情况值得先说:agent 启动以来从没报过空闲,发给它的消息就会一直排队。send-message 当场说明并给出补 hook 的 setup 命令,dashboard 和手机页上也标出来。
二十四个工具,分六类:
create_session、create_window、split_pane、rename_pane、kill_pane:自己开一队 pane 出来,用完收掉whoami、list_panes:自己是谁,别人都在干什么、忙不忙、收件箱里积了几条send_message、reply、wait_message、current_message:派活、回信、在自己一轮里等下一条、查服务端记的当前消息list_messages、trace_message、drop_message、move_message:翻收件箱,查一条的下落,插队或者删掉set_status、set_work_mode、list_tasks、show_task、query_events:上报进度,翻任务链和事件日志
list_links、link_info、read_screen、create_remote_pane:配过对的机器、某台的情况、某个 pane 的屏幕,以及在另一台上开 pane(list_panes带host看那台的 pane,kill_pane带地址关那边的)
新 pane 跑 claude、codex、gemini、cursor-agent、opencode 默认进 ai 模式,跑 shell 默认进 shell 模式。能起的程序限在 agent-commands 那几个名字里,一个 agent 连同它开出来的 pane 最多再开 8 个。还有一条:set-work-mode 在 pane 里运行时只改得动这个 pane 自己,指向别人会报错,所以一个 agent 没法把别人的 pane 变成「收到什么就执行什么」;人在 keepane 外面的终端、快捷键或者 C-b : 里运行,才能改任意 pane。
用量与花费
几个 agent 并排跑着,哪个在烧钱、哪个上下文快满了,原本得挨个窗口自己翻。
keepane 认 pane 里跑的是不是 agent,现在认 Claude Code、Codex 和 pi(看程序名,node 起的看它在跑什么)。这几家都会给工作目录写一份会话记录,keepane 每 2 秒读一次,只读上次之后新写的那一段,从里面拿到模型、用掉的 token、调了几次工具、几轮回复、上下文占了多少;CPU 和内存另从进程表读,agent 自己开的子进程一并算上。
花费这块 pi 自报,直接用。其余按 LiteLLM 的公开价目表 给每个模型的新输入、读缓存、写缓存、输出分别计价,表一天下一次,只在真有 pane 跑 agent 时才下。
这个数的口径要说清楚:表上是各家的 API 标价,所以它答的是「同样这些活走 API 要花多少」,订阅制(Claude Max、ChatGPT Pro 之类)另有自己的算法,对不上是正常的。表里没有的模型不计价,花费后面带个 +,意思是至少这么多。set -g agent-cost off 连表都不下。
set -g pane-border-status top
set -g pane-border-format " #{pane_index} #{?agent,#{agent_model} #{agent_cost} ctx #{agent_context},#{pane_current_command}} "dashboard 的 pane 列表里是 claude $2.10 34%,标题上是总花费;选中哪个,上面一行把它的全部数字摊开。手机上 agent 的卡片多一行。给脚本用的是 keepane list-agents,-J 出 JSON,不知道的字段是 null。
一处边界:它只读 keepane 自己 pane 里的 agent,但要是另一个终端或者编辑器里的 agent 正好在同一个目录干活,记录写进同一个文件夹,就会被算到这个 pane 头上。
完成就通知
人走开了,总得有个东西把「跑完了」送到眼前。keepane 认四种完成:一条跑了一会儿的命令结束、agent 这一轮结束、一条消息的任务走完、程序退出。
手机页开着的时候顶部弹横幅,点一下跳过去,响一声,安卓还震一下,标签页标题上记未读数;页面在后台时完成的,切回来补告诉你
set -g notify on走系统桌面通知set -g done-webhook <地址>往外发一条,done-webhook-format选格式:飞书、企业微信、钉钉、Slack、Discord,或者text(ntfy、Bark 这类只要一行字的)
想接别的服务就挂pane-donehook,这次完成的信息都在环境变量里:KEEPANE_DONE_KIND、KEEPANE_DONE_TEXT、KEEPANE_DONE_OUTPUT、KEEPANE_DONE_OK、KEEPANE_DONE_SECONDS,还有一份KEEPANE_DONE_JSON
哪些算完成、对哪些 pane 生效,都能调:done-events 挑事件,done-after 30 要求命令至少跑够 30 秒,done-panes named 只管起过名字或者 ai/shell 模式的,done-lines 5 顺带把 pane 最后几行输出捎上。同一个 pane 三秒内只响一次,agent 这一轮结束和它完成的任务算同一次。
手机页那条有个物理限制得讲明白:普通 HTTP 的页面发不了系统通知,锁屏或者切走之后浏览器会把页面暂停。所以锁着屏也要收到的,得走 hook 或者 webhook。
手机
keepane web 在终端里打一个二维码,手机扫了是一个列出所有 pane 的页面,按 session 和窗口分组,标着每个跑的什么程序、有没有提醒标记。点进去看屏幕、带颜色,屏幕一变就推过来,不用刷新。

卡片上那行大字是 pane 的名字,没起名就退到程序自己设的终端标题,再没有才显示程序名;下面一行是在跑什么、最后输出的一行、多久没动静,还有一个小圆点表示空闲还是忙、收件箱里排着几条。不点进去就知道哪个跑完了。
底下输入框是边打边发的:你在框里打字那边就跟着出,删字那边也跟着删,输入法选好字才发,回车就是回车。嫌它太激进,视图菜单里可以改成等回车再发,或者点一下「发送」只送文字不带回车,每台设备各自记住。上面一排是手机键盘没有的键,Ctrl+C 和回车钉在最右边不会被滚走;先点 Ctrl、Alt 或 Shift(可以几个一起按住)再点别的键,就是带着修饰键按下。
shell 和 ai 模式的 pane,输入框旁边有个信封按钮,按下去发的就不是按键而是消息,进这个 pane 的收件箱排队,跟 send-message 同一条路。长按卡片或者点「排队 N 条」能把收件箱打开:正在处理的那条、排队每条的全文,可以置顶、上移、删掉,删了还能撤销。
手机屏幕行太少,vim、htop、agent 这些占满屏的程序摊开就没法看。视图菜单里的「适配这块屏幕」让当前 pane 占满所在窗口,整个会话调成手机的行列数,这些程序按手机的大小重画一遍。一个会话只有一个大小,所以适配的这段时间,电脑跟前和别的手机上看这个会话也是小的,页面和状态栏都会说一声。离开这个 pane、退回列表、锁屏都会恢复,没人看了 10 秒 keepane 自己也会恢复。
页面在 0.25 整个用 React 和 HeroUI 重做了一遍,打包成一个文件编进二进制,所以电脑浏览器里也是同一个页面:宽屏时左边列表右边 pane,列表能收成一条窄栏。右上角那个菜单管页面外观(日间、夜间、跟随系统)、终端主题(选了电脑上的终端跟着变)和页面语言(跟随系统、中文、英文),旁边实时显示到电脑的往返时间,100 ms 以下绿点,300 ms 以下黄点,再高就是红点和「离线」。还能在输出里查找、复制屏幕文字、双指缩放调字号;退出过的 pane 写出退出码,点「重新运行」再起一次。
二维码里带着地址和一把每次启动新生成的 128 位随机密钥,除了页面本身,没这把钥匙什么都不回。起来之后是服务端在后台供着,运行 keepane web 那个终端就空出来了,web status 看谁连着、在看哪个 pane,web stop 掐掉。--read-only 只让看不让打,--keep-key 让二维码每次一样、书签不失效,写 web-start -k 进配置就是开机自己起。自己家网络直接连,在外面就在中间放一层 Tailscale 这类私有网络,绑到它的地址上。
日常
剩下这些是为「人不在跟前」配的:
C-b v的 dashboard 把所有 pane、选中那个的收件箱、任务摆在一屏里,右边是它的实时屏幕、滚动历史或者事件,能直接发消息、改模式、解卡、跳过去
重启电脑之后keepane resume把 session、布局、每个 pane 的目录和原命令摆回来,屏幕上最后 500 行也回放(save-history all连整个滚动历史一起);Windows 上keepane startup on让它开机自己起,Linux 和 macOS 先写进自己的登录脚本C-b u十秒内找回手滑关掉的 pane 或者窗口,程序还在跑C-b q每块显示自己的编号和工作模式,按数字直接跳过去(两位数的编号也认,按到第一位能唯一确定就走),起过名字的在编号下面显示%名字C-b y把上一条命令的输出取下来,C-b [之后用[]在命令之间跳;C-b F给屏幕上所有 pane 里的路径、网址、git hash 标上字母,按小写复制,按大写打开(路径有code就用 VS Code 跳到那一行)
前缀按下去之后手停了一会儿,自动弹一个面板告诉你这些键都干什么theme tokyo-night/theme tokyo-day是内置主题,手机页上选了电脑的终端跟着变C-b s的会话树改成了方块图,hjkl移动,a全展开,配过对的机器也画在里面C-b C-t在每条命令那行的行尾空白处标出开始时间、耗时和成败,不改 pane 宽度,也不进capture-paneC-b /按 pane、按天翻它打印过的东西,一天一个文件留 30 天,C-b B是任务板,一行一个 pane,能跳转、关掉、重启
后台窗口有动静、响铃或者太久没声音,状态栏标出来,C-b M-n跳过去;set -g notify on之后直接发系统通知,Windows 上的 toast 带一个「跳到 pane」的按钮
装和用
Windows 上 scoop 直接读仓库里的清单,不要管理员权限,另外有 MSI 和 zip;Linux 和 macOS 走 Homebrew。
scoop install https://raw.githubusercontent.com/newdee/keepane/master/packaging/scoop/keepane.jsonbrew install newdee/tap/keepanekeepane # 新建 session 并进去
keepane new -d -s bg htop # 后台起一个
keepane attach -t work # 换个终端窗口也能接回来
keepane resume # 重启之后把存过档的 session 都摆回来日常那半边不用重新学:前缀 C-b,% " 分屏,[ 进 copy mode,: 命令行,format 串、hook、插件、配置文件的写法照旧,keepane import-config 把现成的 .tmux.conf 过一遍就能接着用。tmux 3.5 的命令表没有落下的,逐条对照在 tmux-parity.md。
实现
Rust 写的,一个二进制既当客户端也当服务端。第一次运行拉起一个脱离终端的服务端进程,所有 session 归它管,客户端用命名管道(Windows)或者 Unix socket 连上去。每个 pane 是一个伪终端加一份 vt100 屏幕模型,服务端把可见的 pane、边框和状态栏合成一帧,只把变了的格子发给客户端。
主循环有 16 ms 的帧率上限。4 个 pane 满速刷屏、挂着一个客户端的时候,服务端 CPU 在一个核以内,同样负载下没有这个上限是 166% 核;按键仍然立刻画出来,不加打字延迟。
平台相关的都收在 src/platform/ 下面,Windows 走 ConPTY、命名管道、job object,Linux 和 macOS 走 pty、Unix socket、进程组和信号,其余代码两边共用,三平台 CI 每次提交跑全量。
剩下的都写在 README 和 keepane man 里了,也挂了一份功能一览。手机那页做完之后,我最常拿它干的事是躺着看编译跑完,大概不算什么好习惯。