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 是等做完,回来的是输出和成败。另一种等法不盯消息盯屏幕:wait-output -t %server "Listening on" 堵在那里,直到那个 pane 打出含这段字的一行,把这行给你。

发送方能填的只有 --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,那个不算,免得把别人机器上的提示符当成本地空闲。
ai 这边后来多了一条不依赖钩子的路:Claude Code、Codex 和 pi 都往会话记录里写这一轮什么时候开始、什么时候结束,keepane 本来就每 2 秒读这个文件算花费,顺手也就知道了谁闲下来。没装钩子最多晚三秒,装了立刻生效。这条路还补上了钩子够不着的一种情况:人按 Esc 打断,钩子根本不会跑,记录里却留着,于是 pane 照样转空闲,手上那条消息记成没做完。
pane 现在用一个词说自己在什么状态(#{pane_state},dashboard 和手机上都显示):idle 闲着、busy 在干活、typing 有人打了字还没回车、no-agent 是 ai 模式的 pane 退回了 shell 提示符、unknown 是从没发出过提示符标记的 shell(cmd 那种)、exited 程序已经退了。
按键、粘贴、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 里,也只有这台机器关得掉。
再往后一步是把代码也送过去。消息能跨机器之后,很快就撞上「那边那台看不到我刚写的提交」这件事,于是加了 clone 和 push,走同一条配过对的链路,不用在中间架 git 服务:
keepane clone 100.64.0.3:7681 # 在对方建一份副本
keepane push # 以后:把当前分支的新提交送到上次那个位置
keepane push --tell %worker # 送完顺手给那边的 pane 发一条消息只送已提交的东西,打成 git bundle 分块传,对方核对整个文件的 SHA-256 之后才交给 git。落在对方的 <本机名>/<分支> 下;能快进、分支正检出着、跟踪的文件又没改动,才顺带把工作区一起更新,否则分支按兵不动并回话说明原因,让那边自己决定合还是变基。本机记的是对方的公钥不是地址,所以对方换了网络照样找得到。未提交的文件不送,回复里写个数;浅克隆直接拒绝,因为它没有早期历史。
提交送过去了,对话还留在原来那台。Claude Code、Codex、pi 的对话只存在它们跑过的那台机器上,换台电脑就得把背景从头讲一遍。send-agents 把项目连着对话一起送:
keepane send-agents 100.64.0.3:7681 -n # 先看看会送什么
keepane send-agents 100.64.0.3:7681 # 送这个项目和它的对话
keepane send-agents # 以后:只送变了的,送到同一个文件夹在仓库里运行就按 push 那套送提交,再加上 git 列出来的改动和新文件,被忽略的不送;不是仓库的文件夹整个送,跳过 node_modules、target 这类生成目录。对话里的 cwd 改成对方的路径,第一次落在它的 ~/keepane/from/<本机名>/<项目名>,回复的最后直接写出在那边怎么接上:
go on there: cd /home/me/keepane/from/laptop/app
claude --resume 9516c53b-ede9-4566-9d6a-76117964e69bWindows 上开的头,送到 WSL 里用 Claude Code --resume 接着聊,这条路走通过。再送只送新增的那一段。对方的东西不会被盖掉。它改过的文件原地留着,送过去的那份摞在旁边叫 <文件名>.from-<本机名>;它也接着聊过的那段对话,换个新 id 单独放。这件事和 clone、push 一样要对方单独放行,keepane link allow <本机> --agents 和 --push 各开一次。对话里什么都可能有,agent 读过的文件、跑过的命令和输出,而 link 的请求只签名不加密,所以只在 Tailscale 或者自己信得过的网里用。keepane web 现在会同时监听局域网地址和本机的 Tailscale 地址。每条决定为什么这么定,写在 link.md、link-push.md 和 agent-sync.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,照样能接。
上面那三家靠会话记录也能判空闲,所以 hook 现在更像是「让它快三秒」而不是必需品。其余的 agent 还是得装,不然从没报过空闲,消息就一直排队;这种时候 send-message 当场说明并给出补 hook 的 setup 命令,dashboard 和手机页上也标出来。
0.52 起 setup --install 还装第二个 hook:agent 要权限、或者问了你一句话的时候跑 keepane pane-wait,通到下面那节的「等你」。Claude Code、Codex、Gemini CLI 有这个口子,Cursor 和 opencode 没有。之前装过的再跑一次 setup <agent> --install,缺哪条补哪条。
二十七个工具,分六类:
create_session、create_window、split_pane、rename_pane、kill_pane:自己开一队 pane 出来,用完收掉whoami、list_panes:自己是谁,别人都在干什么、忙不忙、收件箱里积了几条send_message、reply、wait_message、current_message、wait_output:派活、回信、在自己一轮里等下一条、查服务端记的当前消息,以及堵着等某个 pane 打出某段字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、push_repo、list_ssh_hosts:配过对的机器、某台的情况、某个 pane 的屏幕,在另一台上开 pane(list_panes带host看那台的 pane,kill_pane带地址关那边的),把当前分支的提交送过去,以及这台机器 ssh 去过哪些地方
新 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 和 Cursor CLI(看程序名,node 起的看它在跑什么)。Cursor 的对话 keepane 读不了,所以它只有 CPU 和内存两栏。这几家都会给工作目录写一份会话记录,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 头上。
一步步在做什么
花费和上下文是总量,不说它这一轮到底在干什么。同一份会话记录里连每步的起止时间都写着:一轮从你提问到它最后一次回复,这一轮里每次调模型、每次动工具,各自什么时候开始、跑了多久、有没有失败。dashboard 右边按 T 切到 Trace,把这些画成瀑布图,每 2 秒重画一次。

模型品红,读和搜青色,改文件黄色,跑命令蓝色,子 agent 绿色,别的工具不着色,失败的红色并在前面标 ✗;还在跑的那根加粗,末尾一个 ▶。模型调用写明是哪个模型,命令按原样跟在 $ 后面,文件只留路径末尾,Claude 的子 agent 缩在启动它的那次调用下面。每一轮拿你那句提问当名字;不是你开口的那轮,就用开头那件事命名:后台任务做完了、! 命令、定时任务、别的会话发来的消息。默认只摊开最新一轮,按 e 全摊开。一轮结束之后,那行写它已经等了你多久。
会话记录是一件事做完才落一行,所以这次模型调用要等回复写下来才出现。一轮还没完、手上又没有工具在跑,那就是模型从上一步起一直在想,这段写成 thinking。给脚本的是 keepane trace-pane -t <pane>,打成一棵树,-J 出 JSON,每步带类型、名字、参数摘要、起止毫秒和父步骤。
完成就通知
人走开了,总得有个东西把「跑完了」送到眼前。keepane 认六种完成:一条跑了一会儿的命令结束、agent 这一轮结束、agent 反过来在等你、一条消息的任务走完、程序退出、没有 agent 的 pane 响了铃。
手机页开着的时候顶部弹横幅,点一下跳过去,响一声,安卓还震一下,标签页标题上记未读数;页面在后台时完成的,切回来补告诉你
set -g notify on走系统桌面通知set -g done-webhook <地址>往外发一条,done-webhook-format选格式:飞书、企业微信、钉钉、Slack、Discord,或者text(ntfy、Bark 这类只要一行字的);飞书、钉钉的机器人开了签名校验的,把密钥写进done-webhook-secret
想接别的服务就挂pane-donehook,这次完成的信息都在环境变量里:KEEPANE_DONE_KIND、KEEPANE_DONE_TEXT、KEEPANE_DONE_OUTPUT、KEEPANE_DONE_OK、KEEPANE_DONE_SECONDS,还有一份KEEPANE_DONE_JSON
「等你」是 0.52 加的那种,走 setup 装的第二个 hook:agent 要权限、或者问了你一句话就跑 keepane pane-wait,通知写成 waits for you: Claude needs your permission,底下把问题和那几个选项一并捎上,所以在手机上扫一眼就知道该回哪个。Claude Code 要等问题挂了六秒左右没人理才触发,免得你本来就坐在跟前。别的程序也能借这条路喊人:keepane pane-wait "要部署到生产环境吗?"。响铃那种默认不开,只管没有 agent 的 pane,同一个 pane 一分钟最多响一次。
agent 那一轮的通知里还带一段它在仓库里干了什么。起点取这轮开始时仓库所在的提交,结束时比一次:
git:
main 1a2b3c4..5d6e7f8: 1 commit, 2 files +40 -12, 1 new file
5d6e7f8 parse the header
src/parser.rs +38 -12
new notes.md这段同时出现在 trace-message 里,所以派活的那个 pane 用 -w 等结果时会一起拿到,不用再去问「你到底改了什么」。它统计的是工作区在这段时间里的全部改动,不分是谁改的,所以两个 agent 共用一个工作区就会混在一起,要分清得给每个 agent 一个 git worktree。起点提交被 rebase 掉了就从结束时算起并注明 (start unknown)。
顺带,pane 目录在 git 仓库里的话,分支名后面常驻一行 main ●3 ↑2 ↓1 !1:改了 3 个文件、领先 2 个、落后 1 个、1 个冲突,没有的项不显示。
哪些算完成、对哪些 pane 生效,都能调:done-events 挑事件,done-after 30 要求命令至少跑够 30 秒,done-panes named 只管起过名字或者 ai/shell 模式的,done-lines 5 顺带把 pane 最后几行输出捎上。同一个 pane 三秒内只响一次,agent 这一轮结束和它完成的任务算同一次;keepane list-done 翻最近一百条。
手机页那条有个物理限制得讲明白:普通 HTTP 的页面发不了系统通知,锁屏或者切走之后浏览器会把页面暂停。所以锁着屏也要收到的,得走 hook 或者 webhook。
手机
keepane web 在终端里打一个二维码,手机扫了是一个列出所有 pane 的页面,按 session 和窗口分组,标着每个跑的什么程序、有没有提醒标记。点进去看屏幕、带颜色,屏幕一变就推过来,不用刷新。

卡片上那行大字是 pane 的名字,没起名就退到程序自己设的终端标题,再没有才显示程序名;下面一行是在跑什么、最后输出的一行、多久没动静,还有一个小圆点表示空闲还是忙、收件箱里排着几条。不点进去就知道哪个跑完了。列表顶上能换成网格,一格一个 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 这类私有网络,绑到它的地址上。
把文件递出去
手机页的地址和端口既然已经通了,顺着就能做另一件事:把一个文件或者文件夹丢给手机、丢给旁边那台电脑。
keepane share report.pdf # 打印二维码和链接
keepane share ~/photos --for 2h # 文件夹,下载成 photos.zip,两小时内有效
keepane share build.log --once # 完整下载一次就失效每次分享有自己的随机令牌,不是手机页那把钥匙,所以拿到链接的人只能下这一个东西,别的什么都看不到。默认 24 小时,--for 改期限。文件原样发,断了能续传;文件夹边读边打 zip,不跟符号链接。所有东西一律当附件发出,不会被浏览器当网页打开,所以分享一个 html 不会变成在你的地址上跑别人的页面。手机页没开的话,share 会顺带把它开起来。
日常
剩下这些是为「人不在跟前」配的:
C-b v的 dashboard 把所有 pane、选中那个的收件箱、任务摆在一屏里,右边是它的实时屏幕、滚动历史、事件、agent 的 trace,或者它所在仓库的分支、改了哪些文件、最近几个提交;能直接发消息、改模式、解卡、跳过去。窗口宽过 150 列,最左边再多一栏,每秒写这台机器的 CPU、内存、交换区、磁盘和网络、温度、功率、电池
重启电脑之后keepane resume把 session、布局、每个 pane 的目录和原命令摆回来,屏幕上最后 500 行也回放(save-history all连整个滚动历史一起);keepane startup on让它登录时自己起,Windows 上写一个 Run 键值,macOS 上写一个 launchd agent,Linux 上写一个 systemd 用户 unit,顺带记住当时的PATH、SHELL和语言设置C-b u十秒内找回手滑关掉的 pane 或者窗口,程序还在跑C-b q每块显示自己的编号和工作模式,按数字直接跳过去(两位数的编号也认,按到第一位能唯一确定就走),起过名字的在编号下面显示%名字C-b y把上一条命令的输出取下来,C-b Y从所有 pane 跑过的命令里挑一条:跳过去、复制它的输出、或者在原地再跑一次;C-b [之后用[]在命令之间跳;C-b F给屏幕上所有 pane 里的路径、网址、git hash 标上字母,按小写复制,按大写打开(路径有code就用 VS Code 跳到那一行)
前缀按下去之后手停了一会儿,自动弹一个面板告诉你这些键都干什么
默认配色 0.38 起是paper-dark:底色取这个博客夜里代码块那张纸,正文墨色,一点朱砂留给当前窗口和活动边框,面板和状态栏上每个字的对比度都压到 4.5:1 以上;白天那套叫paper,照着白天的代码块。theme tokyo-night/tokyo-day还在,手机页上选了电脑的终端跟着变。pane 之间的分隔线默认换成了粗线,pane-frames on则给每块包一个圆角框,边框上的那行字写在框的上沿C-b t的时钟底下有东西跟着机器动,现在十三种:监护仪的心跳按 CPU 快慢、字符雨、每个核一根的均衡器、火焰、星野,俯视的鲨鱼追着鱼群、一天里长大的盆景、月光下的海、睡着的猫、跑过沙漠的小恐龙、冰面上的企鹅、霓虹网格和城市上空的烟花;数字是真从传感器和进程表读的,keepane 每投递一条消息,心跳抢一拍、水面荡一圈波纹、猫醒三秒。clock-mode-look还能把其中任何一种改用·•●、方块或者 0 和 1 重画一遍C-b s的会话树改成了方块图,hjkl移动,a全展开,配过对的机器也画在里面C-b C-t在每条命令那行的行尾空白处标出开始时间、耗时和成败,不改 pane 宽度,也不进capture-paneC-b /按 pane、按天翻它打印过的东西,一天一个文件留 30 天,在这个列表里再按/,一次搜遍每个 pane 的每一天;C-b B是任务板,一行一个 pane,能跳转、关掉、重启
后台窗口有动静、响铃或者太久没声音,状态栏标出来,C-b M-n跳过去;set -g notify on之后直接发系统通知,Windows 上的 toast 带一个「跳到 pane」的按钮C-b @列出ssh去过的机器,正连着的排最前:跳到连着它的那个 pane,或者新开一个窗口、分屏连上去;名单从 pane 里看见的 ssh、~/.ssh/config、known_hosts和 shell 历史里攒,密码和密钥一概不读不存C-b e彩色列出每个 pane 拿到的环境变量;set-environment设的东西也进已经开着的 pane,PowerShell、bash、zsh、fish 和 wsl 里的 shell 在下一次提示符处拿到
pane 里再跑一个 keepane(ssh 到服务器上那个),F12把这个 pane 的键全让给它,前缀也让,状态栏写上passthrough,再按一次收回来
装和用
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 都摆回来升级走 keepane update(--check 只看看有没有新版)。Scoop 那条要绕一下:Scoop 只在一个应用的程序全都没在跑的时候才更新,而 keepane 的服务端一直在跑,所以 update 先把一份副本挪到 Scoop 目录外面,存好会话、停掉服务端、让 Scoop 装,再带着这些会话用新版本起来,经过写进 update.log。
日常那半边不用重新学:前缀 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 里了,也挂了一份功能一览。手机那页做完之后,我最常拿它干的事是躺着看编译跑完,大概不算什么好习惯。