Files
terminalX/docs/HANDOFF.md
kid 4b5292107b feat: 业务逻辑对齐 + 首页/弹框七项减法 + 终端页四项体验修复
业务逻辑(用户定稿):
- 点主机总是新建会话(会话:主机=多: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>
2026-07-26 17:49:58 +08:00

398 lines
69 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 模拟器 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=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不能跑本地 shellmacOS 才支持本地 PTY。
- 分发目标 App StoreM5已暂缓
- 四大核心:**libghostty**(终端引擎) · **mosh** · **Tailscale tsnet(用户态,非 NetworkExtension)** · **tmux control-mode(-CC) hook**
## 2. 关键架构决策(已定,勿推翻)
1. 终端引擎用 **libghostty**,经社区 **GhosttyKit**`Lakr233/libghostty-spm`,已 vendor 到 `vendor/libghostty-spm`)。`TerminalViewState``ObservableObject`;喂字节入口 `InMemoryTerminalSession.receive(Data)`SwiftUI 视图 `TerminalSurfaceView(context:)`
2. Tailscale 走 **tsnet 用户态**Gogomobile bind → `TsnetBridge.xcframework`**不用 NetworkExtension**(避开 VPN entitlement + 50MB 内存墙)。
3. SSH 用 **libssh2 + mbedTLS**`CSSHCore.xcframework`),支持传入外部 fd。
4. **SSH-over-tsnet = fd 桥**`tsnet.Dial()` 得 net.Conn无真实 fd`socketpair(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 能 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-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.swift`DCS 检测)—— 纯解析。TXCore 全部单测共 **50**40 swift-testing + 10 XCTesttmux 解析/`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 targetlibssh2 头 + modulemap`import CSSH`)、`CSSHCore`binaryTargetlibrary-only
- `CMosh/`C target`moshiosbridge.h`(mosh_main)+modulemap+占位 shim.c`import CMosh`)、`MoshCore`binaryTargetlibrary-onlyTXTransport 链 `libc++`/`libz`mosh 是 C++ 且 compressor 用 zlibCommonCrypto 属 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 安全**`TXTransport` `SSHConfig.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()`(tsnet `InjectEvent`+`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)`surfaceByPane` O(1) 路由 %output`applyLayout` reconcile(增删留)attach: `refresh-client -C WxH``list-windows`(带 layout)→每 pane `capture-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.sendtmux 模式→send-keys
- **数据流(tsnet)**`TsnetManager.ensureUp`(auth key)→`dialFD`(socketpair fd)→`SSHSession(preconnectedFD:)`
## 6. 已知问题 / TODO继续工作的入口
- **M4 安全(已完成,剩真机 SE**host key TOFU pin(`SSHConfig.hostKeyVerifier`→app `KeychainKnownHostsStore` 文件后端) + publickey 认证(`Authentication.publicKeyCallback(SSHSigner)``SSHSession` C `sshSignCallback``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→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利好审核)。
- **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 均已完成并验证;以下按价值)
1. **真机化(最大缺口,多项验证只在模拟器)**配置签名 + entitlementstsnet 用户态**无需** NE利好审核)→ 上真机/TestFlight真机上补验三件模拟器测不了的:①**SE 硬件密钥**生成+签名+keychain 持久化M4b 现回退软件+文件`SigningKeyProvider` 已备 SE 路径);②**mosh 深挂起恢复**锁屏数分钟过 WG 180s 过期线 / socket defunct `MoshRelay.Rebind`+`hop_port`);③签名后 **Keychain 可用**当前未签名 SecItem -34018host key pin 才回退到文件)。
2. **产品化**主机列表持久化签名后用 Keychain 存凭据/host 配置)、多账号多 tsnet 节点架构决策 §2.6 host egress禁多 tailnet 盲探)、软键盘运维工具栏GhosttyKit 内建接线即可)、多标签 tmux)。
3. **打磨(可选,不阻塞)**
- 清理诊断埋点`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`需清理策略
- tmuxpause 流控(`%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 状态)。
4. **冷启动 mosh 重连(大活)**mosh 进程被杀后接原 detached 会话—— SSP 序号防重放限制需持久化 MOSH_KEY+port 且改 mosh 序列化客户端状态Blink )。价值中等成本高
5. **macOS target**本地 PTY`TXPTY`未建
6. **M5 合规提审**用户指示暂缓
## 8. 测试资源(用户提供,**凭据勿写入仓库/勿硬编码**,每次向用户索取)
- 一台局域网 macOS( tmux + **mosh-server** MacBook-Air-M1fish 为默认 shell`mosh-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-airipad-promacbook-air-m1macbook-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:` IOSurfaceLayer `Metal.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`+`allowsHitTesting`surface 不释放内容保留后台窗口持续收 %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.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`inputAccessoryStyle` SwiftUI 未暴露需桥接)。
- 无头 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 顾问Q1Q5 裁决)。**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/deletemeta 与机密分离) + `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`(libssh2 `publickey_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` 统一构造 confighost key pin 命名空间=`egressPlan.egressKey`(`direct`/`tsnet-<uuid>`)。删实例级 `tsnetAuthKey`/`TsnetManager` 单例
- 无头验证`-txTsnetKey` 合成临时连接固定 connID + stateDir `tsnet-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`(主题+关于 + `KnownHostsView` pin 查看/删除)。
- **直接输入自动对象化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.txtOFL-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 不占位 resizepinned 钉住各 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` 从生命周期广播里排除否则 scenePhaseactive 广播经 `syncUI()` 把注入状态覆盖回 idle
- **验收**四步截图取证全通过A 终端会话1 B `ui/minimize` 回首页3 会话并存 + mac-studio 最小化中置顶)→ C `ui/session/2` 切会话2独立内容 + tsnet 徽标)→ D `ui/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` 分不出)。
修正项
1. **沉浸不彻底(最严重)**原来把 86pt 预留带做成"pane 容器整体下移"胶囊浮在舞台底色上改为 **pane 容器从舞台顶部就开始**终端底色 + 边框铺满只把 surface 内容 `padding(.top, 86)` —— 预留带铺的是 `flavor.terminalBg`故与终端内容视觉连续胶囊真正"悬浮在终端之上"。像素采样验证预留带与内容区同为 `#1E1E2E`
2. **终端页隐藏系统状态栏**设计稿 `1b`/`3a` 顶部就是胶囊/logo`1e`/`1g` 才有状态栏+ `ignoresSafeArea(edges: .top)`。****`statusBarHidden` 不能挂在 `TerminalScreen` ——它常驻挂载SessionCanvas会连首页一起隐藏要挂在 `RootView` 上按 `router.route` 判断
3. **侧边栏动画**三态切换统一走 `AppRouter.sidebarAnimation``.easeOut(0.24)`设计要求 200260ms面板 `.move(edge: .leading)` 滑入 + 遮罩 `.opacity` 淡入
4. **底部两区同高**侧边栏脚与舞台提示条共用 `TX.Layout.footHeight`(34pt)故两侧分割线落在同一 y侧边栏脚原来的深色块 + 齿轮**去掉** 1pt `divider`设计稿如此)。
5. 轨道结构**搜索/首页/设置是裸图标**无底无框只有 logo 与展开键有框`TXIconButton(bare:)`纵向节奏按扫描到的设计坐标写死logo 28 · 头像 174/220/266 · TMUX 标签 388 · 数字 408 TMUX 段标签用 accent-500 而非中性灰
6. 侧边栏面板logo 30pt会话行 48ptwindow 44pt实测值分组分隔线改**渐隐** LinearGradient占位分支补回漏掉的新建 window T对齐后与设计稿偏差 9ptwindow 区完全重合)。
7. 缩略卡卡头/卡脚分隔线用 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`**3c436/404pttmux 推荐仅断开 ⏎」(`detach-client`服务端继续跑) + 次要红字结束会话」(`kill-session`**再点一次才执行**)原生窗口 琥珀警告 + 危险按钮设计精确色 `#8d5560` / `rgba(92,58,63,.35)` / `#f5a0ac`)。`closeSession(kill:)` 先发 tmux 命令延迟 250ms teardown 传输层 `%exit` 走既有 `onExit`)。
- **分屏菜单 `SplitMenuView`**3b360pt**不问方向**(⌘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 菜单最小化往返内容原样日志无 crashTXCore 50 tests 绿。**pane 拖拽只做了静态编译与逻辑走查没有真实 tmux pane 会话验证过**fixture 没有 controller)——有测试主机时必须补
### 12.3e 接真实数据 + 空状态UI-5 之前的必要一步)
之前几轮都靠 `-txUIFixture` 假会话对拍设计稿真实路径下有数据缺口和裸露的空白屏本轮补齐
**真实数据打通**
- **tmux 会话名**kickoff 时顺带 `listSessions()``currentSessionName` = list-sessions attached 的那个 状态胶囊/侧边栏标题显示真实 `tmux <会话名>`拿不到就只显示 `tmux`不编造)。
- **cwd**`list-windows` format 追加 `#{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.83macOS + fish + tmux 3.7b)。过程中查出 3 个真实 bug
1. **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` 才确诊
2. **UI 不刷新**正是 CLAUDE.md 嵌套 ObservableObject 不冒泡那条坑的另一面`tmuxSummary`/window 列表是读 controller computed property而轨道/胶囊/侧边栏只订阅 session 数据解析对了 UI 也不动修法`TerminalSession` Combine 订阅 `controller.objectWillChange` 并转发自己的 `objectWillChange`一处解决所有读 controller 的视图)。
3. **会话名猜错**`currentSessionName` 原来取 `list-sessions` `session_attached==1` 的那个结果把用户自己终端 attach `demo` 当成了当前会话改用 `display-message -p "#{session_name}"`权威本客户端所在会话)。分屏菜单的当前判定也随之改为按会话名比较
4. 顺带修正轨道/侧边栏的 window 编号用 `#{window_index}`用户可见的 0/1/2…)而非内部 `window_id`@2/@3…)。
**验证结果**服务器侧交叉验证
- tmux 自动进入 状态胶囊 `direct | ssh | tmux main`标题胶囊 `fish · ~ 163×50`轨道 window `0/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`三个坑都在注释里
1. **PTY 回显**命令本身会被回显标记若字面出现在命令里回显行会被当成结果起点 写成相邻字符串拼接 `"__TX""_SESSIONS_BEGIN"`shell 输出完整标记回显的是拼接前的样子)。
2. **三态要分开**:「没装 tmux装了但没会话 stderr 都被 `2>/dev/null` 吞掉标记之间同样是空的只能靠 `tmux -V` 的版本行区分 —— 两者的正确 UI 完全不同安装引导 vs 让用户新建)。
3. **别脏屏**探测命令与输出会打在用户屏幕上 命令末尾接 `printf '\033[H\033[2J\033[3J'`归位+清屏+ scrollback不依赖 terminfo)。刚连上时清掉的只有 motd/shell 欢迎语首屏反而更干净
- **`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` + 轨道 window `0/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.961 缩放+淡入) / `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提示条 / 侧边栏脚 / 轨道齿轮三者中心线同一条 y995上方分割线仍同在 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` 的 overlayoverlay 不参与父视图定尺),尺寸完全由外部决定,溢出的旧行被 `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×3413pt 视觉不变):关闭这种误触代价高的动作不该给这么小的目标。
- 搜索框补聚焦态描边(键盘焦点可见,无障碍底线)。
- 删掉 `HomeView` 里**嵌套重复定义**的 `ConnectingRow`(与文件级那份一字不差,编译能过但是死代码)。
- 遮罩浓度令牌化 `TX.Scrim`(对话框 .5 / 侧边栏 .42),不再散落 `Color(hex: 0x0A0B12)`
**验收**iPad 13″ 横屏逐屏对拍(终端 / Pin / 遮罩 / 关闭确认 / 分屏菜单 / 首页)+ iPhone 17 Pro 竖屏(灵动岛 + indicator像素采样确认 —— 右上角 `(1365,0)` 为终端底色(不再是底色三角)、两态面板脚同在 964/982991、三张会话卡同为 166..710、遮罩把终端文字亮度 185→97TXCore 45 tests 绿。
### 12.3i 触控化与标签规范(用户第四轮走查 + **真机部署**2026-07-26
1. **首页会话卡固定 3 格 4:3**`LazyVGrid` 列数写死 3不再 `min(3, count)`),不足 3 条自然从左往右排、卡片尺寸不随会话数跳动;高度改 `.aspectRatio(4/3, contentMode: .fit)`,删掉按屏高算高度那段。
2. **技术标签规范(新 `TXTech`**
- **拼写**:照各自官方写法 —— 缩写全大写(`SSH`)、项目名按作者写法(`mosh`/`tmux`/`tsnet` 本来小写)、产品名照抄(`macOS`/`TrueNAS`)。**`TXSectionLabel` 不再 `.uppercased()`** —— 它会嵌主机名/会话名,之前渲染成 `TMUX · MAC-STUDIO · DEV` 把名字全改错了;轨道标签同步改 `tmux`
- **配色**:传输协议冷色区分(`mosh` teal / `SSH` blue二者互斥所以一行只出现一个`tsnet` 用 app 强调色(招牌能力);凭据名回中性(那是用户命的名,不是技术标签,避免一排全彩色)。主机卡图标染**发行版品牌色**Ubuntu 橙→peach、Debian/树莓→red、Fedora/TrueNAS→blue、Arch→teal、macOS→中性银认不出的回中性。徽标只染文字+描边、底色 12%。
3. **关闭/最小化按钮重做**:原 macOS 交通灯式 13pt 纯色圆点三处不合适 —— 纯色块不说明自己干什么mac 靠 hover 浮字形,触屏没有 hover、13pt 命中目标太小、圆点是 mac 窗口语汇与本 app 圆角矩形语汇不搭。改 **22pt 圆角方块 + 常显字形**`✕` / `⌄`),保留红=关闭、琥珀=最小化的颜色语义,底色 14% 不铺满,命中 26×34间距放宽。`⌄` 而非双向内收箭头:后者在 9.5pt 下糊成一团,且向下正好对上设计的「下滑最小化」手势。
4. **侧边栏自身的平移动画(新 `leadingColumn`**:原来是 `if pinned { 面板 } else { 轨道 }` —— 两者**互换**只能硬切,用户看到面板"消失"、轨道"出现",没有平移。改为轨道**常驻**在底、面板叠在上列宽随三态动画56/56/288+ `clipped()`:收起时面板向左滑出并被裁掉、轨道从背后露出。慢放实测 1.2s 处列宽正处于中间态≈140pt确认逐帧插值而非硬切。
5. **去掉命令面板**:底部 `⌘K 命令面板` 与首页搜索框的 `⌘K` 提示一并删除(快捷键体系属 UI-5未实现的提示等于假承诺
6. **底部改动作条 `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` 重组):
1. **三态唯一视图**:内容恒按 288pt 全宽排版;收起 = 容器宽度收到 railWidth + `clipped()`(字面实现「左侧裁切」)。最左 railWidth 为**图标列**logo 30 / 裸图标 17pt / 头像 30 / window 徽标 26×22 / 状态点 / 开合键 / 齿轮,两态同尺寸同位置;右侧明细(标题/副行/快捷键/选中底)`opacity(expanded)` 淡入淡出。
2. **行对应关系**:轨道的 24pt 短分隔线 ⇄ 面板的分组标题(同一行两种密度,交叉淡化);轨道 `tmux` 迷你标签 ⇄ `tmux · 主机 · 会话` 分组标题;搜索裸图标 ⇄ 搜索框放大镜位置几乎重合底部统一为「tailnet 状态行 / 开合键行 / 分割线+齿轮 footBand」两态元素不增不减。
3. **组合**:舞台前的占位列只在 Pin 态推到 288保持「钉住/解除各 resize 一次」);遮罩与侧边栏**常驻**在同一 overlay 里,遮罩动 opacity、投影(`Elevation.panel`)动 opacity —— 遮罩⇄Pin 时侧边栏本体**一动不动**,只有遮罩淡出+舞台平移。
4. **新坑:`clipped()` 只裁绘制不裁触摸** —— 收起态下整行按钮被裁掉的明细区会伸进终端抢点击。整行按钮用 `LeadingHitShape`Animatable `contentShape`,命中区=当前可见宽度);纯明细控件(搜索框/Pin/新建 window再加 `allowsHitTesting(expanded)`
5. **验证**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→290pteaseOut 先快后慢),无硬切;收起态 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()`、无头 URL `ui/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 顶边现在朝 chrometopLeading 收圆角、顶描边恢复(不再 `top:-1` 推出屏外右边仍贴屏幕物理边缘不收角。pane 内容留 `TX.Space.s` 呼吸位。
- **顶带层级(一眼一个主角)**`[✕ ⌄ 独立按钮组] · 弱化标题 · [tab 组]`。按钮升到 28pt 圆角方块自成一簇(危险操作与身份信息不共容器);标题降为纯文本行(状态点 5pt + 主机名 mono12 text2 + cwd textWeak + 格子数 textFaint**当前 window title 从标题里删掉** —— 它已由 tab 组选中 chip 承担写两遍是重复tab 组是顶带唯一带容器的元素。
- **tab 组(`TmuxWindowTabGroup`**surfaceDark 圆角槽radius 11内衬 3ptwindow 是槽内段式 chip选中 = selectedBg+描边浮起,未选 = 透明贴槽底),尾部 `+` 新建 window。**坑:槽体背景必须挂在 ScrollView 的内容层** —— 挂 ScrollView 自身就是一条拖到屏幕右边的空槽ScrollView 恒占满剩余宽度。占位态controller 未 attach 但有 tmuxSummary共用 chip 本体;原生窗口不渲染 tab 组。
- 验证fixture 截图 ×3tmux 态顶带 / 原生窗口态无 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灰显占位
</content>