Vibe side project · Web terminal + Android
wetty-mobilevibe 副产品
把 Mac 上跑着的 Herdr 多 agent 工作区装进手机:WeTTY 的一个手机分支,外面套一个 23 KB 的安卓壳。滑动翻回滚、长按复制、底部一条常驻按键栏、锁屏再亮自动接回同一个会话——离开电脑的时候,看一眼谁跑完了、催一句、接手一个。它和上面的法律 agent 产品线没有关系,是同一套「agent 驱动开发」顺手做出来的第四件东西。
它长什么样
三张设计图:默认一屏、切换工作区、断线重连。手机上没有第二级页面,所有动作都在这一屏和底下两排键里。
21:32▲ ▮▮ ▮
1 obsidian2 wetty3 android
w1:p1合同审查意见done
w1:p2产品页working
w2:p1keybar 扁平化blocked
────────────────────────
✻ Brewed for 1m 42s · done 9:31 PM
※ recap: 底部按键栏改成扁平
两排 28px,去掉渐变与凸边。
next: 手机上重开页面看效果。
❯
obsidian Fable 5 ctx:12%
EscTabCtrl◀▲▼▶⌨
^C⇧Tab粘贴Goto◀TabTab▶+Tab⏎
默认Herdr 在 45 列宽下自动切成单栏:上面是 tab 与 agent 状态,下面是当前 pane。底下两排键第一行是终端通用键,第二行是粘贴与 Herdr 的前缀组合键,回车在右下角拇指的位置。
21:33▲ ▮▮ ▮
1 obsidian2 wetty3 android
// Goto = C-b g
// 单栏布局没有侧栏,
// 这个选择器就是侧栏。
obsidian Fable 5 ctx:12%
EscTabCtrl◀▲▼▶⌨
^C⇧Tab粘贴Goto◀TabTab▶+Tab⏎
Goto · 键盘弹出软键盘一弹,按键栏贴着键盘上沿一起上来,终端在上面收缩,不留黑边。Ctrl 是粘滞键:亮起后下一个字母发控制码——安卓软键盘也认。
07:48▲ ▮ ▮
1 obsidian2 wetty3 android
w1:p1合同审查意见done
w1:p2产品页done
────────────────────────
✻ Brewed for 6m 08s · done 11:52 PM
※ recap: 产品页已写完并部署,
curl 返回 200。
❯
EscTabCtrl◀▲▼▶⌨
^C⇧Tab粘贴Goto◀TabTab▶+Tab⏎
第二天早上手机在口袋里断了一夜。遮罩上没有一个字,只有 Herdr 那只羊头在呼吸;亮屏的瞬间页面自己接回去——会话一直活在 Mac 上,断的只是一根网线,所谓重连不过是重新贴上去。
设计理念
先试了两个现成的 SSH 客户端,都不行;回头发现问题根本不在传输层。之后的每一条原则,都是被一句真机反馈逼出来的。
决定体验的是终端模拟器,不是协议Herdr 是重型 TUI:真彩色、鼠标捕获、同步输出、CJK 宽字符。手机 SSH 客户端的自制终端撑不住这些,Mosh 再顺滑也救不回来;浏览器里的 xterm.js 反倒是支持最全的一个。所以留在网页上,把网页做好。
触摸就是鼠标上报终端里没有「手势」这回事,只有程序申请的几种鼠标模式。单击翻成一次按下抬起,滑动翻成滚轮,长按翻成选区,双指长按翻成右键——每一种都对着协议表写,而不是对着感觉写。
不依赖戳屏幕Herdr 的每个动作都有前缀键。与其猜测哪一次点击会被哪层吞掉,不如把 C-b g / p / n / c 做成按钮,按下去一定到。粘贴也是一个键:网页在局域网 HTTP 下读不到剪贴板,就让安卓壳递过来。回车放在右下角,因为拇指本来就在那里。
按钮不能抢焦点软键盘只要一失焦就收起。按键栏在 pointerdown 上截住默认行为,焦点始终留在终端的隐藏输入框里;方向键长按连发,每次按下震一下。
粘滞 Ctrl 要走输入事件安卓软键盘的按键事件是 Unidentified,靠键盘事件判断字母永远发不出 Ctrl+C。控制码的翻译放在文本输入路径上,物理键盘才走按键拦截。
会话在服务端,重连就是重贴断线不弹「已断开」让人点,而是半秒起步无限重试;切回前台、网络恢复、从后台缓存回来都立刻连。重连时把旧终端和旧监听拆干净,不然每次都会多一份幽灵输出。
先把字体等到,再量格子xterm 只在打开时量一次字符宽度。自带的 JetBrains Mono 要是晚到,整张网格就按回退字体算错。所以先等字体,再连 socket,首帧落在正确尺寸的终端里。
缝隙涂成程序的底色终端只能是整数列,余下几个像素总要露出来。不去硬凑,而是每帧采样网格边缘格子的背景色,把页面和滚动层涂成一样——程序换主题,边也跟着换。
键盘是布局收缩,不是覆盖iOS 的键盘盖在页面上,需要给它让一行;安卓与 WebView 里键盘是把页面推短。同一段代码两种处理,分不清就会留一条黑边,键盘收了它还在。
壳越薄越好安卓端是纯 WebView,不引任何库,23 KB。它只做网页做不到的五件事:全屏、系统栏与输入法的内边距、返回键变菜单、屏幕常亮、把系统剪贴板递给页面。其余全部留在网页里,改一次两端都得。
怎么工作
从手机到 Mac 上的 Herdr 会话,中间四站;所有站点都在家里的局域网内。
安卓壳WebView · 23 KB返回键 = 菜单 · 常亮
→局域网
HTTP
代理一次 token · 之后 cookieWebSocket 透传
→本机
回环
WeTTY 手机分支xterm.js · 触摸 · 按键栏重连 · 字体 · 底色
→ssh
公钥
Herdr 会话常驻服务端手机只是又一个客户端
| 遇到什么 | 怎么做 |
| 滑动 | Herdr 这类接管鼠标的程序收到滚轮上报,滚自己的回滚区;vim 收到方向键;普通 shell 滚 xterm 的缓冲区。带惯性,再次触摸即停 |
| 单击 / 双击 | 单击是纯点击,透传给程序,永远不弹键盘;双击或 ⌨ 键才唤出软键盘 |
| 长按 / 双指长按 | 长按选中手指下的词、拖动扩选、松手复制进剪贴板;双指长按发右键 |
| Herdr 内部复制 | 程序通过 OSC 52 写剪贴板,落到系统剪贴板;只写不读,程序偷看不到你的剪贴板 |
| 锁屏 / 切应用 / 换网络 | 亮屏、切回、恢复网络的瞬间立刻重连,接回同一个 Herdr 会话;旧终端与监听全部拆除 |
| Herdr 退出(C-b q) | 覆盖层显示「会话已结束」,一键起一个新会话,不刷新页面 |
| 连不上 Mac | 壳显示本地重试页,三秒一次自动重试;返回键菜单里改地址 |
| 首次启动 | 填一次带 token 的地址,代理种下 cookie,之后地址栏里只剩主机名 |
上游 WeTTY 本身没有触摸支持、不能在局域网 HTTP 下工作、也不认 OSC 52。这个分支的改动全部在客户端一层,服务端只补了 macOS 上的两处小问题;上游合并新版本时照常 rebase。
为什么叫 vibe 副产品
起因还是那几个后台 agent:它们一跑就是几分钟,人不可能一直坐在电脑前。桌上的墨水屏(见 herdr2dot)解决了「谁跑完了」,但看见之后总要回去接手——催一句、批一下、换个任务。想在手机上做这件事,试了两个 SSH 客户端,Herdr 在里面不是花屏就是点不动。回头一查,家里早就有一个为了别的目的搭起来的 WeTTY 网页终端,它用的 xterm.js 反倒把 Herdr 画得端端正正。于是问题从「换个客户端」变成「把网页做顺手」。人说的只有一句话:在手机上用 Herdr,要像在电脑上一样。
- 07-06分支诞生:让上游 WeTTY 在局域网 HTTP 下能用,补上触摸滚动、长按选择、软键盘唤起与 OSC 52 剪贴板;一周后修好键盘挡住提示行的问题
- 08-28晚上:先后试了两个手机 SSH 客户端,断定问题在终端模拟器而不在协议。两小时内给网页加上常驻按键栏、能用的粘滞 Ctrl、自动重连、自带字体、边缘底色;再搭一个安卓壳,画一个图标
- 同一晚之后的十几次迭代全部来自手机上的一句话:「右侧有一块缝隙」「收起键盘后有条黑边」「首次启动双击不弹键盘」「按钮做扁平一点」「回车放右下角」——每一句对应一次提交
四个副产品放在一起看,共同点仍然是那一条:每一个都只做一件事,状态少到可以写在一行里,边界清楚到 agent 可以放心改。这一个的状态甚至不在自己手上——会话活在 Herdr 里,手机端只是一块玻璃,碎了换一块贴上去就行。
技术与状态
网页端WeTTY 3 的分支,xterm.js 6。改动集中在客户端:触摸与手势、按键栏、粘滞 Ctrl、重连、自带 JetBrains Mono、边缘底色采样、自适应视口。
安卓端纯 Java WebView 壳,不引 AndroidX,23 KB;minSdk 30。自适应图标含 Android 13 主题单色层。
边界只在局域网内使用:Mac 侧代理凭 token 种 cookie,WebSocket 原样透传,SSH 走公钥、只连本机。手机上不存密钥,不存会话。