feat: UI 商业化改造 + 连接前 tmux 会话选择(真机验证)

按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从
「从不自动进」修成「连接前探测 → 让用户选会话」。

设计令牌与导航地基
- Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端)
- vendor JetBrains Mono 四权重(附 OFL 许可)
- AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦
- SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提)

按设计稿落地的屏
- 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次)
- 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips
- 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条
- 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭

tmux 真实链路(真机查出并修掉 4 个 bug)
- 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端)
- format 分隔符 tab 经 PTY 变成下划线 → 改用 |:|
- controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新)
- 当前会话名不能靠 session_attached 反推 → 改用 display-message

传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+)
无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法
TXCore 45 tests 绿(新增 5 个会话探测单测)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kid
2026-07-25 22:59:35 +08:00
parent 6dd23275ff
commit 28e9cfc207
46 changed files with 10237 additions and 1204 deletions

View File

@@ -115,4 +115,177 @@
### 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`(列表+HostCard+HostEditorView认证 segmented[直接输入/ Keychain ]+出口 Picker[Direct/ Tailscale])、`KeychainView.swift`(列表+CredentialEditorView密码/SSH密钥[生成·粘贴·导入]/Face ID 占位)、`TailscaleView.swift`(列表+编辑器 name/hostname/authKey+注册按钮经 registry Up+SelfIP)、`SettingsView.swift`(主题+关于 + `KnownHostsView` pin 查看/删除)。
### 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 拖拽 | 完成pane 拖拽待真机 tmux 验证 |
| 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:)` 复用同主机会话`focus`/`minimize`/`close`前后台广播、⌘19 索引首页排序最小化置顶 + 最近活跃)。
- `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.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>