Files
terminalX/docs/HANDOFF.md
kid 6dd23275ff feat: 修复 3 个渲染/tmux 显示 bug + UI 产品化第一波(对标 Moshi)
渲染/tmux 修复(e2e 复现+回归,模拟器 idb 点击+截图取证):
- 终端底部残影:ghostty iOS 释放 surface 不摘 IOSurfaceLayer(上游缺陷),
  platformSetup 清现存子层 + TerminalSurfaceCoordinator.onSurfaceFreed 钩子摘孤儿层
- tmux 切 tab 切回旧 tab 内容消失:改所有窗口常驻挂载(ZStack+opacity+hitTesting),
  surface 不释放、内容保留、后台窗口持续收 %output
- tmux pane 内容钉底/顶部残行:pane grid 由容器几何定(tab 条吃高度),75 行快照喂 73 行
  溢出上滚;几何测量上移 TmuxTabbedView + attach kickoff 双条件(attachAcked∧containerKnown)
  + capture-pane 铺快照前剔尾空行

UI 产品化(对标 Moshi getmoshi.app:深蓝黑 + 终端绿 + Catppuccin):
- 设计系统 Theme.swift:TXPalette(色角色 struct)+TXThemeManager(环境注入),颜色封装供主题系统
- 统一主题:抽离 4 套 Catppuccin(Mocha/Macchiato/Frappé/Latte),一套同时驱动 app+终端配色,
  设置里选主题 app 与终端一起变色 + UserDefaults 持久化
- 主屏/主机管理页 HomeView:HostCard 主机卡 + FAB 加主机(AddHostSheet) + 设置(主题选择器),
  HostStore 本地 JSON 持久化(密码暂存/TODO Keychain),点卡片 connect(to:)
- 终端外壳 chrome TerminalTopBar:圆形返回钮 + 会话标题 + SSH/mosh 传输徽章 + 状态提示,
  tmux tab 绿 pill 化,底部工具栏深色自适应,终端默认 Catppuccin Mocha

iPad mini(A17 Pro) live 端到端验证:加主机→连接→终端→切 tab→分屏点焦点→主题切换同步。
详见 docs/HANDOFF.md §10。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 22:43:00 +08:00

19 KiB
Raw Blame History

terminalX 交接文档HANDOFF

目的:让没有上下文的新会话读完即可掌握项目全貌并继续工作。配合 ../CLAUDE.md(操作规范/命令)与项目记忆(~/.claude/projects/-Users-kid-...-terminalX/memory/,本项目会话会自动注入)一起看。 最后更新2026-07-24。


0. 一句话现状

一个原生 iPad SSH 终端已可用并真机验证libghostty 渲染 + 键盘输入 + SSH 直连 + SSH-over-tsnet(Tailscale 用户态) + tmux -CC 原生 tab + 多 pane 分屏 + mosh(LAN + over-tsnet 均已端到端验证) + 断线自动重连 + mosh 挂起→恢复<3s(M3)。四大核心需求libghostty/tailscale/tmux/mosh 均已验证)。仅在 iOS 模拟器验证(未上真机/未签名)。

2026-07-24 本轮:修复 3 个渲染/tmux 显示 bug + 完成 UI 产品化第一波(对标 Moshi。详见 §10。UI 已从 demo 态变为深色 Catppuccin 主题 + 主机管理页 + 终端 chrome + 统一主题切换。 mosh 状态2026-07-24 全部验证通过)mosh(C++,blinksh/ios)+protobuf-lite 交叉编译成 MoshCore.xcframeworkCommonCrypto 后端,含 arm64 模拟器 slicetsnet 桥加 UDP relay(StartMoshRelay)MoshSession(pipe↔FILE*/pthread/SIGWINCH)+MoshConnectScanner(TXCore)+SSHTerminalModel 编排SSH 引导 mosh-server→解析 MOSH CONNECT→接管

  • LAN 直连端到端验证192.168.9.199mosh-server 存活 104s≫60s 无客户端超时+子 shell+心跳=真连。
  • mosh over tsnet 端到端验证vohive-vm 100.69.201.101app 日志 parsed MOSH CONNECT port=60002relay up localPort=63542activateMosh started;屏幕渲染远端 shell 且无 mosh 断连 overlay=relay UDP 双向流通。R1(tsnet UDP 数据报语义) 证伪
  • 复现 tsnet模拟器已持久化 tsnet 节点状态(~/Library/.../tsnet-main)-txTsnetKey 传任意非空串即可凭存量身份重连(节点未被 tailnet 清理时);节点被清理则需新 ephemeral key。

1. 项目愿景与硬约束

  • 目标iPad/iPhone 远程轻量运维 macOS/Linux + vibe coding。平台优先级 iPadOS > iOS > macOS(Apple Silicon)
  • iOS/iPadOS 只远程(沙盒禁 fork/exec不能跑本地 shellmacOS 才支持本地 PTY。
  • 分发目标 App StoreM5已暂缓
  • 四大核心:libghostty(终端引擎) · mosh · Tailscale tsnet(用户态,非 NetworkExtension) · tmux control-mode(-CC) hook

2. 关键架构决策(已定,勿推翻)

  1. 终端引擎用 libghostty,经社区 GhosttyKitLakr233/libghostty-spm,已 vendor 到 vendor/libghostty-spm)。TerminalViewStateObservableObject;喂字节入口 InMemoryTerminalSession.receive(Data)SwiftUI 视图 TerminalSurfaceView(context:)
  2. Tailscale 走 tsnet 用户态Gogomobile bind → TsnetBridge.xcframework不用 NetworkExtension(避开 VPN entitlement + 50MB 内存墙)。
  3. SSH 用 libssh2 + mbedTLSCSSHCore.xcframework),支持传入外部 fd。
  4. SSH-over-tsnet = fd 桥tsnet.Dial() 得 net.Conn无真实 fdsocketpair(AF_UNIX) + 双向 io.Copy 泵 → 把另一端 fd 交给 libssh2。见 vendor/tsnet-bridge/tsnetbridge/bridge.go: DialTCPFD
  5. 一进程只一个 Go runtime、只一次 gomobile bindC/C++ 库符号隐藏只导出 C API。
  6. 多账号(未实现):多 tsnet 节点并存;每 host 绑 egress profile(Direct/tailnet)禁止多 tailnet 并发盲探(重叠 100.x/同名会误连)。

3. 环境(决定构建方式,极重要)

  • 带认证的 HTTPS 代理HTTPS_PROXY=...@...:38128)。github.com release blob 能 curlgo.dev / ghcr.io 被代理 abortSwiftPM 不支持带认证代理 → 所有 SPM 依赖 vendor 本地化。Go 模块用 goproxy.cn
  • Go 1.26.5 在 ~/.local/go(不在默认 PATH经 aliyun 镜像 curl 装),gomobile/gobind 在 ~/go/bingo env -w GOPROXY=https://goproxy.cn GOSUMDB=off 已固化。
  • 工具链Xcode 26.6 / Swift 6.3 / xcodegen 2.46 / zig 0.15.2 / cmake 4.4 / ninja 1.13 / git。

4. 里程碑状态

里程碑 状态 真机实证
M0 SSH 直连终端 完成 libghostty 渲染(SGR/CJK)、键盘输入、连接;白屏 bug 修复(OutputGate);会话状态机+自动重连TXCore 单测总数见 §5=50
M1 tsnet + SSH-over-tsnet 完成 app 内 tsnet 加入 tailnet(IP 100.74.60.108);对端枚举;SSH-over-tsnet 登录 Ubuntu shell(服务端 Last login from 100.x 证明走 tailnet
M1.5 tmux -CC 原生 tab MVP 直连跑 tmux -CC attach → 渲染 fish/tmux 两原生 tab + 活动窗口内容
dial 重试打磨 TsnetManager.dialFD 6 次退避重试,解决 tsnet 首连路径预热超时
R5 Go+gvisor+C+++Swift 同进程 retire 全程真机无崩溃
M2 mosh 完成LAN + tsnet 均验证) libmoshios(C++)+protobuf-lite→MoshCore.xcframeworkUDP relay over tsnetLAN 直连 + mosh-over-tsnet 均端到端验证通过R1 证伪)
M3 生命周期深化 mosh 快恢复(真机深挂起待补) scenePhase FSM + mosh 挂起→恢复<3s 已验证模拟器15/30s 冻结→前台wake pulse→1.3s 收到服务器包,"已恢复");冷启动重连(SSP 序号+key 持久化)划入 M4/M5
M4 安全 完成(真机 SE 硬件待补) host key TOFU pin(首次信任+不符告警,指纹对齐 ssh-keygenMITM 关闭)+ publickey 认证P-256 sign_callback→SecKeyCreateSignature→DER→SSH mpint免密登录 192.168.9.199 验证通过SE 优先/未签名回退软件+文件SE 生成在模拟器 hw=true 可达,真机 SE 硬件签名+keychain 持久化待补)
M5 合规提审 ⏸️ 暂缓 用户指示暂缓

5. 代码地图与数据流

  • packages/TXCore/Sources/TXCore/
    • Tmux/TmuxControlParser.swift TmuxEvent.swift TmuxIDs.swift TmuxOutputDecoder.swift TmuxLayout.swift TmuxControlSequence.swiftDCS 检测)—— 纯解析。TXCore 全部单测共 5040 swift-testing + 10 XCTesttmux 解析/SessionMachine含mosh三相/MoshConnect/SSHWireswift test --package-path packages/TXCore 全绿。
    • Session/SessionMachine.swift —— reduce(event)->[Effect] 会话状态机(指数退避重连/后台冻结/前台恢复)。M3 加 mosh 三相 moshActive/moshParked/moshResuming + 事件 moshEstablished/moshHealthy/moshExited/resumeWatchdogFired + 效果 nudgeResume/scheduleResumeWatchdog/teardownMoshmosh 接管即拆 SSH、后台冻结不重建、前台打唤醒脉冲等 SSP 续、两次脉冲无效兜底全量重建。
  • packages/TXTransport/Sources/
    • TXTransport/Transport.swift(协议+SSHConfig+SSHErrorSSHSession.swiftlibssh2preconnectedFD 支持外部 fd事件循环 poll+非阻塞)。
    • TXTransport/MoshSession.swiftM2驱动 mosh_mainpipe↔FILE* 桥in/out、Foundation.Thread 跑阻塞主循环+pthread_self()pthread_kill(SIGWINCH) resize、忽略 SIGPIPE、setenv UTF-8 locale。
    • CSSH/C targetlibssh2 头 + modulemapimport CSSH)、CSSHCorebinaryTargetlibrary-only
    • CMosh/C targetmoshiosbridge.h(mosh_main)+modulemap+占位 shim.cimport CMosh)、MoshCorebinaryTargetlibrary-onlyTXTransport 链 libc++/libzmosh 是 C++ 且 compressor 用 zlibCommonCrypto 属 libSystem 自动解析)。
    • TXCore/Mosh/MoshConnect.swiftMoshConnectScanner 流式扫描 MOSH CONNECT <port> <key>(注意 Swift 视 \r\n 为单 Character行尾判定须用 isNewline)。
    • TXCore/SSH/M4SSHWire(string/mpint 编码)/ECDSAConv(DER→SSH 签名·P256 blob)/HostKey(opensshFingerprint·evaluate·HostTriple·KnownHostsStore 协议)SSHWireTests 10 单测(真实指纹向量+高位 DER
  • M4 安全TXTransport SSHConfig.hostKeyVerifier(闭包)+SSHError.hostKeyMismatch+SSHSigner 协议+Authentication.publicKeyCallbackSSHSession 握手后 libssh2_session_hostkey 校验、sshSignCallback(@convention(c)+malloc 出参)接 libssh2_userauth_publickey。appKeychainKnownHostsStore(文件后端 TOFU pin)、SigningKeyProvider(SE 优先/软件回退+文件持久化,SecKeyCreateSignature)、mismatch alert(ContentView)。
  • M3 快恢复链bridge.goNode.WakeUp()(tsnet InjectEvent+MagicSock.Rebind/ReSTUN) 与 MoshRelay.Rebind()(同端口重开 loopback、泵按 socket 生成容错)iosclient.ccg_mosh_last_heard(收到服务器包即更新,导出 mosh_last_heard_ms()) 并恢复 SIGCONT 全屏重绘;MoshSession.nudge()(SIGCONT) + lastHeardMs()SSHTerminalModel 前台执行"唤醒脉冲三连"(WakeUp+Rebind+nudge)+healthy 轮询(探针增长→moshHealthy)+恢复看门狗+beginBackgroundTask
  • apps/TerminalX/iOS/
    • SSHTerminalModel.swift —— 核心编排:OutputGate(surface-ready 缓冲)、TransportHolderTsnetManager(tsnet 节点 up/dial+重试)、TsnetErrorTmuxRouter(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、autoConnectIfConfigured(启动参数)。
    • ContentView.swift —— 分支:tmuxController != nilTmuxTabbedView(tab 条+活动窗口);否则 RawTerminalViewTsnetProbeView(纯自检)ConnectionForm
    • TmuxController.swift —— tmux 网关:消费解析事件;pane-per-surfaceTmuxPaneSurface=session+state+gate/paneTmuxWindow 持 panes 字典 + visibleLayout(渲染)/fullLayout(diff)surfaceByPane O(1) 路由 %outputapplyLayout reconcile(增删留)attach: refresh-client -C WxHlist-windows(带 layout)→每 pane capture-paneawaitingCapture 丢弃快照前 %output输入绑 pane→send-keys -t %selftap→select-pane。字节经 SSHTerminalModelTmuxByteChannel(AsyncStream 单消费者)保序入网关。
    • TsnetProbe.swift —— M1 自检视图(加入 tailnet + 对端列表)。
    • TerminalXApp.swift —— @main。
  • 数据流(直连)transport.onBytes → TmuxRouter.feed → (raw) OutputGate.deliversession.receive→libghostty(检测到 DCS) → TmuxController.feed→解析→各 window。输入libghostty→session write 闭包→transport.sendtmux 模式→send-keys
  • 数据流(tsnet)TsnetManager.ensureUp(auth key)→dialFD(socketpair fd)→SSHSession(preconnectedFD:)

6. 已知问题 / TODO继续工作的入口

  • M4 安全(已完成,剩真机 SEhost key TOFU pin(SSHConfig.hostKeyVerifier→app KeychainKnownHostsStore 文件后端) + publickey 认证(Authentication.publicKeyCallback(SSHSigner)SSHSession C sshSignCallbackSigningKeyProvider)。:真机验证 SE 硬件密钥生成+签名+keychain 持久化(未签名/模拟器回退软件+文件);libssh2_session_method_pref 锁定 pinned hostkey 类型防算法漂移误报fable 建议未做M4DBG NSLog 埋点可清理。
  • tmux 多 pane 分屏 + 动态 resize 已做pane-per-surface + layout rect 绝对定位渲染 + 按 pane %output 路由 + tap 切焦点/select-pane + attach refresh-client -C 设尺寸 + iPadOS 拖拽窗口/旋转 → onChange(geo.size)containerResized(点×displayScale÷cell像素)→debounce→refresh-client -C→%layout-change→更新 frame,验证:容器 1032x1280@2x cell16x35→129x73tmux 精确采纳)。剩余:完整 scrollback仍靠 capture-pane 抓当前屏pause 流控(%pause/%continue,防单 pane 刷屏split/kill pane 手势tmux<3.1 降级路径未做。
  • tsnet 首连:已加 dial 重试;更优是等 peer online 再 dial读 PeersJSON online 状态)。
  • 签名/真机CODE_SIGNING_ALLOWED=NO,仅模拟器;上真机/TestFlight 需配置签名 + entitlementstsnet 用户态无需 NE entitlement利好审核
  • gitmain 分支已有 6 次提交(初始 + M2/M3/tmux多pane/tmux resize/M4a/M4b工作区干净未 push 远端(无 remote。commit/push 只在用户明确要求时。artifacts/*.xcframeworkCSSHCore/TsnetBridge/MoshCore+ vendor/mosh+vendor/protobuf 源码均有意提交(克隆即可构建,无需网络);构建中间产物(vendor/build/mosh-outvendor/protobuf/build-*)已 gitignore。
  • 多平台iOS target 已跑macOS target本地 PTY, TXPTY)未建。

7. 下一步建议M0~M4 + tmux 多pane/resize 均已完成并验证;以下按价值)

  1. 真机化(最大缺口,多项验证只在模拟器):配置签名 + entitlementstsnet 用户态无需 NE利好审核→ 上真机/TestFlight。真机上补验三件模拟器测不了的SE 硬件密钥生成+签名+keychain 持久化M4b 现回退软件+文件;SigningKeyProvider 已备 SE 路径);②mosh 深挂起恢复(锁屏数分钟过 WG 180s 过期线 / socket defunctMoshRelay.Rebind+hop_port);③签名后 Keychain 可用(当前未签名 SecItem 全 -34018host key pin 才回退到文件)。
  2. 产品化:主机列表持久化(签名后用 Keychain 存凭据/host 配置)、多账号多 tsnet 节点(架构决策 §2.6:每 host 绑 egress禁多 tailnet 盲探、软键盘运维工具栏GhosttyKit 内建,接线即可)、多标签(非 tmux
  3. 打磨(可选,不阻塞)
    • 清理诊断埋点:SSHTerminalModel/TmuxController/iosclient.ccNSLog("MOSHDBG/TXM3/TMUXDBG/M4DBG …") + moshiosbridge.cc 的 fwrite("Hello from the Bridge!")
    • moshactivateMosh 写死 80x24 初值(靠首次 resize 纠正mosh 接管后 idle SSH 未关;MoshSession.close() 后 mosh 线程可能滞留;远端累积 detached mosh-server每次 mosh-server new)需清理策略。
    • tmuxpause 流控(%pause/%continue 防单 pane 刷屏)split/kill pane 手势;外接键盘 Cmd+Opt+方向 导航 pane完整 scrollback现靠 capture-pane 抓当前屏tmux<3.1 降级路径。
    • M4libssh2_session_method_pref 锁定 pinned hostkey 类型防算法漂移误报host key pin 的 UI 管理(查看/删除已信任)。
    • tsnet 首连:更优是等 peer online 再 dial读 PeersJSON online 状态)。
  4. 冷启动 mosh 重连(大活)mosh 进程被杀后接原 detached 会话——受 SSP 序号防重放限制,需持久化 MOSH_KEY+port 且改 mosh 序列化客户端状态Blink 式)。价值中等,成本高。
  5. macOS target:本地 PTYTXPTY)未建。
  6. M5 合规提审:用户指示暂缓。

8. 测试资源(用户提供,凭据勿写入仓库/勿硬编码,每次向用户索取)

  • 一台局域网 macOS(有 tmux + mosh-server,如 MacBook-Air-M1fish 为默认 shellmosh-server/opt/homebrew/bin 且交互 shell PATH 可见)、一台 tailnet 内 online 的 Ubuntu VM(vohive-vm)、一个 tailnet auth keyephemeral可能过期用户会给新的。用于直连 SSH / SSH-over-tsnet / tmux / M2 mosh 端到端验证。凭据每次向用户即时索取。
  • tailnet 是 tail3b5057.ts.net已见节点vohive-vm(linux)、iphone-air、ipad-pro、macbook-air-m1、macbook-air。

9. 工作纪律(务必遵守)

  • 绝不臆想工具结果:调用后等真实返回再推理,绝不编造 BUILD/日志/截图。曾因连续编造工具输出(含假 BUILD SUCCEEDED、错误归因 GhosttyKit而误诊——真相全靠老实 Read 完整 log。此规则已写入全局 ~/.claude/CLAUDE.md
  • 每步用真实证据验证:swift test 看真实计数、xcodebuild 看真实 BUILD 结果、模拟器 screenshot + Read 看真实画面。
  • 与用户用简体中文commit/push 只在明确要求时。

10. UI 产品化 + 渲染修复2026-07-24 本轮)

10.1 修复的 3 个渲染/tmux 显示 buge2e 复现+修复+回归,均模拟器 idb 点击+截图取证)

  1. 终端底部残影旧帧钉屏底ghostty 在 iOS 用 addSublayer: 挂 IOSurfaceLayerMetal.deinit 从不 removeFromSuperlayer(上游 iOS-only 缺陷SwiftUI attach 抖动导致同一 view 多次建 surface 时旧层泄漏、冻结小尺寸首帧,被 CA topLeft gravity 钉在屏底。修vendor UITerminalView.platformSetup 建 surface 前清现存子层 + TerminalSurfaceCoordinator.onSurfaceFreed 钩子(tearDownSurface 后回调)在 commonInit 里摘子层。(诊断经 Fable 顾问在 ghostty 源码层锚定。)
  2. tmux 切 tab 切回旧 tab 内容消失TmuxTabbedView 原只渲 activeWindow 且 .id(win.id),切走销毁 pane 视图→释放 surface→内容丢失。修改成所有窗口常驻挂载、仅激活可见ZStack+opacity+allowsHitTestingsurface 不释放、内容保留、后台窗口持续收 %output。
  3. tmux pane 内容钉底/顶部残行pane 的 ghostty grid 由容器像素几何决定tab 条吃高度→73 行),而 attach 用 raw 全屏 75 行 capture75 行快照喂 73 行 grid 溢出上滚。修:(A) 容器几何测量上移到 TmuxTabbedView 内容槽(破鸡生蛋死锁);(B) TmuxController attach kickoff 改 attachAcked ∧ containerKnown 双条件(kickoffIfReady(C) capture-pane 铺快照前剔尾部空行。

10.2 UI 产品化(对标 Moshi getmoshi.app深蓝黑底 + 终端绿 + 圆角卡片)

  • 设计系统(颜色封装,为主题系统)apps/TerminalX/iOS/Theme.swiftTXPalette(所有色角色 struct)+TXThemeManager:ObservableObject(环境注入);结构量走 enum TX(圆角/间距/等宽字体)。
  • 统一主题:从 GhosttyTheme 抽离 4 套 Catppuccin(Mocha/Macchiato/Frappé/Latte),每套同时定义 app 配色+终端配色(ghosttyNameterminalTheme)。设置里选主题 → app 与终端一起变色 + 持久化(UserDefaults(txThemeStorageKey))ContentView .onChange(palette.id)→model.applyTerminalTheme();新终端/pane 经 TerminalTheme.txDefault 读持久化。
  • 主屏/主机管理页(ContentView.HomeView):主机卡(HostCard)+FAB 加主机(AddHostSheet)+设置(SettingsStubView 含主题选择器)HostStore(Application Support/hosts.json 持久化,密码暂存本地/TODO Keychain);点卡片 SSHTerminalModel.connect(to:)
  • 终端外壳 chrome(ContentView.TerminalScreen+TerminalTopBar):顶栏(圆形返回钮+会话标题model.connectionTitle+传输徽章model.moshEngaged?mosh青:SSH绿+连接态点+状态提示)TmuxTabChip 绿 pill 化;底部 GhosttyKit 工具栏深色自适应;终端默认 Catppuccin Mocha。
  • 验证iPad mini(A17 Pro) live 验证全流程(加主机→连接→终端→切 tab→分屏点焦点→主题切换 app+终端同步)。

10.3 待续UI

  • 会话缩略卡区Moshi 首屏实时终端预览横滑,需会话管理);设置页字体/工具栏配置;导航壳打磨;终端工具栏 accent tintinputAccessoryStyle SwiftUI 未暴露需桥接)。
  • 无头 UI 测试:idb ui tap x yPython3.14 需事件循环 shim /tmp/idbrun.py;坐标=屏幕像素×0.5,注意设备朝向)。诊断埋点(GKDBG/txDbg*)已清理MOSHDBG/TMUXDBG/M4DBG 仍在(旧 cleanup-todo)。