
这是什么
一个用 Rust 写的终端,它的 shell 待在后台的服务器里,而不是窗口里。退出应用,每个 shell 都继续跑;再次启动时面板带着布局和最后留在屏幕上的内容回来,装好钩子的编码 agent 还能接着上一次的对话。它按面板里正在跑的命令认出二十六种编码 CLI,把它们放进同一条侧边栏,标出是在工作、在等你、还是做完了——这个状态是通过装进 agent 自己配置里的钩子送回来的。既然状态是程序可以问的东西,发问的就不必是人:split、send、wait、capture 四条命令就是整个循环,窗口关着也照样能用。远端工作区是同一套服务器跑在另一台机器上,标签页、面板、git 与 diff 都留在那边,不同步任何文件,走的是项目自己写的 SSH 客户端。渲染走 Zed 的 gpui,终端内核来自 Alacritty,构建覆盖 macOS、Windows、Linux、iOS 与 Android。
谁做的仓库所有者,十二周里 1,546 次提交中有 1,302 次出自这个账号;其余 244 次来自另外三十三个账号,其中一个 89 次、一个 30 次。316 条带共同作者尾注的提交里,92 条写的是一个 Claude 模型名。除提交历史之外,报告里没有关于作者本人的资料。
它是怎么搭起来的
组成 · 6一个客户端加一个服务器。守护进程持有面板、它们的 PTY 与回滚缓冲,而窗口、命令行与手机都是它的客户端——所以关掉一个客户端什么也杀不掉,也所以远端工作区不需要单独做一个功能,它就是同一个守护进程在别处。三个后果塑造了代码。服务器必须扛得住被替换,于是有交接路径、pidfile、单例守卫、更新守卫和一个独立的更新器二进制。服务器是「存在什么」的唯一真相,因此工作区、面板与刚关掉的标签页都是机器树里的记录,每个客户端都读它,而不是某一个窗口持有的状态。本地主机与远端主机之间是一条真实的接口,配一致性测试和一个在 CI 里检查有没有人越界的脚本。渲染通过 gpui 走 GPU,gpui 来自 Zed 的一个分支,终端内核取自 Alacritty。
- crates/tty7-core/src/daemon/
- 服务器:
server.rs68 KB、control.rs110 KB、pane.rs316 KB、spawn.rs94 KB、protocol.rs104 KB、router.rs52 KB,周围是长命进程需要的东西——singleton.rs、pidfile.rs、handoff.rs、update_guard.rs、scrollback.rs、history.rs——外加两个自成体系的子系统:ssh/(认证 66 KB、转发 53 KB、SFTP 37 KB、known hosts 36 KB)与install/(70 KB,另有 WSL 的 63 KB,以及 Windows 主机与代理各 19 KB)。 - src/
- 桌面应用:
main.rs49 KB,src/ui/下 105 个文件共 5.4 MB,其中app.rs594 KB、tree_sync.rs231 KB、tab_sidebar.rs196 KB、switcher.rs180 KB、settings.rs166 KB;src/terminal/是 32 个文件 1.8 MB,最大的是 677 KB 的view.rs,其后是 126 KB 的search.rs与 54 KB 的input.rs。src/bin/下另有一个 173 KB 的更新器二进制。 - crates/tty7-cli/
- 非交互客户端:十七个文件,
commands.rs207 KB、cli.rs43 KB,外加一个 51 KB 的端到端测试,以及整个 agent 循环所依赖的寻址模型——面板用%83,标签页用@7,工作区用名字或 id。 - mobile/
- 一个 Tauri 应用,有自己的 lockfile 和自己的发布标签:
src/main.ts118 KB、style.css40 KB,DESIGN.md是 22 KB 的设计系统,.impeccable/design.json是 37 KB 的 token,另有生成的 Android 与 iOS 工程,以及一个在每次重新生成后把应用图标放回去的scripts/testflight.sh。 - crates/tty7-gateway/、crates/tty7-mobile-proto/、crates/tty7-mobile-client/
- 手机这条路:手机配对用的网关(
serve.rs28 KB、state.rs16 KB、service.rs14 KB,一个轮询器和一个bridge.rs集成测试)、一个 23 KB 的协议 crate,以及一个 12 KB、带 mDNS 测试的客户端。 - docs/、skills/tty7/、.github/
- 九个分区下共四十个文档页,三十张配图加一份文档配置;一份给 agent 用的技能(
SKILL.md18 KB、25 KB 的命令参考、13 KB 的委派参考);还有发布机器——四个工作流与十一个脚本,从三个平台的打包到主机边界检查。
取舍,以及它替代了什么
shell 归后台服务器,不归窗口 替代 在终端里套一个复用器来让它们活着
README 开头讲的就是这个问题:多数终端把 shell 绑在窗口上,于是人们在里面跑 tmux——能用,但为了绕开窗口得再学一套快捷键、再维护一份配置。把 shell 移进守护进程之后,退出应用变成空操作,命令行与手机成了同一批面板的平等客户端,而远端工作区只是同一个程序跑在别处,不再是一个单独的功能。
让 agent 待在自己的 PTY 里 替代 把它们包起来或做代理
一句话写清楚了:「The agent you start is the real one, running in an ordinary PTY. Simple things are more likely to keep working.」随之而来的是状态——从装进每个 agent 自己配置里的钩子读出,每个 agent 一次点击——而不是一个拦截层;拦截层意味着要跟它所识别的每一个 CLI 同步演进。
状态取自 agent 自己 替代 在屏幕上找哨兵字符串
CLI 文档说屏幕是个矩形:滚上去的内容没了,折行的行不是一行,退出码从来不在屏幕上,所以它建议把答案写进文件。状态经由装进 agent 配置的通道送回来,这才让
tty7 wait能阻塞等到某个 agent 进入等待或完成,而不是靠轮询。一个工作区同时只由一个窗口驱动 替代 让本地窗口与远端窗口同时驱动同一批面板
两者原本同时在驱动它们,肉眼可见的后果是面板尺寸来回跳——谁最后 resize 谁说了算——TUI 因此不停重排(pull request 1042)。这条规则同样适用于跑在守护进程自己机器上的窗口,实现成一小段与远端路径共用的纯状态。
远端就是同一套服务器,什么都不用同步 替代 一个会同步文件或转发会话的工作区
README 把远端说成「服务器在别处的本地情形」,于是一个守护进程、一棵机器树、一套面板同时服务两种情形,主机边界靠一致性测试和一个 CI 脚本守住。它也解释了 bug 从哪来:一个远端面板就是一条真实的 SSH 会话,于是「每条连接默认十个会话」这种限制会变成产品限制(1034)。
依据README.md、docs/agents/overview.mdx、docs/cli/overview.mdx、mobile/DESIGN.md、编号 1029 至 1058 的 pull request 与 issue 正文,以及完整的 754 个文件树及其体积。
制作过程
6 个阶段- 01
十二周、1,546 次提交,以及七月里重启的版本线
第一次提交在 2026-07-06,写的是「a GPU-rendered, daemon-backed terminal in pure Rust」;报告里最新的一次是 2026-09-30 的链接解析修复,中间共 1,546 次提交——七月 543 次、八月 562 次、九月 441 次,十二周里几乎没有一个月松下来。其中 1,302 次出自同一个账号;其余 244 次来自另外三十三个账号,一个 89 次、一个 30 次;1,546 次里有 1,544 次能关联到 GitHub 账号。316 条带共同作者尾注的提交里,92 条写的是一个 Claude 模型名,其中 Opus 各版本最多,作者还把自己署成共同作者 199 次。报告里的二十个 release 从 2026-07-15 的 v0.16.0 开始,两天内走过四个版本:v0.17.0 与 v26.7.0 都落在 2026-07-16,此后编号改成「年.月」,版本线从 0.x 跳到 26.x 只用了一天——26.7.x 一路到 2026-09-29 的 v26.9.4,后者的标题自称带来了 IDE 级代码编辑器、v5 改版和 GitHub 标签页,而 v26.8.3 自称是一次 151 项修复的打磨。nightly 是一个滚动的预发布标签而不是版本号,手机应用有自己的标签(mobile-v0.1.1 与 mobile-v0.1.2,都在 2026-09-30),仓库里那份 CHANGELOG.md 有 376 KB,二十个标签里还留着 v26.9.0、v0.15.0 与 v0.15.0-beta.1,其中 v26.9.0 有标签却没有对应的 release。
- 02
shell 属于服务器,而服务器必须可以被换掉
README 把整个设计说成一个决定:shell 属于后台运行的服务器,窗口只负责显示它们,所以关掉窗口对它们毫无影响,工具其余大部分行为都是这个决定的后果。服务器在
crates/tty7-core/src/daemon/——server.rs68 KB、control.rs110 KB、pane.rs316 KB、spawn.rs94 KB、protocol.rs104 KB、router.rs52 KB——周围是长命进程需要的东西:singleton.rs(24 KB)、pidfile.rs、handoff.rs(21 KB)、update_guard.rs(8 KB)、scrollback.rs(17 KB)、history.rs(15 KB)。另有一个 173 KB 的更新器二进制,于是「换掉服务器」这件事不必由服务器自己动手。守护进程眼里的世界是machine.json里的一棵机器树:每个面板一条记录,存着工作目录、shell、SSH 参数与 agent 事实;按 pull request 1037 又加了closed列表,记录刚关掉的标签页及其布局与closed_at时间——于是重开标签页能扛过 GUI 重启,而且对该工作区的其他窗口可见,不再只活在某一个窗口的内存里。重启是这套设计不打算假装解决的问题:进程没了,回来的是布局、最后留在屏幕上的内容,以及装了钩子的 agent 的那段对话。tty7 doctor回答服务器是否可达、它的协议方言与发问的这个二进制是否对得上;服务器的测试名字是daemon_reap、daemon_stop、handoff、scrollback_restore、machine_tree。 - 03
gpui 上的私有分支、来自 Alacritty 的终端内核,以及「屏幕是个矩形」
README 的副标题就是技术栈:纯 Rust、GPU 渲染走 Zed 的 gpui、终端内核来自 Alacritty。两边都产生了必须改进别人代码的活。gpui 把每一行文字画进一个 scene layer,这个 layer 的边界决定它与之后图元的绘制顺序;而边界是按未对齐的原点算的,于是右对齐的字形落到了自己 layer 之外,会直接画穿鼠标悬停时出现在行右端的暂存/丢弃条;修法是在 Zed 的分支里加一个
layer_bounds辅助函数——l0ng-ai/zed@3d46ee70fb,分支tty7(pull request 1030)。第二处来自组件库:标题栏在子元素之前为 macOS 的红绿灯留了 80pt 的 lead,于是居中在「lead 之后那条带」里的 Search Everywhere 输入框,比终端列偏右 40pt,侧栏收起时还比窗口中心偏右 40pt(1036)。终端一侧,仓库里有kitty_graphics.rs(87 KB)、input.rs(54 KB)、images.rs(37 KB)、boxdraw.rs(27 KB);在 Windows 上它随包附带微软的conpty.dll与OpenConsole.exe,版本 1.24.260710001,x64 与 arm64 各一套,而不是信任系统自带的那一对(1043)。有一个还开着的 issue 报告:在 Windows 上,协商了 Kitty Keyboard Protocol 的 TUI 里,实体 Escape 键毫无反应;作者复现不出来,于是给的不是修复而是他做过的探针——一条把实体 Escape 编码成\x1b[27u的单元测试、两套 ConPTY 都原样透传CSI 27 u、以及在 Python 与 Node 下合成WM_KEYDOWN VK_ESCAPE都能收到\x1b[27u。性能是用脚本讲的:11 MB 的cat用 95 ms、DOOM-fire 跑到 888 fps、冷启动 116 MB(其中窗口 105、服务器 11),M1 Pro 上五次取平均,scripts/bench/与那个 fire 基准的补丁都提交在仓库里。 - 04
会驱动别的 agent 的 agent,前提是不把它们包起来
二十六种编码 CLI 靠面板里正在跑的命令被认出来,识别能看穿完整路径、Windows 上的
.cmd与.exe、前置的环境变量赋值,以及前面的解释器(node .../claude/cli.js);自己的包装脚本可以在config.json里映射到某个 agent。识别是免费的——头像、分支、diff、标签页标题。状态不免费:它来自 agent 自己,经由 tty7 装进该 agent 配置里的一条通道,在 Settings → Integrations 里每个 agent 一次点击;状态点、通知、托盘图标、tty7 wait以及重启后的续聊都靠它。状态之所以值得有,是因为发问者不必是人:tty7 split、send、wait、capture就是整个循环,窗口关着也能用。文档也说清了这条路在哪断——屏幕是个矩形,所以被折行的行只有在--plain下才会还原成一行,用\r自我重写的进度条读到的是最终值,而退出码从来就不在屏幕上,因此它建议把答案写进文件,而不是去 grep 画面。README 用一句话说明这些 agent 既没被包裹也没被代理:「The agent you start is the real one, running in an ordinary PTY. Simple things are more likely to keep working.」背后是agent_hooks.rs(163 KB)、cli_agent.rs(109 KB)、agent_history.rs(72 KB);仓库还自带一份 agent 技能——skills/tty7/SKILL.md18 KB,外加 25 KB 的命令参考与 13 KB 的委派参考——用npx skills add l0ng-ai/tty7安装。CLI 本身是一个很薄的非交互客户端:%42这样的面板地址在面板一生中不变,@7这样的标签页地址在任何东西新建或关闭时都会移位,工作区用名字、id 或 id 前缀寻址。 - 05
远端就是同一套服务器,难点也都在这里
远端工作区不是一个同步功能:它就是同一个守护进程跑在另一台机器上,标签页、面板、文件树、git 与 diff 都留在那边,不复制任何文件;SSH 客户端是项目自己写的 Rust 代码,所以不依赖系统的
ssh——有带钥匙串密钥的配置、跳板机、SFTP 与自动端口转发,而tty7-server装一次即可、不需要 root。这个说法背后是有界面的工程:daemon/install/有 70 KB,另有 63 KB 的wsl.rs、19 KB 的windows_host.rs与 19 KB 的proxy.rs;host 层分成本地、远端与服务器三块,conformance.rs44 KB,而.github/scripts/check-host-boundary.sh就是为守着这条线而存在的。有意思的是那些 bug。一个 17 个面板的远端工作区挂了十多秒、标签页一个一个地回来,因为 sshd 默认每条连接允许十个会话,而每个远端面板就是一个:第十一次打开被拒,连接就在十个面板还在用它的时候被退休,此后一切都重新拨号、重做完整握手与服务器探测——包括那个每十秒一次的存活探测,因为没有东西持有替代品。重新接入又把它弄得更糟:除了沉默之外的任何失败都被读成「面板没了」,于是它新起一个 shell,还自动跑了claude --resume,而原来那个面板其实还活着(1034)。另一处:跑在守护进程自己机器上的窗口完全跳过了工作区接管,于是远端客户端与本地窗口同时驱动同一批面板,谁最后 resize 谁说了算,TUI 不停重排;事后写下的规则是「一个工作区同时只由一个窗口驱动」,实现成src/ui/local_link.rs里一小段纯状态(1042)。Windows 主机又带来一串 POSIX 假设——sh -c、/dev/null、setsid、/proc、kill -TERM、可执行位、先删掉再替换文件——而tty7-server --stdio干脆拒绝在 Windows 上运行(1041);默认 shell 是 PowerShell 的主机上,shell 集成需要一行所有候选 shell 都能不出错读出来的探测命令(1040);重新拨号的 SSH 面板回到登录目录,是因为守护进程匹配了派生消息却把cwd字段丢掉了(1035)。 - 06
nightly 的下载表、UTC 的构建号,以及写清「还没验过什么」的说明
四个工作流——ci 19 KB、nightly 26 KB、release 23 KB、mobile 8 KB——以及
.github/scripts/下的十一个脚本,其中有 19 KB 的 macOS 打包脚本、14 KB 的安装器脚本、10 KB 的 Windows 包校验脚本,还有若干断言:产物不依赖 VCRuntime、静态链接、Mach-O 类型正确。发布工程得到的重视和功能一样:nightly 说明现在以一张下载表开头,覆盖两个架构上的 macOS、Windows、Linux 与tty7-server,表由当晚真正产出的文件生成,构建失败的就不列出来,而不是给一个死链接;所有链接都走滚动的nightly标签;还加了一列 Phone,指向 TestFlight 邀请与最新的mobile-v*APK(1054、1055)。Windows ARM64 那一版是交叉编译的,并且标为实验性、带continue-on-error,直到它成功发布过一次——于是它失败时只是丢掉 ARM64 包,而不会卡住整个发布;Rust 缓存按 target 分键,两条 Windows 腿不会互相覆盖(1043)。报告记下的失败都很具体、很日常:TestFlight 构建号原本是本地时间,于是一台在 UTC+8 的机器编号比 UTC 的 CI runner 快八小时,八小时内同版本的 CI 上传反而更小,被 App Store Connect 拒收(1050);证书检查匹配的是固定字符串,而新版 apksigner 按 SDK 范围给签名者命名,于是它读到的是空摘要(1047);yes在 SDK manager 停止读取后被 SIGPIPE 杀掉,而pipefail把它算成 NDK 这一步失败(1045);Android 上发布的是模板图标,因为android init从没采纳icons/android,iOS 上只要gen/被重新生成就会带上 Tauri 的占位图标(1051、1038)。有两条说明比它们伴随的修复更值钱:一处手机端改动只在 iOS 模拟器的深色主题下验过——「Not yet checked in the light theme or on a device」;一处手机客户端的中继修复干脆没走 pull request,因为想先出一个包:「shipping a TestFlight build from the branch first」(1039、1057)。README 本身也是在一个 pull request 里被重写的,改成以三根支柱和那句「The terminal that outlives its window」开头,而不是一份功能清单(1046)。
相关档案
全部档案 →第 073 号
Zeron
一个 Rust 桌面程序,把你在用的编码 agent——Claude Code、Codex、Cursor、Devin、Grok、Hermes、Pi、Antigravity——装进同一个控制台,默认只在本机跑、不需要账号;只有你主动登录,它才会把这些会话同步到你其它的设备上。
第 061 号
Reticle
一个 MCP 服务器加一个只在开发期生效的 SDK:让编码 agent 从应用内部去读、去操作一个正在运行的 web 或桌面应用,然后给出判词和该改的文件与行号,而不是一张截图。
第 077 号
Lody
一个让团队共用他们本来就在跑的编码 agent 的工作台:连上一台机器,把 Claude Code、Codex、Kimi 或任何其他说同一套协议的 agent 接进来,然后从桌面端、手机、网页或终端派活;会话之间可以互相派活,而代码始终留在机器主人自己连上来的那台机器上。