业务逻辑(用户定稿): - 点主机总是新建会话(会话:主机=多:1);activate 选中断线会话自动重连 - 「直接输入」凭据保存时自动落 Keychain(去重复用),主机只存引用 - 主机编辑页可就地新建 Tailscale 连接;删无人引用的旧 HostsListView 首页/弹框减法: - 删侧边栏「连接新主机」、无 tmux 横幅与安装引导、页头「新建连接」 - 搜索移到「全部主机」行;添加主机虚线卡与主机卡等高(骨架复刻) - 会话卡加 ✕ + 复用终端页同款二次确认 - 会话选择器 sheet → 项目风格居中弹框(去手柄/Header/最近) 终端页修复: - 侧边栏会话列表改创建顺序,切换不再重排抖动 - 焦点高亮上移到 pane 容器边框(四边完整,去负 padding) - 软键盘双重让位修复:画布忽略键盘 + TXKeyboardObserver 显式让位一次 - 自动聚焦:focusTick 脉冲强制翻转 FocusState + surface 就绪补聚焦; vendor 补 iPadOS 触摸板 indirectPointer 的 becomeFirstResponder Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
69 KiB
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.xcframework(CommonCrypto 后端,含 arm64 模拟器 slice);tsnet 桥加 UDP relay(StartMoshRelay);MoshSession(pipe↔FILE*/pthread/SIGWINCH)+MoshConnectScanner(TXCore)+SSHTerminalModel编排(SSH 引导 mosh-server→解析 MOSH CONNECT→接管)。
- LAN 直连端到端验证(192.168.9.199):mosh-server 存活 104s≫60s 无客户端超时+子 shell+心跳=真连。
- mosh over tsnet 端到端验证(vohive-vm 100.69.201.101):app 日志
parsed MOSH CONNECT port=60002→relay up localPort=63542→activateMosh 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,不能跑本地 shell);macOS 才支持本地 PTY。
- 分发目标 App Store(M5,已暂缓)。
- 四大核心:libghostty(终端引擎) · mosh · Tailscale tsnet(用户态,非 NetworkExtension) · tmux control-mode(-CC) hook。
2. 关键架构决策(已定,勿推翻)
- 终端引擎用 libghostty,经社区 GhosttyKit(
Lakr233/libghostty-spm,已 vendor 到vendor/libghostty-spm)。TerminalViewState是ObservableObject;喂字节入口InMemoryTerminalSession.receive(Data);SwiftUI 视图TerminalSurfaceView(context:)。 - Tailscale 走 tsnet 用户态(Go,gomobile bind →
TsnetBridge.xcframework),不用 NetworkExtension(避开 VPN entitlement + 50MB 内存墙)。 - SSH 用 libssh2 + mbedTLS(
CSSHCore.xcframework),支持传入外部 fd。 - SSH-over-tsnet = fd 桥:
tsnet.Dial()得 net.Conn(无真实 fd)→socketpair(AF_UNIX)+ 双向 io.Copy 泵 → 把另一端 fd 交给 libssh2。见vendor/tsnet-bridge/tsnetbridge/bridge.go: DialTCPFD。 - 一进程只一个 Go runtime、只一次 gomobile bind;C/C++ 库符号隐藏只导出 C API。
- 多账号(未实现):多 tsnet 节点并存;每 host 绑 egress profile(Direct/tailnet);禁止多 tailnet 并发盲探(重叠 100.x/同名会误连)。
3. 环境(决定构建方式,极重要)
- 带认证的 HTTPS 代理(
HTTPS_PROXY=...@...:38128)。github.comrelease blob 能 curl;go.dev / ghcr.io 被代理 abort;SwiftPM 不支持带认证代理 → 所有 SPM 依赖 vendor 本地化。Go 模块用goproxy.cn。 - Go 1.26.5 在
~/.local/go(不在默认 PATH;经 aliyun 镜像 curl 装),gomobile/gobind 在~/go/bin,go 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.xcframework;UDP relay over tsnet;LAN 直连 + 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-keygen,MITM 关闭)+ 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.swiftTmuxEvent.swiftTmuxIDs.swiftTmuxOutputDecoder.swiftTmuxLayout.swiftTmuxControlSequence.swift(DCS 检测)—— 纯解析。TXCore 全部单测共 50(40 swift-testing + 10 XCTest:tmux 解析/SessionMachine含mosh三相/MoshConnect/SSHWire),swift test --package-path packages/TXCore全绿。Session/SessionMachine.swift——reduce(event)->[Effect]会话状态机(指数退避重连/后台冻结/前台恢复)。M3 加 mosh 三相moshActive/moshParked/moshResuming+ 事件moshEstablished/moshHealthy/moshExited/resumeWatchdogFired+ 效果nudgeResume/scheduleResumeWatchdog/teardownMosh:mosh 接管即拆 SSH、后台冻结不重建、前台打唤醒脉冲等 SSP 续、两次脉冲无效兜底全量重建。
packages/TXTransport/Sources/TXTransport/Transport.swift(协议+SSHConfig+SSHError)、SSHSession.swift(libssh2;preconnectedFD支持外部 fd;事件循环 poll+非阻塞)。TXTransport/MoshSession.swift(M2):驱动mosh_main;pipe↔FILE* 桥(in/out)、Foundation.Thread 跑阻塞主循环+pthread_self()供pthread_kill(SIGWINCH)resize、忽略 SIGPIPE、setenv UTF-8 locale。CSSH/(C target:libssh2 头 + modulemap,供import CSSH)、CSSHCore(binaryTarget,library-only)。CMosh/(C target:moshiosbridge.h(mosh_main)+modulemap+占位 shim.c,供import CMosh)、MoshCore(binaryTarget,library-only);TXTransport 链libc++/libz(mosh 是 C++ 且 compressor 用 zlib;CommonCrypto 属 libSystem 自动解析)。TXCore/Mosh/MoshConnect.swift:MoshConnectScanner流式扫描MOSH CONNECT <port> <key>(注意 Swift 视\r\n为单 Character,行尾判定须用isNewline)。TXCore/SSH/(M4):SSHWire(string/mpint 编码)/ECDSAConv(DER→SSH 签名·P256 blob)/HostKey(opensshFingerprint·evaluate·HostTriple·KnownHostsStore 协议),SSHWireTests 10 单测(真实指纹向量+高位 DER)。
- M4 安全:
TXTransportSSHConfig.hostKeyVerifier(闭包)+SSHError.hostKeyMismatch+SSHSigner协议+Authentication.publicKeyCallback;SSHSession握手后libssh2_session_hostkey校验、sshSignCallback(@convention(c)+malloc 出参)接libssh2_userauth_publickey。app:KeychainKnownHostsStore(文件后端 TOFU pin)、SigningKeyProvider(SE 优先/软件回退+文件持久化,SecKeyCreateSignature)、mismatch alert(ContentView)。 - M3 快恢复链:
bridge.go加Node.WakeUp()(tsnetInjectEvent+MagicSock.Rebind/ReSTUN) 与MoshRelay.Rebind()(同端口重开 loopback、泵按 socket 生成容错);iosclient.cc加g_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 缓冲)、TransportHolder、TsnetManager(tsnet 节点 up/dial+重试)、TsnetError、TmuxRouter(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、autoConnectIfConfigured(启动参数)。ContentView.swift—— 分支:tmuxController != nil→TmuxTabbedView(tab 条+活动窗口);否则RawTerminalView;TsnetProbeView(纯自检);ConnectionForm。TmuxController.swift—— tmux 网关:消费解析事件;pane-per-surface(TmuxPaneSurface=session+state+gate/pane);TmuxWindow持 panes 字典 +visibleLayout(渲染)/fullLayout(diff);surfaceByPaneO(1) 路由 %output;applyLayoutreconcile(增删留);attach:refresh-client -C WxH→list-windows(带 layout)→每 panecapture-pane(awaitingCapture丢弃快照前 %output);输入绑 pane→send-keys -t %self;tap→select-pane。字节经SSHTerminalModel的TmuxByteChannel(AsyncStream 单消费者)保序入网关。TsnetProbe.swift—— M1 自检视图(加入 tailnet + 对端列表)。TerminalXApp.swift—— @main。
- 数据流(直连):transport.onBytes →
TmuxRouter.feed→ (raw)OutputGate.deliver→session.receive→libghostty;(检测到 DCS) →TmuxController.feed→解析→各 window。输入:libghostty→session write 闭包→transport.send(tmux 模式→send-keys)。 - 数据流(tsnet):
TsnetManager.ensureUp(auth key)→dialFD(socketpair fd)→SSHSession(preconnectedFD:)。
6. 已知问题 / TODO(继续工作的入口)
- M4 安全(已完成,剩真机 SE):host key TOFU pin(
SSHConfig.hostKeyVerifier→appKeychainKnownHostsStore文件后端) + publickey 认证(Authentication.publicKeyCallback(SSHSigner)→SSHSessionCsshSignCallback→SigningKeyProvider)。剩:真机验证 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→129x73,tmux 精确采纳)。剩余:完整 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 需配置签名 + entitlements(tsnet 用户态无需 NE entitlement,利好审核)。 - git:main 分支已有 6 次提交(初始 + M2/M3/tmux多pane/tmux resize/M4a/M4b),工作区干净,未 push 远端(无 remote)。commit/push 只在用户明确要求时。
artifacts/*.xcframework(CSSHCore/TsnetBridge/MoshCore)+vendor/mosh+vendor/protobuf源码均有意提交(克隆即可构建,无需网络);构建中间产物(vendor/build/mosh-out、vendor/protobuf/build-*)已 gitignore。 - 多平台:iOS target 已跑;macOS target(本地 PTY,
TXPTY)未建。
7. 下一步建议(M0~M4 + tmux 多pane/resize 均已完成并验证;以下按价值)
- 真机化(最大缺口,多项验证只在模拟器):配置签名 + entitlements(tsnet 用户态无需 NE,利好审核)→ 上真机/TestFlight。真机上补验三件模拟器测不了的:①SE 硬件密钥生成+签名+keychain 持久化(M4b 现回退软件+文件;
SigningKeyProvider已备 SE 路径);②mosh 深挂起恢复(锁屏数分钟过 WG 180s 过期线 / socket defunct,验MoshRelay.Rebind+hop_port);③签名后 Keychain 可用(当前未签名 SecItem 全 -34018,host key pin 才回退到文件)。 - 产品化:主机列表持久化(签名后用 Keychain 存凭据/host 配置)、多账号多 tsnet 节点(架构决策 §2.6:每 host 绑 egress,禁多 tailnet 盲探)、软键盘运维工具栏(GhosttyKit 内建,接线即可)、多标签(非 tmux)。
- 打磨(可选,不阻塞):
- 清理诊断埋点:
SSHTerminalModel/TmuxController/iosclient.cc的NSLog("MOSHDBG/TXM3/TMUXDBG/M4DBG …")+ moshiosbridge.cc 的fwrite("Hello from the Bridge!")。 - mosh:
activateMosh写死 80x24 初值(靠首次 resize 纠正);mosh 接管后 idle SSH 未关;MoshSession.close()后 mosh 线程可能滞留;远端累积 detached mosh-server(每次mosh-server new)需清理策略。 - tmux:pause 流控(
%pause/%continue防单 pane 刷屏);split/kill pane 手势;外接键盘Cmd+Opt+方向导航 pane;完整 scrollback(现靠 capture-pane 抓当前屏);tmux<3.1 降级路径。 - M4:
libssh2_session_method_pref锁定 pinned hostkey 类型防算法漂移误报;host key pin 的 UI 管理(查看/删除已信任)。 - tsnet 首连:更优是等 peer online 再 dial(读 PeersJSON online 状态)。
- 清理诊断埋点:
- 冷启动 mosh 重连(大活):mosh 进程被杀后接原 detached 会话——受 SSP 序号防重放限制,需持久化 MOSH_KEY+port 且改 mosh 序列化客户端状态(Blink 式)。价值中等,成本高。
- macOS target:本地 PTY(
TXPTY)未建。 - M5 合规提审:用户指示暂缓。
8. 测试资源(用户提供,凭据勿写入仓库/勿硬编码,每次向用户索取)
- 一台局域网 macOS(有 tmux + mosh-server,如 MacBook-Air-M1;fish 为默认 shell,
mosh-server在/opt/homebrew/bin且交互 shell PATH 可见)、一台 tailnet 内 online 的 Ubuntu VM(vohive-vm)、一个 tailnet auth key(ephemeral,可能过期,用户会给新的)。用于直连 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 显示 bug(e2e 复现+修复+回归,均模拟器 idb 点击+截图取证)
- 终端底部残影(旧帧钉屏底):ghostty 在 iOS 用
addSublayer:挂 IOSurfaceLayer,但Metal.deinit从不removeFromSuperlayer(上游 iOS-only 缺陷),SwiftUI attach 抖动导致同一 view 多次建 surface 时旧层泄漏、冻结小尺寸首帧,被 CAtopLeftgravity 钉在屏底。修:vendorUITerminalView.platformSetup建 surface 前清现存子层 +TerminalSurfaceCoordinator.onSurfaceFreed钩子(tearDownSurface后回调)在commonInit里摘子层。(诊断经 Fable 顾问在 ghostty 源码层锚定。) - tmux 切 tab 切回旧 tab 内容消失:
TmuxTabbedView原只渲 activeWindow 且.id(win.id),切走销毁 pane 视图→释放 surface→内容丢失。修:改成所有窗口常驻挂载、仅激活可见(ZStack+opacity+allowsHitTesting),surface 不释放、内容保留、后台窗口持续收 %output。 - tmux pane 内容钉底/顶部残行:pane 的 ghostty grid 由容器像素几何决定(tab 条吃高度→73 行),而 attach 用 raw 全屏 75 行 capture,75 行快照喂 73 行 grid 溢出上滚。修:(A) 容器几何测量上移到
TmuxTabbedView内容槽(破鸡生蛋死锁);(B)TmuxControllerattach kickoff 改attachAcked ∧ containerKnown双条件(kickoffIfReady);(C)capture-pane铺快照前剔尾部空行。
10.2 UI 产品化(对标 Moshi getmoshi.app;深蓝黑底 + 终端绿 + 圆角卡片)
- 设计系统(颜色封装,为主题系统):
apps/TerminalX/iOS/Theme.swift=TXPalette(所有色角色 struct)+TXThemeManager:ObservableObject(环境注入);结构量走enum TX(圆角/间距/等宽字体)。 - 统一主题:从 GhosttyTheme 抽离 4 套 Catppuccin(Mocha/Macchiato/Frappé/Latte),每套同时定义 app 配色+终端配色(
ghosttyName→terminalTheme)。设置里选主题 → 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 tint(
inputAccessoryStyleSwiftUI 未暴露需桥接)。 - 无头 UI 测试:
idb ui tap x y(Python3.14 需事件循环 shim/tmp/idbrun.py;坐标=屏幕像素×0.5,注意设备朝向)。诊断埋点(GKDBG/txDbg*)已清理;MOSHDBG/TMUXDBG/M4DBG 仍在(旧 cleanup-todo)。
11. Hosts / Keychain / Tailscale 三分离管理(2026-07-24 本轮,对标 Termius)
把连接拆成 Hosts / Keychain(凭据) / Tailscale 连接 三者分别管理,iPad 优先。设计阶段咨询 Fable 顾问(Q1–Q5 裁决)。BUILD SUCCEEDED + 模拟器 idb 截图全流程验证 + TXCore 40 tests 绿。
11.1 数据模型(app 层新文件)
Credential.swift:Credential(凭据 meta,无机密) +CredentialSecret(自描述机密)。Kind: password / privateKey(source: pasted/imported/generated, publicKeyBlob) / secureEnclave(占位)。CredentialStore.swift:@MainActor protocol CredentialStore(list/secret(for:)/upsert/delete,meta 与机密分离) +FileCredentialStore(沙盒credentials.json,版本化信封{"scheme":"plaintext-v1"},0600+isExcludedFromBackup+completeFileProtection)。不自造加密:无根信任下伪安全;签名后换 Keychain 后端即可(协议已抽象)。TailscaleStore.swift:TailscaleConnection(每个=一个常驻 tsnet 节点;stateDirName="tsnet-<uuid>"创建即固化终身不变——tsnet 无文件锁,同 Dir 双 Up 损坏状态) +TailscaleStore(tailnets.json,无机密;authKey 注册后即弃)。HostStore.swift:SavedHost改为auth: AuthRef(.inlinePassword / .credential(id)) +tailscaleID: UUID?;自定义 decoder 迁移旧 inline username/password 为.inlinePassword(不造幽灵凭据)。
11.2 签名映射 CredentialAuth.swift(复用既有三认证路径,传输层零改动)
- password →
.password;生成软件 P-256 →SigningKeyProvider.from(x963:)→.publicKeyCallback(路径A);粘贴/导入 PEM →.privateKey(libssh2publickey_frommemory路径B)。 - 关键:
SSHSession.authenticate()三分支(.password/.privateKey/.publicKeyCallback)早已全部实现,本次无需动 TXTransport。 SigningKeyProvider加from(x963:)/generateSoftwareP256()/secureEnclaveAvailable()(非持久探测,签名真机自动点亮 Face ID UI)。openssh-key-v1 mbedTLS 不认 → UI 警告提示ssh-keygen -p -m PEM转换。
11.3 多 tsnet 节点 TsnetRegistry.swift(替换旧单例 TsnetManager)
- 单例
@unchecked Sendable+锁,按 connID 懒启动/Up 后常驻/LRU 限 3/in-flight 去重。Fable 经 tsnet v1.102.0 源码确认一进程多tsnet.Server官方支持(dial 走具体 node 句柄无全局路由→重叠 100.x 不误连;红线仅 stateDir 唯一)。 - 坑:
TsnetbridgeNode/MoshRelay非 Sendable,不能在Task.detached捕获或跨 actor 传 → mosh relay/wakeUp 收进 registry 按 connID 操作,model 只留activeConnID(不再持 node)。
11.4 连接编排(SSHTerminalModel)
connect(to:)解析 AuthRef+tailscaleID →EgressPlan(.direct/.tsnet(connID,stateDir,hostname,authKey));startConnect统一构造 config,host key pin 命名空间=egressPlan.egressKey(direct/tsnet-<uuid>)。删实例级tsnetAuthKey/TsnetManager单例。- 无头验证:
-txTsnetKey合成临时连接(固定 connID + stateDirtsnet-main复用模拟器身份)→ 启动参数矩阵不回归(已验证直连 SSH 到 127.0.0.1:22 走通 host key 校验+密码认证)。 model.bind(credentials:tailscale:)由 ContentView 注入两 Store。
11.5 UI(ContentView + 4 个 View 文件 + UIComponents.swift)
RootSplitView=NavigationSplitView侧栏(§连接:主机 / §保险库:Keychain·Known Hosts / §网络:Tailscale / 设置)+详情;iPhone 紧凑自动坍缩。终端改.fullScreenCover($model.showsTerminal)全屏盖(退出回列表,状态保留)。HostsView.swift(现只剩 HostEditorView:认证 segmented[直接输入/从 Keychain 选]+出口 Picker[直连/各 Tailscale,可就地新建];旧HostsListView/HostCard/SessionStatusRow已删——无人引用且"点卡片立即进终端+复用会话"与定稿流程相悖)、KeychainView.swift(列表+CredentialEditorView:密码/SSH密钥[生成·粘贴·导入]/Face ID 占位)、TailscaleView.swift(列表+编辑器 name/hostname/authKey+注册按钮经 registry Up+SelfIP,编辑器带onSave供主机页就地新建后自动选中)、SettingsView.swift(主题+关于 +KnownHostsViewpin 查看/删除)。- 直接输入自动对象化(2026-07-26 用户定稿):主机编辑里「直接输入」的用户名/密码在保存时自动落成 Keychain 凭据(同用户名同密码去重复用),主机只存
.credential(id)引用;.inlinePassword仅保留给启动参数/fixture 合成主机与旧数据迁移。Tailscale 同理:主机页可「新建 Tailscale 连接」(sheet 打开编辑器,保存即自动选中)。三对象(主机/凭据/Tailscale 连接)各自增删改查、互相引用。
11.6 未验证(需用户实时资源)
真 tsnet 多节点 Up(需 authkey)、密钥/凭据成功登录(需服务器信任 key)、mosh over 新 egress plan。这些在有凭据时按 §8 流程补测。
12. Demo → 商业化 UI/UE 改造(2026-07-25 起,按 Claude Design 定稿)
设计定稿(hifi,含精确 token):docs/design/UI-DESIGN-HANDOFF.md + terminalX-iPadOS.dc.html(13 屏画板)。
渲染对拍:python3 docs/design/render-screens.py 拆屏 → headless Chrome 逐屏截 PNG。规划级裁决咨询 Fable 顾问。
总判断:差距不在样式而在结构。三处必须先拆的地基——① chrome 从 Catppuccin 派生改为 Nocturne 中性阶 + 恒定 blurple;② NavigationSplitView+fullScreenCover 改路由 + 会话常驻画布;③ 单会话模型改多会话并存。
12.1 路线图(6 阶段,UI-1/UI-2 已完成)
| 阶段 | 内容 | 状态 |
|---|---|---|
| UI-1 | 设计令牌地基(Nocturne chrome + blurple + JetBrains Mono) | ✅ 完成 |
| UI-2 | 导航与多会话状态模型(AppRouter/SessionManager/SessionCanvas) | ✅ 完成 |
| UI-3 | 终端主界面:56pt 轨道 + 浮起双胶囊 + 侧边栏三态 | ✅ 完成(pane 缝拖拽待做) |
| UI-4 | 首页会话卡 + 关闭确认 + 分屏菜单 + pane 拖拽 | ✅ 完成(分屏功能后被用户裁决整体移除,见 12.3k) |
| UI-5 | 设置·外观 + 键盘体系(GCKeyboard 检测 + 全套 ⌘ 快捷键) | 待做 |
| UI-6 | 竖屏参数化 + iPhone 首版 | 待做 |
12.2 UI-1 设计令牌(Theme.swift 重写)
- 三层分工:
TXAccent(Nocturne blurple 8 档,恒定不随主题变,只做线/点/选中底/文字)·TXChrome(chrome 中性阶 14 档,4 张预生成表)·TXFlavor(Catppuccin flavor,驱动终端 + 状态语义色)。TXPalette是门面,旧取色名(p.accent/p.textPrimary…)保留为 computed,逐屏重写时再换直通名。 - chrome 表推导:Mocha 用设计交付的精确锚点值;Macchiato/Frappé 按 flavor base 相对 Mocha 的 HSL 差平移(文字阶抬升幅度收一半,保对比度);Latte 是唯一浅色,整体反转手工写(仍不用纯黑纯白)。切 flavor → chrome 变阶、accent 不变。
- JetBrains Mono 四权重 vendor 进
apps/TerminalX/Resources/Fonts/(附 OFL.txt,OFL-1.1 要求随分发)。必须平铺 bundle 根(UIAppFonts值不带目录)→ project.yml 用 group 而非 folder reference。TX.Font.mono按 weight 映射 PostScript 名,缺失时 SwiftUI 自动回退。
12.3 UI-2 导航与多会话(新 AppRouter.swift/SessionManager.swift,SSHTerminalModel→TerminalSession)
AppRoute = .home | .terminal(UUID);SidebarState = .collapsed|.overlay|.pinned(overlay 不占位、不 resize;pinned 钉住各 resize 一次)。TerminalSession加身份:id/host/isLive(原 showsTerminal,不再驱动导航)/isMinimized/lastActiveAt/link(LinkState 五态,由 phase+egress 推导)/displayName/initials/isTmuxSession。TXCore、传输层零改动(50 tests 保持绿)。SessionManager:多实例持有、open(host:)总是新建会话(2026-07-26 用户定稿:会话与主机不是一对一,再点同一主机 = 再开一条并行会话;早期"复用同主机会话"的语义已废除)、activate(_:)(选中既有会话:聚焦 + 已断开的自动重连并重新判定 tmux/原生)、focus/minimize/close、前后台广播、⌘1…9 索引、首页排序(最小化置顶 + 最近活跃)。会话列表持久显示(无论连接状态),只有用户显式关闭才移除。ContentView→RootView(ZStack{SessionCanvas, HomeShell})。SessionCanvas 常驻挂载所有会话(详见 CLAUDE.md 坑:摘除即丢内容 + matchedGeometry 要求同层级)。host key alert 下移到TerminalScreen(每会话一份,后台会话也能弹安全告警)。- 无头验证脚手架(DEBUG):
UIPreviewFixture假会话 +terminalx://ui/*导航驱动 +scripts/tx-shot.sh截图定向。坑:假会话必须isSyntheticFixture=true从生命周期广播里排除,否则 scenePhase→active 广播经syncUI()把注入状态覆盖回 idle。 - 验收(四步截图取证,全通过):A 终端会话1 → B
ui/minimize回首页(3 会话并存 + mac-studio 标「最小化中」置顶)→ Cui/session/2切会话2(独立内容 + tsnet 徽标)→ Dui/session/1回会话1 内容原样。
12.3b UI-3/UI-4 两屏还原(新文件 TXParts / RailSidebar / TerminalChrome / HomeView)
TXParts.swift— 设计稿反复出现的小件:TXStatusDot(8pt + 2pt 挖底描边) ·TXInitialsAvatar(26/38pt 两字缩写 + 右上状态点) ·TXSectionLabel(600 10px ls .09em uppercase) ·TXBadge·TXChip·TXSearchField(32pt) ·TXButton·TXIconButton(视觉 34、命中 44) ·TXTerminalMirror(终端文本镜像,底对齐)。RailSidebar.swift—RailView(56pt/竖屏 48pt 轨道:logo 不可点 → 搜索 → 首页 → 主机头像+状态点 → 虚线+ → TMUX 段 window 数字 → 状态点 → »展开 → 齿轮) +SidebarPanel(288pt,遮罩/Pin 共用同一套内容) +SidebarSessionRow/SidebarWindowRow。开合语义唯一:logo 不可点,折叠态»展开在轨道底、展开态«收起在标题右,Pin 是独立按钮只在展开态出现。TerminalChrome.swift— 浮起胶囊(.ultraThinMaterial+ 半透底 + 描边 + 圆角 11):左上标题胶囊(红黄点/竖分隔/状态点/主机名/window title)+ 右上三格状态胶囊(tsnet | mosh | tmux,只染出问题的那层)+ 底部 34pt 提示条(文案随侧边栏三态切换)。HomeView.swift— 页头(34px 标题 + 副行 + 220pt 搜索 + 新建连接 + 齿轮)→ 活动会话 3 列SessionCard(卡头状态/主机名/tmux/链路 · 文本镜像 · 卡脚 windows·panes·上次活跃 + ⌘n + 回到会话)→ 全部主机 4 列HostTile+ 筛选 chips + 添加卡。重连/离线卡:镜像blur(1.5)+rgba(24,26,40,.72)遮罩 + 转圈 + 「屏幕内容已缓存,恢复后原样接回」。- 舞台:pane 区顶部留
TX.Layout.capsuleInset(86pt) 预留带 +clipped()防溢出;pane 之间 2pt 缝(各让 1pt);TmuxStage去掉 tab 条(window 切换移到轨道/侧边栏),保留「全 window 常驻挂载 + opacity」与容器几何单一来源。 - 缩略卡的「真实回滚」:
TerminalSession.captureSnapshot()用readViewportText()抓尾部 32 行(剔尾部空行),首页可见时每 1.5s 轮询 + 最小化前抓一次。没有第二个 surface。 - fixture 扩展:
UIPreviewFixture.hosts(7 台 + 分组/os)、tailnets、每条会话的TmuxSummary注入;HostStore.injectFixtureHosts/TailscaleStore.injectFixtureConnections只进内存不写盘。 - 验收:四态截图对拍设计稿 —— 折叠轨道(08) · 遮罩态(00) · Pin 态(01,终端真的变窄) · 首页(09)。TXCore 50 tests 保持绿。
12.3c 像素级走查修正(用户走查 + 逐屏对拍,2026-07-25 二轮)
走查方法(后续每屏都照这个来):scripts/tx-shot.sh 截图 → python3 把设计稿 PNG 与截图统一缩放到 1366×1024 → 裁同一区域上下/左右并排 → 再对元素列做垂直投影扫描(逐行找与底色不同的段),把元素 y 坐标量化对比。目测靠不住(缩略图里 #1E1E2E 与 #11121C 分不出)。
修正项:
- 沉浸不彻底(最严重):原来把 86pt 预留带做成"pane 容器整体下移",胶囊浮在舞台底色上。改为 pane 容器从舞台顶部就开始(终端底色 + 边框铺满),只把 surface 内容
padding(.top, 86)—— 预留带铺的是flavor.terminalBg,故与终端内容视觉连续,胶囊真正"悬浮在终端之上"。像素采样验证:预留带与内容区同为#1E1E2E。 - 终端页隐藏系统状态栏(设计稿
1b/3a顶部就是胶囊/logo,1e/1g才有状态栏)+ignoresSafeArea(edges: .top)。坑:statusBarHidden不能挂在TerminalScreen上——它常驻挂载(SessionCanvas),会连首页一起隐藏;要挂在RootView上按router.route判断。 - 侧边栏动画:三态切换统一走
AppRouter.sidebarAnimation(.easeOut(0.24),设计要求 200–260ms),面板.move(edge: .leading)滑入 + 遮罩.opacity淡入。 - 底部两区同高:侧边栏脚与舞台提示条共用
TX.Layout.footHeight(34pt),故两侧分割线落在同一 y;侧边栏脚原来的深色块 + 齿轮去掉,改 1ptdivider(设计稿如此)。 - 轨道结构:搜索/首页/设置是裸图标(无底无框),只有 logo 与展开键有框(
TXIconButton(bare:));纵向节奏按扫描到的设计坐标写死(logo 28 · 头像 174/220/266 · TMUX 标签 388 · 数字 408 起);TMUX 段标签用 accent-500 而非中性灰。 - 侧边栏面板:logo 30pt、会话行 48pt、window 行 44pt(实测值);分组分隔线改渐隐 LinearGradient;占位分支补回漏掉的「新建 window ⌘T」行。对齐后与设计稿偏差 ≤9pt(window 区完全重合)。
- 缩略卡:卡头/卡脚分隔线用 1pt
Rectangle(Divider().overlay()压不住默认色);行距fontSize×0.6(设计 11px/1.5);启发式着色 ——readViewportText()只给纯文本,故按行首符号/关键字上色(✓绿error红◇▸青[..]灰 提示符行亮),逼近真实终端密度。
12.3d UI-4 交互闭环(关闭确认 / 分屏菜单 / pane 拖拽)
TmuxController新增命令(纯追加,未动既有链路):detach()(detach-client) ·killSession()·splitWindow(vertical:joinSession:)(split-window/ 跨会话用join-pane -s,不嵌套 tmux)·splitIntoNewSession()(new-session -d+join-pane)·resizePane(cols:rows:)·listSessions()(新PendingKind.listSessions,结果进@Published sessions: [TmuxSessionInfo])。AppModal提到AppRouter(不是视图@State):⌘W/⌘D 快捷键与无头 URL 驱动都要能打开浮层,视图内部状态触达不到。- 关闭确认
CloseConfirmDialog(3c,436/404pt):tmux → 推荐「仅断开 ⏎」(detach-client,服务端继续跑) + 次要红字「结束会话」(kill-session,再点一次才执行);原生窗口 → 琥珀警告 + 危险按钮(设计精确色#8d5560/rgba(92,58,63,.35)/#f5a0ac)。closeSession(kill:)先发 tmux 命令、延迟 250ms 再 teardown 传输层(让%exit走既有onExit)。 - 分屏菜单
SplitMenuView(3b,360pt):不问方向(⌘D/⌘⇧D 本就是两个入口),只列「分到哪里」——当前会话(选中底 + 「当前」)· detached 会话(绿点,因为进程仍在服务端跑 + 上次活跃)· 新建 tmux 会话。前导列统一 20pt 使各行文字左缘一致。原生窗口项灰显标「二期」(需要一台主机多条 SSH channel,见 §12.4),不装作能用。 - pane 拖拽
PaneSplitter:从布局树内部节点算出相邻子节点边界 → 热区 12pt + 把手 44×3pt。拖动中只动视觉(把手位移 + 转强调色),不改 pane frame、不发命令;松手才发一次resize-pane,等%layout-change回来更新真实 frame(与"路线 b"一致:客户端不自行重排)。 - 最小化过渡:离开终端时
opacity + scaleEffect(0.97)(240ms easeOut)。用scaleEffect而非 frame 动画 —— 前者只是渲染变换不触发 PTY resize,后者会让 tmux 反复重排。完整 matchedGeometry 缩回卡片未做(fable 裁决:不对TerminalSurfaceView本体做 matchedGeometry)。 - 验收:三个浮层逐个对拍设计稿(3c 两套文案 + 3b 菜单);最小化往返内容原样、日志无 crash;TXCore 50 tests 绿。pane 拖拽只做了静态编译与逻辑走查,没有真实 tmux 多 pane 会话验证过(fixture 没有 controller)——有测试主机时必须补。
12.3e 接真实数据 + 空状态(UI-5 之前的必要一步)
之前几轮都靠 -txUIFixture 假会话对拍设计稿,真实路径下有数据缺口和裸露的空白屏。本轮补齐:
真实数据打通
- tmux 会话名:kickoff 时顺带
listSessions(),currentSessionName= list-sessions 里 attached 的那个 → 状态胶囊/侧边栏标题显示真实tmux <会话名>(拿不到就只显示tmux,不编造)。 - cwd:
list-windowsformat 追加#{pane_current_path}→TmuxWindow.cwd;标题胶囊显示「window title · ~/src/x」,侧边栏 window 行副行显示「cwd · n panes」。abbreviate()把/home/u/x、/Users/u/x收成~/x。 - 主机的分组 / 系统:
HostEditorView新增「归类」区(分组输入 + 已有分组一键选 chips + 系统描述)。此前SavedHost.group/os只有 fixture 在写,真实主机永远没有分组 → 首页 chips 恒只有「全部」。 - IP 主机名的缩写:
10.255.255.1按字母规则会得到无意义的11→ 纯 IP 取首段前两位(10)。
空状态(真实路径必经)
- 首页首次运行(0 主机 0 会话):终端图标 + 「还没有主机」+ 能力说明 + 「新建连接 / 先配置 Tailscale」。
- 有主机无会话:虚线提示条「还没有在跑的会话 —— 点下面任意主机即可连接」。
- 搜索/筛选无命中:说明是分组还是搜索造成的 + 「清除筛选」。
- 缩略卡无输出:「等待输出…」占位(真实会话刚连上时
readViewportText()还是空的)。 - 侧边栏无会话:「没有在跑的会话」。
- 终端区
TerminalStateOverlay:连接中(转圈 + 分阶段文案 + 已等待 Ns + ≥5s 后出「取消连接」)/ 连接失败(错误详情 + 返回首页 + 重试)/ 已关闭(重新连接)。已连接与重连中不渲染此层,不遮挡终端内容。 - 路由兜底:
route指向的会话已被关闭 → 自动回首页,不停在空画布上黑屏。
传输层修一处真实可用性问题(TXTransport)
SSHSession.connect()原来用裸Darwin.connect,阻塞到系统 TCP 超时(75s+),连不上的主机会让用户对着「连接中」干等。改为connectWithTimeout():置非阻塞 → connect →poll(POLLOUT)等超时 → 查SO_ERROR(可写≠成功,拒绝连接也会变可写)→ 恢复阻塞(libssh2 用阻塞模式)。超时值走新增的SSHConfig.connectTimeoutSec(默认 12s)。tsnet 路径(preconnectedFD)不经此处,不受影响。- 验证:不可达地址 12s 后报「失败:socket("连接失败(12s 超时)…")」,失败态覆盖层给出重试/返回。
12.3f tmux 自动进入(真机反馈修复:连上永远显示「无 tmux」)
根因:全代码库没有任何地方发起 tmux -CC attach。TmuxRouter 只被动检测传输字节流里的 DCS 前导,之前所有 tmux 端到端验证都靠 -txAutoCommand "tmux -CC …" 手动触发 → 真实点击连接只有 raw shell,状态胶囊第三格恒为「无 tmux · 客户端窗口」。不是检测坏了,是从来没人发起。
修复
SavedHost加useTmux: Bool = true(旧数据迁移也默认 true)+tmuxSession: String?(默认main);主机编辑器新增「会话」区(tmux 开关 + 会话名 + mosh 开关,带说明)。TerminalSession.scheduleTmuxBootstrap():连接建立后(mosh 模式则等 mosh 接管后)延迟 2.5s 等 shell 提示符 → 发tmux -CC new -A -s <名>。-A= 同名会话存在就接回(断线原样恢复的关键)。- 探测降级:4s 内没进 control mode →
tmuxUnavailable = true→ 终端顶部行内横幅(设计 2c:说明「关掉 App 后服务端不会继续跑」+「安装并启用 tmux」+ 可关闭)。装完自动重试一次。 exitTmux()重置引导标记 → 重连后重新 attach。- 无头参数:
-txTmux 0|1、-txTmuxSession <名>。
真机端到端验证通过(192.168.99.83,macOS + fish + tmux 3.7b)。过程中查出 3 个真实 bug:
- tab 分隔符被 PTY 吃掉:
list-windows -F "…\t…"的 tab 经 PTY 到 tmux 后变成下划线,整行components(separatedBy:)分不开(count=1)→ title/cwd/会话名全丢、UI 显示兜底名「窗口 N」。改用TmuxController.fieldSep = "|:|"。排查坑:os_log把控制字符显示成_,光看日志无法区分「真下划线」与「被显示替换的 tab」,我因此先误判为 os_log 显示问题——最后靠打印parts.count才确诊。 - UI 不刷新(正是 CLAUDE.md 里「嵌套 ObservableObject 不冒泡」那条坑的另一面):
tmuxSummary/window 列表是读 controller 的 computed property,而轨道/胶囊/侧边栏只订阅 session → 数据解析对了 UI 也不动。修法:TerminalSession用 Combine 订阅controller.objectWillChange并转发自己的objectWillChange(一处解决所有读 controller 的视图)。 - 会话名猜错:
currentSessionName原来取list-sessions里session_attached==1的那个,结果把用户自己终端 attach 的demo当成了当前会话。改用display-message -p "#{session_name}"(权威:本客户端所在会话)。分屏菜单的「当前」判定也随之改为按会话名比较。 - 顺带修正:轨道/侧边栏的 window 编号用
#{window_index}(用户可见的 0/1/2…)而非内部window_id(@2/@3…)。
验证结果(服务器侧交叉验证):
- tmux 自动进入 ✓ 状态胶囊
direct | ssh | tmux main、标题胶囊fish · ~ 163×50、轨道 window0/1/2/3/4(与服务器idx=0..4 active=@3/idx=1完全吻合) - ⌘D 分屏 ✓:
split-window -h后服务器list-panes从%7 164x50变为%7 82x50 + %8 81x50;app 端双 pane 左右并排渲染、2pt 缝、拖拽把手、活动 pane blurple 边框 resize-pane✓:ui/resize/110后服务器 pane 宽度变为110(另一侧自动收缩到 53)——拖拽条的命令链路已验证(手势本身是标准DragGesture)- 会话服务端持久 ✓:app 退出后
tmux ls里测试会话仍在(验证完已 kill 清理) - 新增无头验证命令:
ui/split-now/v|h(跳过菜单直接分屏)、ui/resize/<cols>
12.3g 连接后的会话选择器(用户定稿交互,替换掉「自动 attach 固定会话」)
为什么改:之前实现是连上就 tmux -CC new -A -s main —— 替用户决定了 attach 哪个会话。真实运维里服务器上常有多个会话各跑不同任务,必须让用户挑。
新流程:SSH 连上 → 普通 shell 里跑探测 → 弹选择器 sheet → 按选择 attach。没装 tmux 时不弹框(直接原生终端 + 安装引导横幅,不打扰)。
TXCore/Tmux/TmuxSessionProbe.swift(纯逻辑 + 5 个单测):流式扫描BEGIN…END标记块,产出.available(version:sessions:)/.unavailable。三个坑都在注释里:- PTY 回显:命令本身会被回显,标记若字面出现在命令里,回显行会被当成结果起点 → 写成相邻字符串拼接
"__TX""_SESSIONS_BEGIN"(shell 输出完整标记,回显的是拼接前的样子)。 - 三态要分开:「没装 tmux」和「装了但没会话」的 stderr 都被
2>/dev/null吞掉、标记之间同样是空的,只能靠tmux -V的版本行区分 —— 两者的正确 UI 完全不同(安装引导 vs 让用户新建)。 - 别脏屏:探测命令与输出会打在用户屏幕上 → 命令末尾接
printf '\033[H\033[2J\033[3J'(归位+清屏+清 scrollback,不依赖 terminfo)。刚连上时清掉的只有 motd/shell 欢迎语,首屏反而更干净。
- PTY 回显:命令本身会被回显,标记若字面出现在命令里,回显行会被当成结果起点 → 写成相邻字符串拼接
OutputTap:raw 字节的旁路(同时喂终端渲染与探测扫描器)。抽RawOutputSink协议让TmuxRouter能接受 gate 或 tap —— 不动OutputGate的缓冲/flush 语义(那是修白屏的关键)。SessionPickerSheet.swift(按用户给的设计图):分段控件>_ Tmux|Herdr(置灰) |最近(置灰) + 右侧「跳过 ▷|」;会话行 = 大字会话名 + 「活跃」徽标(被别的客户端占用时另标「已被接入」)+ 副行「N 个窗口 · M 分钟前」;底部「+ 新建 tmux 会话…」(点开就地输入名字)。.presentationDetents([.medium, .large])。TerminalSession:pendingSessionChoice/attachTmuxSession/createTmuxSession/skipTmux(tmuxSkipped让状态胶囊显示「原生窗口 · 不经 tmux」而不是谎报「无 tmux」);探测 6s 超时兜底按无 tmux 处理。- 无头命令:
ui/pick/<会话名>·ui/pick-new/<名>·ui/pick-skip。
时机修正(用户第二轮反馈):选择器属于连接流程的一部分 —— 点主机后留在首页探测、在首页弹选择器、选完才进终端页;不是先把用户丢进空终端再弹框。
SessionManager:open(host:)不再立刻router.enter;改为awaitingEnter集合 +@Published enterRequest(视图层消费后进终端)。evaluateEnter的就绪条件:不等选择 ∧ 不在探测 ∧(已建立 / 无 tmux / 选了原生 / 失败)。失败也进终端 —— 那里有「重试 / 返回首页」,比留在首页只有一行错误更有出路。- 坑:
isProbingTmux必须在.connected那一刻就置位。否则「连接建立」与「探测开始(延迟 2.5s)」之间有空窗,evaluateEnter看到isLive == true就把人送进终端页 —— 第一次改完就是这个症状(选择器弹在终端页上)。 SessionManager也要转发每条会话的objectWillChange(同TerminalSession转发 controller 那条坑),否则首页/选择器不会因会话内部变化刷新。- 首页新增
ConnectingRow(正在连接 / 等待选择 / 失败+重试+取消);liveSessions排除正在连接的会话,避免同一会话既有进度行又有卡片(且卡片里的 tmux 信息此刻还没定,会先显示成「原生窗口」再跳变)。 - 无头验证真实路径:
-txOpenAsHost 1(把启动参数造成一台内存主机后走open(host:),即探测→选择→进终端;与老的-txHost直连路径互斥)。
措辞修正:右上不再是「跳过」(读起来像"跳过某个步骤"、含义模糊),改为**「原生终端」**+ terminal 图标 —— 它是与 tmux 并列的一种连接方式。skipTmux() → useNativeTerminal(),状态胶囊第三格显示「原生窗口 · 不经 tmux」。首版只有 Tmux 与原生两种,所以不做别的出口。
真机验证(192.168.99.83)三条出路全过:
- 探测 →
probe: tmux 3.7b sessions=["demo", "main"],选择器如设计图渲染,终端首屏干净(清屏生效) - attach 已有 →
attach -t demo→currentSession=demo - 新建 →
new -A -s uitest→currentSession=uitest,服务器tmux ls出现uitest: 1 windows (attached)(验完已 kill) - 原生终端 → 状态胶囊
direct | ssh | 原生窗口 · 不经 tmux - 完整真实路径(
-txOpenAsHost)→ 首页「正在连接 192.168.99.83」→ 首页上弹选择器(标题「连接到 …」)→ 选main→ 进终端页tmux main+ 轨道 window0/1/2/3/4
12.3h 质感走查:动效 / 阴影 / 圆角 / 安全区(用户第三轮走查,2026-07-25)
用户反馈四类瑕疵(风格保持不变,只修质感):浮层"特别难展开和消失"缺过渡、胶囊阴影太重不够扁平、终端小圆角与 iPad 屏幕大圆角打架(全屏时右上角被吃掉)、底部混乱没考虑安全区。逐条修完并补了同类问题:
1. 动效令牌化 + 补齐缺失过渡(TX.Motion)
- 新
TX.Motion:standard(0.24 easeOut) /quick(0.14) /slide(edge)/pop(0.96→1 缩放+淡入) /pushBack(首页推远)。统一响应「减弱动态」(UIAccessibility.isReduceMotionEnabled→ 时长减半 + 位移退化为纯淡入淡出)。 - 根因:
AppRouter里只有侧边栏包了withAnimation,route/modal的变更裸着改 → 视图上写好的.transition拿不到动画事务,全退化成硬切。现在enter/goHome/present/dismissModal/toggleSidebar/togglePin全部经animated{},点击 / ⌘ 快捷键 / 无头 URL 三条入口自动同一手感。 TXModalScrim拆成两段转场(遮罩.opacity+ 卡片TX.Motion.pop):整块套一个.transition(.opacity)只是一起变淡,没有弹出感。- 补:无 tmux 横幅显隐、
TmuxInstallSheet、TerminalStateOverlay(连上后淡出)、提示条文案随三态切换、会话卡增减、选中态/状态点变色。 - 按压反馈:新
TXPressStyle(缩放 + 压暗 140ms),替换全部.buttonStyle(.plain)。深色界面里.plain完全没有按下反馈,主机卡/会话卡这种"点下去要等几百毫秒"的入口尤其伤手感。 - 会话卡改成整张一个 Button,卡脚「回到会话 →」退化为文字提示(同一动作做成嵌套 Button 只会抢命中区 + 两套按压反馈)。
-txSlowMotion 1(DEBUG)把所有动效 ×12:simctl io screenshot自身要几百毫秒,抓不到 240ms 的中间帧,没这个开关就无法用截图证明"动效真的在跑"。验证方法:连发三张截图,采样同一像素应得到三个不同的中间值(硬切时三张完全一样)。
2. 阴影扁平化(TX.Elevation,刻意偏离设计初稿)
设计稿给的是 CSS 值(胶囊 0 10px 30px rgba(0,0,0,.55)、对话框 0 30px 80px .7),在深色底上会糊成一团脏斑 —— 半透胶囊叠重投影像"漏墨",终端文字从投影里穿过去。改为:floating(.22/7/y2) · panel(.28/16/x4) · dialog(.34/16/y6) · card(.16/4/y2) · cardFocused(.24/8/y3),分层交给描边与材质,胶囊底不透明度提到 .92 补偿。统一经 View.txShadow(_:),视图里不许再手写 .shadow。
顺带补上遮罩态侧边栏浮层一直漏掉的投影(设计要求 24px 0,故 panel 是唯一带 x 位移的)。
3. 圆角不再与屏幕圆角打架(paneShape)
终端右上角贴着屏幕物理边缘,那里设备本身是 ~22pt 大圆角 —— 再画 6pt 小圆角必然被"吃掉"(小圆角整段落在屏幕圆角之外,切口处露出底色三角)。改为 UnevenRoundedRectangle:只在朝 chrome 的角收圆角(左上/左下/右下 6pt),顶边与右边直接铺到物理边缘、不收角不描边,由屏幕自己的圆角收口(Stage Manager 小窗同理,窗圆角接管)。
- 描边用负 padding(
top:-1, trailing:-1)把顶边/右边那两条推出屏外:留着就是贴在屏幕边上的一圈多余框线。 - 顺带取消顶边/右边的 2pt 缝(2pt 是pane 之间的缝,不该出现在屏幕边缘),终端格子从 163×48 涨到 164×50。
4. 安全区:chrome 铺满物理屏,底栏自己让位(TX.Layout.footBand / capsuleTop)
- 原来
.ignoresSafeArea(edges: .top)只放开顶边 → 底部内容停在安全区上沿,屏幕最下面留一条 24pt 的空带("没铺到底"),而颜色型.background默认会溢进安全区,于是轨道底色铺满、内容却停在 24pt 之上,一眼就是没对齐。 - 现在整屏
.ignoresSafeArea(),底部区高度 =footHeight(34) + safeBottom,内容在其中居中 → 既贴住物理底边又让开 home indicator。提示条 / 侧边栏脚 / 轨道齿轮三者中心线同一条 y(995),上方分割线仍同在 964。 - 顺带修掉一个状态跳变:遮罩态面板铺满全高、Pin 态被安全区推高 24pt,一钉住面板脚就跳一下。现在两态完全一致。
- 安全区读数经新
TXScreenMetrics(GeometryReader必须在ignoresSafeArea之前,否则读到 0)。 - 胶囊顶距:
capsuleTop(safeTop)—— iPad 隐藏状态栏后无遮挡用设计值 14;灵动岛/刘海机型(safeTop>30)按硬件下移。iPhone 17 Pro 模拟器实测胶囊落在岛下方、底栏在 indicator 之上。
5. 同类问题(走查中发现,一并修)
- 会话卡不等高(一排卡上下边对不齐):
TXTerminalMirror的行堆叠有内容驱动的最小高度,外层.frame(height:)压不住它 ——.frame(height:)只是给了个框,不想撑满的子视图会按自身理想高度居中放进去。改为把行堆叠放进Color.clear的 overlay(overlay 不参与父视图定尺),尺寸完全由外部决定,溢出的旧行被clipped()切掉(正是想要的:底对齐留最新输出)。 - 会话卡从不换行的
HStack改LazyVGrid(最多 3 列):第 4 条会话进来原先会把所有卡压扁。高度由死写 560 改为min(560, max(300, 可用高度×0.56)),竖屏/分屏/小 iPad 上不再把「全部主机」挤出屏外。 - 首页滚动内容铺到物理底边(
ignoresSafeArea(.bottom)+ 内容 padding 补 safeBottom),末行能完整滚到 indicator 之上。 - 窄窗口适配:主机网格 4→2 列、左右留白 40→20、搜索框 220 固定改可伸缩、提示条只留 2 条(4 条在 iPhone 上折行把 34pt 带撑破,且强制单行
lineLimit(1).fixedSize())。 - 胶囊红/黄圆点命中区 22×30 → 26×34(13pt 视觉不变):关闭这种误触代价高的动作不该给这么小的目标。
- 搜索框补聚焦态描边(键盘焦点可见,无障碍底线)。
- 删掉
HomeView里嵌套重复定义的ConnectingRow(与文件级那份一字不差,编译能过但是死代码)。 - 遮罩浓度令牌化
TX.Scrim(对话框 .5 / 侧边栏 .42),不再散落Color(hex: 0x0A0B12)。
验收:iPad 13″ 横屏逐屏对拍(终端 / Pin / 遮罩 / 关闭确认 / 分屏菜单 / 首页)+ iPhone 17 Pro 竖屏(灵动岛 + indicator);像素采样确认 —— 右上角 (1365,0) 为终端底色(不再是底色三角)、两态面板脚同在 964/982–991、三张会话卡同为 166..710、遮罩把终端文字亮度 185→97;TXCore 45 tests 绿。
12.3i 触控化与标签规范(用户第四轮走查 + 真机部署,2026-07-26)
- 首页会话卡固定 3 格 4:3:
LazyVGrid列数写死 3(不再min(3, count)),不足 3 条自然从左往右排、卡片尺寸不随会话数跳动;高度改.aspectRatio(4/3, contentMode: .fit),删掉按屏高算高度那段。 - 技术标签规范(新
TXTech):- 拼写:照各自官方写法 —— 缩写全大写(
SSH)、项目名按作者写法(mosh/tmux/tsnet本来小写)、产品名照抄(macOS/TrueNAS)。TXSectionLabel不再.uppercased()—— 它会嵌主机名/会话名,之前渲染成TMUX · MAC-STUDIO · DEV把名字全改错了;轨道标签同步改tmux。 - 配色:传输协议冷色区分(
moshteal /SSHblue,二者互斥所以一行只出现一个);tsnet用 app 强调色(招牌能力);凭据名回中性(那是用户命的名,不是技术标签,避免一排全彩色)。主机卡图标染发行版品牌色(Ubuntu 橙→peach、Debian/树莓→red、Fedora/TrueNAS→blue、Arch→teal、macOS→中性银),认不出的回中性。徽标只染文字+描边、底色 12%。
- 拼写:照各自官方写法 —— 缩写全大写(
- 关闭/最小化按钮重做:原 macOS 交通灯式 13pt 纯色圆点三处不合适 —— 纯色块不说明自己干什么(mac 靠 hover 浮字形,触屏没有 hover)、13pt 命中目标太小、圆点是 mac 窗口语汇与本 app 圆角矩形语汇不搭。改 22pt 圆角方块 + 常显字形(
✕/⌄),保留红=关闭、琥珀=最小化的颜色语义,底色 14% 不铺满,命中 26×34,间距放宽。⌄而非双向内收箭头:后者在 9.5pt 下糊成一团,且向下正好对上设计的「下滑最小化」手势。 - 侧边栏自身的平移动画(新
leadingColumn):原来是if pinned { 面板 } else { 轨道 }—— 两者互换只能硬切,用户看到面板"消失"、轨道"出现",没有平移。改为轨道常驻在底、面板叠在上,列宽随三态动画(56/56/288)+clipped():收起时面板向左滑出并被裁掉、轨道从背后露出。慢放实测 1.2s 处列宽正处于中间态(≈140pt),确认逐帧插值而非硬切。 - 去掉命令面板:底部
⌘K 命令面板与首页搜索框的⌘K提示一并删除(快捷键体系属 UI-5,未实现的提示等于假承诺)。 - 底部改动作条
TerminalActionBar(原TerminalHintBar):iPad 大概率没接键盘,「竖分屏 / 新建 window / 回首页」只写成⌘D文字提示等于这些功能对触控用户不存在 → 改成常驻可点按钮(图标 + 文案,26pt 高胶囊)。非 tmux 会话下前两个灰显但保留(不隐藏,免得布局跳)。左侧上下文文案改为真实会话状态(mac-studio · tmux dev · 3 windows)—— 此前写死「已连接妙控键盘 · 附加行已收起」,没接键盘时那是假话。措辞与侧边栏统一为「新建 window」。
真机部署(用户体验用):-configuration Release -destination 'id=<iPad UDID>' -allowProvisioningUpdates 自动签名(Apple Development: JIANKUAN YANG / Team J75A6S5938)→ xcrun devicectl device install app + process launch。Release 里所有 #if DEBUG 的 fixture / terminalx://ui / -txSlowMotion 全部编译不进去,所以真机跑的必然是真实数据路径(首次运行落在「还没有主机」空状态);签名后 Keychain/Secure Enclave 可用,凭据走系统钥匙串而非文件回退。
12.3j 侧边栏三态动画重构:统一视图身份(用户第五轮反馈「动画混乱」,2026-07-26)
用户判词:三态切换动画混乱;收起态应理解为「展开态在左侧裁切一部分」,允许改 UI 让两态最左侧图标同尺寸同位置。
根源(12.3i 的 leadingColumn 只修了一半):① 轨道 RailView 与面板 SidebarPanel 是两套视图,元素尺寸/位置互不对应(logo 34/30、头像 38/26、徽标 34×30/26×22,纵向节奏也不同)→ 就算列宽在动画,内容仍是两张不相干的画面交叠;② 遮罩态面板挂全屏 overlay、Pin 态面板是 leadingColumn 里另一个实例 → overlay⇄pinned 切换视图身份互换,一个滑出一个滑入互相打架;③ 遮罩/投影靠 if 插拔。
重构(RailSidebar.swift 整文件重写为 TerminalSidebar,ContentView.screen 重组):
- 三态唯一视图:内容恒按 288pt 全宽排版;收起 = 容器宽度收到 railWidth +
clipped()(字面实现「左侧裁切」)。最左 railWidth 为图标列:logo 30 / 裸图标 17pt / 头像 30 / window 徽标 26×22 / 状态点 / 开合键 / 齿轮,两态同尺寸同位置;右侧明细(标题/副行/快捷键/选中底)opacity(expanded)淡入淡出。 - 行对应关系:轨道的 24pt 短分隔线 ⇄ 面板的分组标题(同一行两种密度,交叉淡化);轨道
tmux迷你标签 ⇄tmux · 主机 · 会话分组标题;搜索裸图标 ⇄ 搜索框(放大镜位置几乎重合);底部统一为「tailnet 状态行 / 开合键行 / 分割线+齿轮 footBand」,两态元素不增不减。 - 组合:舞台前的占位列只在 Pin 态推到 288(保持「钉住/解除各 resize 一次」);遮罩与侧边栏常驻在同一 overlay 里,遮罩动 opacity、投影(
Elevation.panel)动 opacity —— 遮罩⇄Pin 时侧边栏本体一动不动,只有遮罩淡出+舞台平移。 - 新坑:
clipped()只裁绘制不裁触摸 —— 收起态下整行按钮被裁掉的明细区会伸进终端抢点击。整行按钮用LeadingHitShape(AnimatablecontentShape,命中区=当前可见宽度);纯明细控件(搜索框/Pin/新建 window)再加allowsHitTesting(expanded)。 - 验证(fixture + ×12 慢放 + 逐帧像素采样):收起→遮罩(遮罩逐帧变暗 30→29→22)、遮罩→收起(面板前沿扫过采样点+遮罩渐浅)、遮罩→Pin(胶囊中间帧 709px vs 终值 826px,舞台平移中)、Pin→收起(427→362)均有中间帧,无硬切。
用户复核后追加裁决(同日)——砍掉悬浮态,只留两态:Pin→收起的"明细淡出 + 终端从右盖上来"效果被点名保留,展开即反向。
SidebarState只剩collapsed|pinned;togglePin/dismissOverlaySidebar/遮罩/浮层投影全删;TerminalSidebar直接进 HStack 参与布局(无占位列),开合就是它的宽度动画(56⇄288),各触发一次 resize;点终端不再收起侧边栏(固定是持久态)。- 开合入口收敛(此前顶部/底部两个收起键冲突、底部那枚缩在左下角不协调):Logo 位=开合主入口(收起态显 » 展开键、展开态还原
›Logo,同框同位交叉淡化——用户定稿:将来 App Logo 设计与展开按钮一体);头部 « 收起键保留;底部开合行删除;Pin 键删除(两态下无意义)。terminalx://ui/sidebar/pin兼容保留=toggle。 - 顺带修掉暴露的旧缺陷:标题胶囊的格子数读
screenGrid.value却从不刷新(旧注释"首次布局后稳定,不必 @Published"在"开合即 resize"下不成立,表现为开合后数字停在旧值)→GridBox加onGridChange(去重)、TerminalSession转发objectWillChange。验证:展开 135×50 / 收起 164×50 实时跟随。 - 验证:展开动画 pane 左描边逐帧 58→198.5→261→290pt(easeOut 先快后慢),无硬切;收起态 Logo 位为 »、底部只剩状态点/分割线/齿轮;展开态头部仅 Logo+名称+«。
12.3k 放弃移动端分屏 + tmux window Tab 上移 + 状态出口合并(用户裁决,2026-07-26)
用户判词:全面放弃移动端的分屏操作(非核心功能);tmux 的切换从侧边栏移到顶部作 Tab,活动会话(主机连接)的切换仍在侧边栏;右上状态文字移到左下并与既有文字合并(原两处信息重复)。 (第一版误把「会话列表」理解成主机连接列表上移为 Tab,用户纠正 + AskUser 确认:Tab = tmux window(与 tmux -CC「window 映射为原生 tab」惯例一致),非 tmux session 切换——后者需 switch-client + 窗口全量重建,未做。)
1. 删分屏(入口 + 逻辑):
- UI 入口:底部动作条「竖分屏」按钮、分屏菜单
SplitMenuView(12.3d 的 3b 卡片整个删除)、AppModal.splitMenu。 - 逻辑:
TmuxController.splitWindow/splitIntoNewSession、list-sessions管线(TmuxSessionInfo/sessions/listSessions()/PendingKind.listSessions——只服务分屏菜单)、TerminalSession.tmuxSessionList/injectedTmuxSessions、fixture 的tmuxSessions()、无头 URLui/split/ui/split-now。 - 保留:多 pane 的被动渲染与
PaneSplitter拖拽/resizePane(别的客户端建好的分屏 attach 进来仍须按 layout 正确显示)、ui/resize/<cols>。 - 顺带修掉
TmuxController响应 switch 里重复的case .currentSession(死代码)。
2. tmux window Tab(TmuxWindowTabsBar,TerminalChrome.swift):顶带 = 左标题胶囊 + window Tab 条(右上状态胶囊撤销)。一个 window 一个浮起胶囊 tab(编号徽标 + 标题),点击 selectWindow(链路复用旧侧边栏 window 行);当前 window accent 高亮;controller 未 attach 但有 tmuxSummary 时按占位渲染(fixture/接回前),原生窗口不显示。三层嵌套订阅各自拆视图(WindowTabsRow 订阅 controller、WindowTab 订阅 window——title/index 是 window 自己的 @Published)。侧边栏 tmux 段(tmuxDivider/tmuxLabelRow/tmuxRows/SidebarWindowRow/WindowRowBody/「新建 window」行)删除,「新建 window」入口保留在底部动作条;侧边栏 = 首页 / 活动会话(保留,主机连接切换) / 连接新主机 / tailnet / 设置,不再要 session 参数。
3. 状态出口合并(TerminalActionBar.statusLine):原右上三格分层徽标(tsnet|mosh|tmux)与原左下上下文文字合并为左下唯一状态行:[dot 链路] | 传输(重连计数) | 会话概览(tmux 名 · N windows / 原生窗口 / 无 tmux / phaseText)。主机名不再出现(标题胶囊与当前 tab 已有)。「只染出问题的那层”规则原样保留。
- 验证:fixture 三会话截图对拍(window tab 高亮/占位 tab/合并状态行/侧边栏两态排版含活动会话段)+
ui/session/2切换 + 开合 resize 格子数跟随(164×50⇄135×50)。真机 tmux 下点 tab 切 window 待验证(链路与旧侧边栏 window 行相同)。
12.3l 顶带落地重设计:按钮分离 / 标题弱化 / tab 组(用户定稿,2026-07-26)
用户判词:按钮、title、tab 可以在终端外区域;按钮分离出来单独悬浮;标题弱化;tab 要有「组」的概念。
- 浮起胶囊废除,顶带落地为真实 chrome 行(
TerminalTopBar替换TerminalTitleCapsule+FloatingCapsule):stage从 ZStack overlay 改回 VStack 首行,终端 pane 从带下方开始 —— 86pt 预留带(TX.Layout.capsuleInset,已删令牌)与「胶囊遮内容」问题一并消失。pane 顶边现在朝 chrome:topLeading 收圆角、顶描边恢复(不再top:-1推出屏外);右边仍贴屏幕物理边缘不收角。pane 内容留TX.Space.s呼吸位。 - 顶带层级(一眼一个主角):
[✕ ⌄ 独立按钮组] · 弱化标题 · [tab 组]。按钮升到 28pt 圆角方块自成一簇(危险操作与身份信息不共容器);标题降为纯文本行(状态点 5pt + 主机名 mono12 text2 + cwd textWeak + 格子数 textFaint),当前 window title 从标题里删掉 —— 它已由 tab 组选中 chip 承担,写两遍是重复;tab 组是顶带唯一带容器的元素。 - tab 组(
TmuxWindowTabGroup):surfaceDark 圆角槽(radius 11)内衬 3pt,window 是槽内段式 chip(选中 = selectedBg+描边浮起,未选 = 透明贴槽底),尾部+新建 window。坑:槽体背景必须挂在 ScrollView 的内容层 —— 挂 ScrollView 自身就是一条拖到屏幕右边的空槽(ScrollView 恒占满剩余宽度)。占位态(controller 未 attach 但有 tmuxSummary)共用 chip 本体;原生窗口不渲染 tab 组。 - 验证:fixture 截图 ×3(tmux 态顶带 / 原生窗口态无 tab 组 / 展开侧边栏组合态),标题去重与槽体紧贴成组均确认。
12.3m 换行错乱根因修复:客户端格子的权威改为 ghostty 实测(真机报障,2026-07-26)
用户报障:无论侧边栏收起/展开,终端(Claude Code 等全屏 TUI)换行错乱、行首冒碎片,竞品完美。
- 根因(模拟器连本机 sshd 复现实锤):
containerResized用「容器px ÷ 整数cell px」算 tmux 客户端格子,复刻不了 ghostty 内部取整/内边距 —— 实测算得 164×51、ghostty 真实 163×50(列差来自TmuxPaneView的 1pt padding 圈,行差来自像素除法本身)。远端按 164 列排版、surface 按 163 列自动换行 → 每条满宽行掉字符,重绘积累成花屏。 - 修复(
TmuxController+ContentView+TerminalChrome):① pane resize 回调从「只报 cell 像素」升级为上报完整实测格子(paneViewportChanged);② 单 pane window 的 pane 帧==容器帧 → 它的实测格子就是客户端格子,据此发refresh-client -C(帧只由容器决定,无反馈环);③ kickoff 初值改用 raw surface 实测格子(setClientSize不再被像素换算覆盖);④ 单 pane 时去掉 pane 的 1pt padding 圈(solo);⑤ 像素除法只兜底「全是多 pane window」的场景;⑥ 顶栏格子数 tmux 模式改读TmuxController.clientGrid(screenGrid随 RawTerminalView 卸载已冻结)。 - 验证(模拟器 → 本机 sshd + tmux 3.7b 端到端):
tput cols宽字符标尺恰好一行不换行;侧边栏收起 164×50 ⇄ 展开 135×50 双态各验证;top全屏重绘干净;顶栏胶囊数字随开合正确跳变。回归法已写入 CLAUDE.md(模拟器-txPubkeyAuth 1连 127.0.0.1,公钥临时入 authorized_keys)。
12.4 首版明确推迟(Fable 裁决,勿在 UI-3~6 里偷偷做)
mosh <RTT> 实时数字(需改 vendor/mosh iosclient 导出 SRTT,属传输层)· ⌘K 命令面板 · 无 tmux 主机的多客户端窗口(保留横幅引导)· iPad 竖屏 pane 强制上下堆叠(会推翻「路线 b」,交给 tmux relayout)· 缩略卡 ANSI 彩色(用 readViewportText() 文本镜像,绝不做离屏 Metal/双 surface——IOSurfaceLayer 泄漏史)· 设置页连字/CJK 开关(ghostty 无运行时 mutation,灰显占位)。