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

@@ -0,0 +1,241 @@
# Handoff: terminalX — iPadOS 终端 App沉浸轨道方案
## Overview
terminalX 是面向 iPadOS / iOS 的原生终端运维工具,解决 iPad 远程运维 macOS / Linux 的痛点。技术选型:**libghostty**(终端引擎)· **mosh**(抗断线传输)· **Tailscale tsnet**(用户态入网)· **tmux control mode**(会话复用)。平台优先级 **iPadOS > iOS > macOS(Apple Silicon)**
本包描述的是**终端主界面(沉浸轨道)+ 首页(主机管理)+ 设置**这一套已定稿的 UI含侧边栏三态、分屏模型、关闭/最小化语义、竖屏与 iPhone 降级规则。
## About the Design Files
`terminalX iPadOS.dc.html` 是**设计稿**,不是生产代码。它用 HTML/CSS 表达最终视觉与交互意图1:1 像素尺寸),目的是让你在**目标工程里用原生技术重建**——本项目的目标环境是 **SwiftUIiPadOS 26 / iOS 26**,请使用工程已有的架构与组件模式实现,不要移植 HTML/CSS。
打开方式:浏览器直接打开该文件即可,画板可自由缩放平移。每块屏幕都带 `data-screen-label`,便于对照本文档。
## Fidelity
**High-fidelityhifi**。颜色、字号、圆角、间距、行高均为最终值,可按本文档的精确数值实现。文案(简体中文,技术术语保留英文)也是最终稿。
唯一例外:终端内容区的命令输出是**示意数据**,用于验证排版密度与配色,不必照搬。
---
## Design Tokens
### 颜色 — App chromeNocturne 设计系统)
| 用途 | Hex | Nocturne token |
| --- | --- | --- |
| 画布底 / 屏幕底 | `#161826` | `--color-bg` |
| 侧边栏 / 轨道 / 卡片底 | `#1b1d2b` | surface-dark |
| 控件底(输入框、次要按钮、徽标) | `#232532` | `--color-surface` |
| 舞台底pane 之间的缝) | `#11121c` | bg-deep |
| 分隔线(弱) | `#2c2f3d` | neutral-800 |
| 描边(常规) | `#3f424d` | neutral-700 |
| 描边(强调 / 对话框) | `#595d6c``#4a4d5e` | neutral-600 |
| 强调色(唯一) | `#9184d9` | `--color-accent` |
| 强调 - 边框 | `#796cbf` | accent-600 |
| 强调 - 选中底 | `#2b2741` | accent-900 |
| 强调 - 选中描边 | `#423a6a` | accent-800 |
| 强调 - 文字 | `#b5abfc` / `#d2cefd` | accent-400 / -300 |
| 强调 - 弱化文字 | `#5d5294``#968ae0` | accent-700 / -500 |
| 主文字 | `#e9e9ed` | `--color-text` |
| 次文字 | `#cfd3e5` / `#b2b6ca` | neutral-300 / -400 |
| 三级文字 | `#9397ab` | neutral-500 |
| 弱文字 / 占位 | `#75798c` | neutral-600 |
| 极弱(提示、注脚) | `#4a4d5e` | neutral-700 |
**规则**:强调色只做线、点、选中底和文字,**绝不大面积铺色**;不使用纯黑纯白。
### 颜色 — 终端内容区Catppuccin Mochalibghostty 渲染)
`base #1e1e2e`pane 底)· `mantle #181825` · `surface0 #313244`vim 状态栏)· `text #cdd6f4` · `subtext #a6adc8` · `overlay #6c7086`(次要输出)· `surface2 #585b70`(禁用/缓存态)
语义色:`green #a6e3a1`(成功、提示符)· `red #f38ba8`error· `yellow #f9e2af`warning、git 标记)· `blue #89b4fa`(路径、字段)· `mauve #cba6f7`(关键字)· `teal #94e2d5`(勾选)· `peach #fab387`(重连中)· `pink #f5c2e7`
设置页可切换 Mocha / Macchiato / Frappé / Latte**chrome 的中性阶随之推导,强调色恒为 Nocturne blurple `#9184d9`**。
macOS 窗口按钮沿用系统色:关闭 `#ff5f57`,最小化 `#febc2e`13pt 圆点,`inset 0 0 0 .5px rgba(0,0,0,.25)`,仅悬停/触摸时显出符号)。
### 字体
全局 **JetBrains Mono**400 / 500 / 600 / 700chrome 与终端同源。中文走系统回退PingFang SC
| 场景 | 规格 |
| --- | --- |
| 屏幕主标题(首页 terminalX | 500 34px, letter-spacing -.025em |
| 页面标题(设置、总览) | 500 2428px, ls -.02em |
| 分区标题 | 500 22px, ls -.02em |
| 分组小标题(活动会话 / TMUX | 600 10px, ls .09em, uppercase, `#75798c` |
| 列表主行 | 400500 13px |
| 列表副行 / 元信息 | 400 10.511px, `#75798c` |
| 徽标 / 胶囊 | 500 11.512px |
| 终端正文(主 pane | 400 13.5px / 1.55 |
| 终端正文(次 pane / 缩略) | 400 1112px / 1.5 |
| 终端 pane 头 | 500 11px, `#6c7086` |
| 底部提示条 | 400 11.5px, `#75798c` |
### 间距 / 圆角 / 阴影
- 间距2 / 6 / 8 / 9 / 10 / 12 / 14 / 16 / 18 / 20 / 26 / 34 / 40px
- 圆角:徽标 57 · 控件 89 · 卡片 1113 · 对话框 1415 · 屏幕卡 18 · 手机卡 38
- 阴影:浮起胶囊 `0 10px 30px rgba(0,0,0,.55)`;侧边栏浮层 `24px 0 60px rgba(0,0,0,.55)`;对话框 `0 30px 80px rgba(0,0,0,.7)`;卡片 `0 6px 18px rgba(0,0,0,.4)`
- 遮罩:`rgba(10,11,18,.42.5)`,浮层用 `backdrop-filter: blur(20px)`
### 图标
**Phosphor Icons**regular。用到`magnifying-glass` `house` `plus` `gear-six` `caret-double-left` `caret-double-right` `caret-down` `caret-left` `push-pin` `columns` `terminal-window` `hard-drives` `desktop-tower` `cpu` `key` `shield-check` `lightning` `paint-brush` `text-aa` `keyboard` `export` `info` `warning` `x` `minus` `arrow-right` `arrow-left` `lightbulb` `moon` `check-circle` `wifi-high` `battery-high` `browsers` `rows` `git-branch` `plugs` `trash` `arrows-in-simple` `textbox` `sidebar-simple` `globe-hemisphere-west`
SwiftUI 实现请换成等义 **SF Symbols**(例如 `house` / `magnifying-glass``house``magnifyingglass``caret-double-left``chevron.compact.left``sidebar.left``push-pin``pin`)。
---
## Layout 基准
- 画板尺寸iPad Pro 13″ 横屏 **1366×1024pt**;竖屏 **1024×1366pt**iPhone **393×852pt**
- 折叠轨道宽 **56pt**(竖屏 48pt元素中心线统一在 28pt竖屏 24pt
- 展开侧边栏宽 **288pt**
- pane 之间的缝 **2pt**(拖拽热区 **12pt**,把手 44×3pt 圆角 2pt
- 触控命中区不低于 **44pt**(键盘附加行按键高 3840pt + 6pt 间距 = 44pt 节距)
- 终端主 pane 顶部预留 **86pt** 给浮起胶囊2c 因多一条横幅为 122pt预留带用 padding-top 实现,内容容器 `min-height:0; overflow:hidden` 防止向上溢出
- Home indicator 条200×4ptiPhone 140×5pt
---
## Screens / Views
### 1) 终端主界面 · 沉浸轨道 — `1b 终端-沉浸轨道`
**Purpose**:真正干活的界面,终端面积最大化。
**Layout**`HStack`:左 56pt 轨道 + 右舞台。舞台是 `VStack`pane 区flex+ 硬件键盘提示条 34pt + home 区 14pt。pane 区 padding 2pt上下两个 pane 比例 1.7 : 1中间 12pt 拖拽条。
**轨道自上而下**Logo 方块34×34圆角 11`#232532`inset 描边 `#3f424d`,字形 `` 700 16px `#b5abfc`**不可点**)→ 搜索图标 → 首页图标 → 1pt 分隔 → 主机头像38×38 圆角 11两字缩写选中底 `#2b2741` + inset 描边 `#5d5294`;右上角 8pt 状态点,`box-shadow: 0 0 0 2px #1b1d2b` 挖底)→ 虚线 + 号 → 分隔 → `TMUX` 标签600 9px ls .1em+ window 数字34×30选中底 `#232532` + inset 描边 `#4a4d5e`)→ `Spacer` → 连接状态点 8pt → 展开键(`caret-double-right`34×34 底 `#232532` 描边 `#3f424d`)→ 设置齿轮。
**浮起 chrome**`position: absolute``backdrop-filter: blur(20px)`,底 `rgba(27,29,43,.86.9)`,描边 `#3f424d`,圆角 11
- 左上标题胶囊 `top 14, left 16`:红/黄圆点 → 1pt 竖分隔 → 6pt 绿点 → 主机名500 12.5px)→ 「当前 window title · cwd」400 11.5px `#75798c`
- 右上状态胶囊 `top 14, right 16`:三格分层徽标 `tsnet` | `mosh 41ms` | `tmux dev`,格间 1×16pt 竖线
**底部提示条**:接硬件键盘时只留 34pt 提示条(快捷键清单);触控时替换为键盘附加行(两行,见 iPhone / 竖屏)。
### 2) 侧边栏三态 — `3a 侧边栏-遮罩态`、`3a 侧边栏-Pin 态`
同一条栏的三个状态,**元素不增不减**,只是展示密度不同:
| 状态 | 宽度 | 行为 |
| --- | --- | --- |
| 折叠 | 56pt | 只显图标与两字缩写 |
| 遮罩(默认展开) | 288pt | 面板**盖住**轨道(`left: 0`,不并排,避免两套入口),右侧终端压 `rgba(10,11,18,.5)` 遮罩,**点终端任意处收起**;终端不 reflow不触发 PTY resize |
| Pin | 288pt | 点面板右上 `push-pin` 后钉住:遮罩消失,终端变窄,**只在钉住/解除时各 resize 一次**;再点解除回遮罩态 |
**开合语义(务必唯一、不歧义)**`` 方块是 App logo不可点。开合只有一枚按钮两态方向相反——折叠态在轨道底部 `caret-double-right`(展开),展开态在标题右侧 `caret-double-left`收起。Pin 是独立按钮,只在展开态出现。快捷键 `⌘\` 开合。
**面板内容自上而下**Logo + `terminalX`500 14px+ Pin + 收起 → 搜索框32pt 高,圆角 8→ **首页**行(`house` + 「首页」+ `⌘⇧H`)→ 分组「活动会话 n」→ 会话行 → 1pt 渐隐分隔 → 分组「TMUX · <主机> · <会话>」→ window 行 + 「新建 window ⌘T」→ `Spacer` → Tailscale 状态脚(绿点 + tailnet + 设备 IP + 齿轮)。
**行规格(左缘必须对齐)**:会话行 = 26pt 头像列 + 10pt gapwindow 行 = 26×22pt 徽标列 + 10pt gap两者标题左缘同为 **x=53**。选中态用 `box-shadow: inset 0 0 0 1px #423a6a` 而非 `border`,避免选中时内容位移 1pt。
### 3) 首页 · 主机管理(落地页)— `1e 主机-会话卡`
**Purpose**App 冷启动落点;「回到刚才那个会话」是首要动作。
**Layout**:状态栏 24pt → 页头terminalX 500 34px + 副行)+ 右侧搜索 220pt / 新建连接 / 设置 → 分组「活动会话」→ **3 列会话缩略卡flex:1** → 分组「全部主机」+ 筛选 chips → **4 列主机卡(按内容高度)** → home 区。左右留白 40pt。
**会话缩略卡**(圆角 13选中卡描边 `#423a6a` + `0 6px 18px rgba(0,0,0,.4)`):卡头 36pt状态点 + 主机名 + tmux 会话 + RTT→ 终端缩略(`#1e1e2e`11px/1.5**内容底对齐**,真实回滚)→ 卡脚 38pt「n windows · 上次活跃」+ `⌘1/2/3` + 主动作「回到会话 →」)。离线卡加 `rgba(24,26,40,.72)` + `blur(1.5px)` 遮罩,内含 26pt 转圈 + 两行说明;**遮罩必须是 pane 的最后一个子节点**pane 为 `position: relative`)。
**最小化回来的卡**:置顶并标「最小化中」,转场是从终端缩回该卡的动画。
### 4) 新建分屏 — `3b 新建分屏菜单`
**不问方向**(竖分 `⌘D` / 横分 `⌘⇧D` 本来就是两个入口)。菜单只问「分到哪里」:
- 分组「tmux 会话」:当前会话(带「当前」标签)、其余 detached 会话title · n windows · 上次活跃)、「新建 tmux 会话…」
- 分隔线
- 「原生窗口 / 不经 tmux · 关 App 即结束」
选完**立即分屏**。菜单 360pt 宽,圆角 14`#1b1d2b` + 描边 `#595d6c`;行高 ~40pt前导列统一 20pt状态点也要包在 20pt 居中列里,保证五行文字左缘一致 x=50
**模型约束**:分屏只发生在**同一台主机内**。卡片内部的分割 = 真 tmux pane`split-window`,服务端持有,断线原样恢复);跨主机分屏**已取消**(层级过深),要同时看两台机器就切会话或 Pin 侧边栏。
### 5) 关闭 / 最小化 — `3c 关闭确认`
标题胶囊左侧 macOS 红黄圆点:
- **黄点(最小化)****不确认**,直接回首页,会话继续跑;卡片置顶标「最小化中」。手势:终端下滑。快捷键 `⌘M`
- **红点(关闭)****弹一次确认**,按主机能力分两套文案:
- tmux 会话 → 主按钮「仅断开 ⏎」(`detach`,服务端继续跑);左侧次要文字按钮「结束会话」(`kill-session`,需再确认)
- 原生窗口 → 琥珀警告「没有 tmux 兜底,前台进程会随之结束」;推荐「取消」,危险按钮「关闭窗口」(描边 `#8d5560`,底 `rgba(92,58,63,.35)`,字 `#f5a0ac`
- 快捷键 `⌘W``esc` 取消;`⏎` 选推荐项
对话框 436pt / 404pt 宽,圆角 15。
### 6) 首页 ⇄ 终端 跳转关系 — `3d 页面跳转关系`
**进入终端2 条)**:点活动会话卡 / 会话行 → 恢复到**上一个焦点 pane**;新建连接或主机 + → 建好即进终端,侧边栏同步出现该会话。
**离开终端3 条)**:侧边栏首页行(`⌘⇧H`,会话完全不动)· 黄点最小化(同上但语义是"待会儿还回来")· 红点关闭二次确认tmux detach / 原生窗口结束)。
换会话不必回首页——侧边栏直接切;回首页表示"换任务"。会话总览层(原 `⌘O` 缩放)**已取消**。
### 7) 无 tmux 主机 — `2c 无 tmux 主机`
轨道下半段的 `TMUX` 段换成「窗口」段(客户端窗口,每个 = 一条独立 SSH channel / mosh 会话,可随意新建)。状态胶囊第三格写「无 tmux · 客户端窗口」而非报错。终端顶部一条可关闭的行内横幅:说明窗口由 App 维持、mosh 兜底,但**关掉 App 不会在服务端继续跑**,右侧给「安装并启用 tmux」次要按钮。不弹窗、不强推。
### 8) 竖屏与 iPhone — `2d iPad 竖屏`、`2d iPhone 竖屏`、`1h iPhone-终端`、`1h iPhone-主机`
- **iPad 竖屏1024×1366**:轨道降到 48pt元素中心 24pt同样含搜索/首页/展开键pane 由左右平铺改为**上下堆叠,最多两层**,多余 pane 折成顶部段控;状态胶囊压缩为「主机名 + RTT」两枚底部常驻单行键盘附加行。
- **iPhone393×852**:轨道下沉为**底部主机条**ms / u2 / nh 胶囊);单 pane 全屏,顶部 3 段进度式 pane 指示 +「左右轻扫切 pane」下拉标题打开会话列表键盘附加行两行esc/ctrl/tab/~///| 与 ↑↓←→/⌘K主机页用底部 tab主机 / 会话 / 密钥 / 设置)。
### 9) 设置 · 外观 — `1g 设置-外观`
左 280pt 类别栏(外观与主题 / 终端与字体 / Tailscale / 密钥与安全 / mosh 与重连 / tmux 集成 / 键盘与手势 / 诊断日志,脚注版本行),右内容区 padding 28/40。内容标题 + 说明 → 4 张 Catppuccin 主题卡88pt 预览 + 5 色点 + 名称,选中描边 `#796cbf` + `check-circle`)→ 两栏(左「实时预览」终端框,右「排版」面板:字体、字号 13.5pt、行高 1.50、连字、CJK 等宽补偿、跟随系统深浅色)。
---
## Interactions & Behavior
| 交互 | 行为 |
| --- | --- |
| 侧边栏开合 | `⌘\` 或开合键遮罩态点终端收起Pin 切换推挤/浮层 |
| 切会话 | 轨道主机头像 / 侧边栏会话行 / `⌘1…9`;恢复到该会话上一个焦点 pane |
| 切 window | 轨道数字 / 侧边栏 window 行 / `⌃⇥` |
| 分屏 | `⌘D` 竖分、`⌘⇧D` 横分 → 弹会话选择菜单 → 立即分屏 |
| 新建 | `⌘T` 新 tmux window`⌘⌥T` 原生窗口 |
| 调整 pane 比例 | 拖 12pt 分隔条(把手 44×3pt松手才向 tmux 发 `resize-pane` |
| 回首页 | `⌘⇧H` / 侧边栏首页行 |
| 最小化 | 黄点 / `⌘M` / 终端下滑;无确认 |
| 关闭 | 红点 / `⌘W`;二次确认,`⏎` = 推荐项,`esc` 取消 |
| 命令面板 | `⌘K`(模糊匹配主机、会话、动作) |
| 硬件键盘 | 检测到即收起附加行,只留 34pt 提示条;拔掉恢复附加行 |
| 长按 `⌘` | 显示全部快捷键面板iPadOS 系统行为) |
**动效**:状态切换 200260ms `easeOut`;侧边栏浮层从左滑入 + 遮罩淡入;最小化 = 终端缩回首页卡片matched geometry重连转圈 1s 线性循环。
**状态徽标规则(分层,只染出问题的那层)**
1. `tsnet` — 网络可达;断则本格转灰/琥珀
2. `mosh <RTT>` — 数字实时跳动;`≤120ms` 绿 `#a6e3a1``120200ms` 中性,`>200ms` 琥珀 `#fab387`
3. `tmux <会话名>` / 「无 tmux · 客户端窗口」
**重连**:指数退避,上限 30s卡片/胶囊显示「重连中 · 第 n 次 · ms 后」;屏幕内容本地缓存,恢复后原样接回(目标 <3s)。
**安全**host key TOFU 固定在钥匙串不匹配时**拦截连接**并告警私钥存 Secure Enclave不可导出密码回退默认关闭
## State Management
```
AppRoute = .home | .terminal(sessionID)
SidebarState = .collapsed | .overlay | .pinned
Session { id, host, kind: .tmux(name) | .nativeWindow, windows[], focusedPaneID,
transport: .ssh | .sshOverTsnet | .mosh | .moshOverTsnet,
link: .direct(rtt) | .relay(derp, rtt) | .reconnecting(attempt, nextIn) | .offline,
lastActiveAt, isMinimized }
TmuxWindow { index, title, cwd, panes[] } // title 为 shell 默认值时 UI 回退显示 cwd
Host { id, name, group, user, addr, port, os, tsnetIP, hasTmux, hostKeyPinnedAt }
```
关键转换`选择会话 → .terminal``首页/最小化 → .home`会话保留`关闭 → detach 或 kill → .home``Pin → .pinned`触发一次 PTY resize)。
## Files
- `terminalX iPadOS.dc.html` 全部 13 块屏幕的设计稿画板可缩放平移
- `CLAUDE.md` / `README.md`源项目文档若已在仓库中则以仓库版本为准
## Assets
无位图资产图标全部来自 Phosphor实现时换 SF Symbols字体 JetBrains MonoGoogle Fonts终端配色 Catppuccin