# PLAN_IOS_IPAD.md — iPad 适配（自适应布局，非分叉）

> 落地方案文档。目标：让已完成的 iPhone 客户端（[PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md)，P0+P1 已交付，分支 `feat/ios-client`）**原生适配 iPad**——大屏分栏、双向布局、指针/硬件键盘，而**不分叉出第二套 UI**。
> 拓扑决策：**单一代码库 + size-class 自适应**——`NavigationSplitView` 在 regular 宽度（iPad 全屏/大分屏）给 sidebar+detail，在 compact 宽度（iPhone、iPad Slide Over/小分屏）**自动退化为现有 stack**。iPhone 行为字节级不变。
> 状态：**规划中（2026-07-05，未开工）**。
> 本文是 iPhone 计划之上的**布局适配层**，不改协议/会话模型/纯逻辑包；沿用 [PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md) 的 §3 契约、§4 工程标准、§5 安全模型、§6 并行规则。冲突以 iPhone 计划为准。
> **G1 日志铁律**：`PROGRESS_LOG.md` 由 orchestrator 独写；被派 subagent 不写 LOG，在返回消息末尾附可粘贴条目。

---

## 0. 目标与范围

### 做什么

在 iPad（iPadOS 17+）上把「口袋驾驶舱」升级为「桌面级驾驶舱」，利用大屏做手机做不到的事：

- **分栏常驻**：左 sidebar = 会话列表（+ Projects 入口），右 detail = 终端 + gate/digest 叠层——不用来回 push/pop，一眼看全 + 直接介入。
- **终端更宽**：iPad 全屏能放下接近桌面的列数，直接缓解「宽桌面 + 窄手机同看一个全屏 TUI 折行成竖条」的多设备张力（见 [PROGRESS_LOG](./PROGRESS_LOG.md) 该条）——iPad 自己就是宽屏 writer。
- **双向布局**：横竖屏、Split View、Slide Over、Stage Manager 尺寸变化全程 size-class 自适应，绝不写死方向。
- **硬件键盘为一等公民**：iPad 常接键盘——现有 `UIKeyCommand` 全键位复用；软键盘 KeyBar（`inputAccessoryView`）在有硬件键盘时可隐、无则保留。
- **指针/悬停**：iPadOS 指针 hover 高亮、右键（次要点击）上下文菜单（kill/新建/在 cwd 开）。

### 不做（本期范围外）

- **多窗口 / 多场景（`UISceneConfiguration` 多实例、拖拽会话到新窗口）**：iPad 能开两个终端并排是诱人的，但涉及场景生命周期重构 + 每场景独立 SessionEngine，单列一期（见 §7 后续）。本期 = 单场景自适应分栏。
- **Stage Manager 外接显示器专属布局**：本期只保证 Stage Manager 下尺寸变化不崩、布局自适应；不做外接屏专属多窗排布。
- **Apple Pencil、拖放文件进终端、Mac Catalyst**：非目标（同 iPhone 计划 §0）。
- **纯逻辑包改动**：`WireProtocol/SessionCore/HostRegistry/APIClient` 与设备无关，**一行不改**（若发现某常量隐含 iPhone 假设 → 回对应包 owner，不在本计划直改）。
- **服务器 / `public/` / `src/`**：零触点（iPad 只是又一个说同一协议的客户端）。

### 关键约束：iPhone 零回归（硬性）

**任何自适应改动必须先满足 compact 宽度 == 现有 iPhone 行为**。每个任务的验收都含一条「iPhone 模拟器全套件仍绿 + 目视无变化」。self-check：compact size class 下走的代码路径应与改动前逐帧一致（分栏只在 regular 宽度激活）。

---

## 1. 整体架构 / 适配策略

```
                       size class 驱动的单一根视图（AdaptiveRootView）
        ┌─────────────────────────────┴─────────────────────────────┐
   horizontalSizeClass == .compact                 horizontalSizeClass == .regular
   （iPhone / iPad Slide Over / 小分屏）             （iPad 全屏 / 大分屏 / Stage Manager 大窗）
        │                                                  │
   现有 NavigationStack                            NavigationSplitView
   列表 → push 终端（字节级不变）                    ├ sidebar: 会话列表 + Projects section
                                                   └ detail : TerminalContainerView（gate/digest 叠层）
        └───────────────── 共享同一 AppCoordinator / 同一 SessionEngine ─────────────────┘
                    隐私遮罩（ZStack 顶层，!= .active）· scenePhase · deep link 全部设备无关，原样复用
```

**为什么自适应而非 iPad 分叉**：`horizontalSizeClass` 是同一 App 内运行时可变量（iPad 拉出 Slide Over 立刻从 regular 变 compact）——分叉两套 UI 无法应对同一次运行内的尺寸切换，且双倍维护。自适应 = 一套代码、一处 size-class 分支、compact 分支就是现有已测代码。

**为什么 detail 复用 TerminalContainerView 不动**：终端 + gate + digest 的组装（T-iOS-15/24 的 `TerminalContainerView`）与它挂在 push destination 还是 split detail 无关——只换外层容器，内容零改。SessionEngine 单活 WS、latest-writer-wins、隐私遮罩、生命周期全部设备无关，直接复用。

**列宽收益（自动，非新逻辑）**：终端列数由 SwiftTerm 的 `sizeChanged` 从其视图宽度 + 字号算出（T-iOS-11 已实现），detail 面板越宽 → cols 越多 → 越接近桌面宽度。**无新代码**，是分栏的自然结果。

**依赖方向**：iPad 适配全部落在 **App 胶水层**（`ios/App/WebTerm/**`）；纯包不动；自适应「决策」抽成纯函数（`LayoutMode` 之于 size class，仿 `PrivacyShadePolicy` 先例）以便单测，SwiftUI 布局本身靠模拟器目视 + XCUITest。

---

## 2. 目录结构（新增/改动）

```
ios/App/WebTerm/
├── Wiring/
│   ├── RootView.swift              # 改：抽 compact 分支为子视图，加 regular 分支入口（T-iPad-2）
│   ├── AdaptiveRootView.swift      # ★新：size-class 分支 + LayoutMode 决策消费（T-iPad-2）
│   ├── SplitRootView.swift         # ★新：NavigationSplitView（sidebar+detail）（T-iPad-2）
│   ├── LayoutMode.swift            # ★新：纯函数 size class → LayoutMode（.stack/.split）（T-iPad-2）
│   └── AppCoordinator.swift        # 改：sidebar 选中态 ↔ 现有 terminal/projects 路由桥（最小增量）
├── Screens/
│   ├── SessionListScreen.swift     # 改：sidebar 语境下的选中高亮 + Projects section（自适应，compact 不变）
│   └── ProjectsScreen.swift        # 改：regular 宽度下作为 sidebar section / 多列网格（compact 仍是 sheet）
├── Components/
│   ├── KeyBar.swift                # 改：硬件键盘在场时可隐（T-iPad-3）
│   └── TerminalContextMenu.swift   # ★新：指针右键/长按上下文菜单（kill/新建/cwd 开）（T-iPad-3）
├── project.yml                     # 改：TARGETED_DEVICE_FAMILY "1,2" + iPad 方向/plist（T-iPad-1）
└── WebTermTests/ , WebTermUITests/ # 各任务的测试
```

`ios/Packages/**` 零改动。

---

## 3. 契约（自适应决策的可测核）

```swift
// LayoutMode.swift — 纯函数，唯一 size-class 决策点（仿 PrivacyShadePolicy）
public enum LayoutMode: Equatable { case stack, split }

public enum LayoutPolicy {
    /// regular 宽度 → split（sidebar+detail）；compact → stack（现有 iPhone 路径）。
    /// 唯一判据，禁止在视图里散落 sizeClass 分支。
    public static func mode(horizontalSizeClass: UserInterfaceSizeClass?) -> LayoutMode
}

// SidebarSelection — split 模式下 sidebar 选中态（与现有 AppCoordinator 路由等价映射）
enum SidebarItem: Hashable { case session(UUID), newSession, projects }
```

隐私遮罩、scenePhase、deep link、SessionEngine 契约**沿用 iPhone 计划 §3，不重定义**。

---

## 4. 工程标准

沿用 [PLAN_IOS_CLIENT.md §4](./PLAN_IOS_CLIENT.md) 全部（TDD 强制、不可变、文件 ≤400 行、函数 <50 行、早返回、无魔法数字、系统边界验证、显式错误、conventional commits、覆盖率仅计 4 个纯包）。追加两条 iPad 专属：

- **size-class 分支只出现在一处**（`LayoutPolicy`）；视图内严禁散落 `if sizeClass == …`。
- **iPhone 零回归是每个任务的验收前置**（compact 路径 == 改动前）。

---

## 5. 任务清单

> 状态图例 `[ ]`/`[~]`/`[x]`/`[!]` 同 iPhone 计划。ID 稳定：`T-iPad-N`。测试框架 Swift Testing；XCUITest 仅 UI happy path。

### W0 · 可安装性（串行，先行）

#### T-iPad-1 · device family + iPad plist/方向 `[ ]` · ~0.5 pd
- **Owns**: `ios/project.yml`（device family、iPad 方向、必要 plist）、`.github/workflows/ios.yml`（加 iPad 模拟器测试腿）
- **Depends**: 无
- **Steps(测试先行)**:
  - [ ] `TARGETED_DEVICE_FAMILY` 由 `"1"` 改 `"1,2"`（project + 各 target level——注意 XcodeGen target 默认覆盖，见 iPhone 计划 W5 finding，逐 target 显式设）
  - [ ] iPad 方向键全开（`UISupportedInterfaceOrientations~ipad` 含 Portrait/Landscape/UpsideDown）；iPhone 方向键**不动**
  - [ ] ATS 五段 CIDR / usage description 原样（设备无关，复核仍在）
  - [ ] `xcodegen generate` → 拆构建产物 plist 断言 `UIDeviceFamily [1,2]`
  - [ ] ios.yml 加一条 `xcodebuild test` 腿跑 iPad 模拟器（iPad Pro 11" 或 iPad (A16)）
- **Accept**: iPad 模拟器 `xcodebuild build` 通过、空跑现有全套件绿（此刻仍是 iPhone 布局放大，不崩）；产物 plist `[1,2]`
- **安全注**: device family 放开后 T-iPad-5 的 ipa 核对须覆盖 iPad 产物（capability↔usage 一一对应不变）

### W1 · 自适应导航壳（核心）

#### T-iPad-2 · AdaptiveRootView + NavigationSplitView + LayoutPolicy `[ ]` · ~2 pd
- **Owns**: `Wiring/{AdaptiveRootView,SplitRootView,LayoutMode}.swift`（新）、`Wiring/RootView.swift`（改：现有 stack 抽成 `StackRootView` 子视图供 compact 复用）、`Wiring/AppCoordinator.swift`（改：sidebar 选中 ↔ 路由最小桥）、对应测试
- **Depends**: T-iPad-1
- **Steps(测试先行, RED)** — `LayoutPolicyTests` + `SidebarSelectionTests`:
  - [ ] `LayoutPolicy.mode(.compact) == .stack`、`.mode(.regular) == .split`、`.mode(nil) == .stack`（未知按最保守 = 现有路径）
  - [ ] SidebarItem ↔ AppCoordinator 路由等价：选 `.session(id)` == 现有 open(id)；`.newSession` == open(nil)；`.projects` == 现有 projects 呈现——**同一 coordinator API，split 只是另一个触发面**（断言不新增会话生命周期路径）
  - [ ] compact 分支渲染的视图树 == 改动前 `RootView`（快照/结构断言：隐私遮罩仍 ZStack 顶层、scenePhase/deepLink/sheet 全在）
  - [ ] size class 从 regular→compact 切换（iPad 拉 Slide Over）时选中态保持、终端不重建（复用 T-iOS-29 的 `.id(controller.id)` 稳定性，断言 controller 不换）
- **Steps(实现, GREEN)**:
  - [ ] `AdaptiveRootView` 读 `@Environment(\.horizontalSizeClass)` → `LayoutPolicy.mode` → 选 `StackRootView`（现有）或 `SplitRootView`
  - [ ] `SplitRootView` = `NavigationSplitView { sidebar } detail { TerminalContainerView 或空态 }`；sidebar = SessionListScreen（选中绑定）+ Projects section 入口；detail 空 = 「选择或新建会话」占位
  - [ ] 隐私遮罩/scenePhase/deepLink/task(bootstrap) 上提到 `AdaptiveRootView`（两分支共享，绝不重复布线）
- **Accept**: iPad 模拟器横竖屏 + Split View 拉入拉出全程分栏/退栈自适应无崩；**iPhone 模拟器全套件绿 + 目视逐屏无变化**；`LayoutPolicy` 覆盖率 100%
- **安全注**: 隐私遮罩必须仍是**两分支共同的** ZStack 顶层——split detail 的终端字节同样要被 `!= .active` 遮住（新增一条 iPad 遮罩目视验收：Stage Manager 切走 → detail 终端被遮）

### W2 · 逐面适配（并行，文件互斥）

#### T-iPad-3 · 终端面板：KeyBar 自适应 + 指针上下文菜单 `[ ]` · ~1 pd
- **Owns**: `Components/{KeyBar,TerminalContextMenu}.swift`、`Screens/TerminalScreen.swift`（增量）、测试
- **Depends**: T-iPad-2 · **Parallel-safe**: T-iPad-4
- **Steps(测试先行)**:
  - [ ] KeyBar 可见性纯谓词：`GCKeyboard.coalesced != nil`（硬件键盘在场）→ 默认隐；无 → 显；用户可手动切（谓词单测，硬件态注入）
  - [ ] 上下文菜单（右键/长按）项 = {在 cwd 开新会话、kill、复制选区}——动作复用现有 OpenRequest/killSession 通道（断言不新增网络路径；kill 仍带 Origin）
  - [ ] 指针 hover 高亮不改字节流（纯 UI）
- **Accept**: iPad 接键盘时 KeyBar 自动隐、去键盘复现；右键菜单在 iPad 生效；iPhone 无硬件键盘时 KeyBar 行为不变
- **安全注**: 上下文菜单的 kill/开会话是既有 G/RO 通道的又一触发面——测试名标「复用 APIClient，无绕过」

#### T-iPad-4 · Projects/Timeline/sheet 大屏化 `[ ]` · ~1 pd
- **Owns**: `Screens/ProjectsScreen.swift`（增量）、Projects/Timeline 呈现方式（regular 下 `.sheet` → 列/`.presentationDetents` 或 sidebar section）、测试
- **Depends**: T-iPad-2 · **Parallel-safe**: T-iPad-3
- **Steps(测试先行)**:
  - [ ] Projects 网格列数随宽度自适应纯函数（compact=1 列现状、regular=多列）——列数决策单测
  - [ ] regular 宽度下 Projects 作为 sidebar section 或 detail 列（compact 仍走现有 sheet，分组/收藏/prefs 往返逻辑零改——只换容器）
  - [ ] Timeline/其它 sheet 在 iPad 用合适 detent/尺寸不占满全屏
- **Accept**: iPad 上 Projects 多列、Timeline 合理尺寸；iPhone 全部走原有 sheet 路径不变；prefs 未知键往返（T-iOS-38 不变量）回归绿

### W3 · 验收（report-only, G4）

#### T-iPad-5 · iPad 验收 + iPhone 回归 + 安全核对 `[ ]` · ~0.5 pd
- **Depends**: T-iPad-2/3/4 · **Owns**: 无源码（report-only，findings 派回 owner）
- **Steps**:
  - [ ] **F-iPad 走查**（§6 清单）逐条：分栏、横竖屏、Split View/Slide Over/Stage Manager 尺寸切换、硬件键盘/指针、终端更宽列数、遮罩覆盖 detail
  - [ ] **iPhone 零回归**：iPhone 16 全套件绿 + 逐屏目视对比无变化（compact 路径未动）
  - [ ] **安全**：拆 iPad 产物 ipa 核对（UIDeviceFamily [1,2]、五段 CIDR、usage 一一对应）；分栏后隐私遮罩仍遮 detail 终端；上下文菜单/键盘触发的 G 调用仍走 APIClient 带 Origin
  - [ ] 真机项（真 iPad 手势/键盘/指针目视、Stage Manager 实机）→ DEFERRED，附手工清单

---

## 6. 测试与验收

### 设备矩阵

| 层 | 环境 |
|---|---|
| 纯逻辑（LayoutPolicy/谓词） | macOS `swift test` / 宿主 WebTermTests |
| App 自适应 | iPhone 16（compact 回归）+ iPad Pro 11"（regular）+ **iPad Split View（同一次运行内 regular↔compact 切换）** |
| XCUITest | iPad happy path 1 条：sidebar 选会话 → detail attach → 输入 → gate approve（复用 iPhone happy path 骨架，换分栏断言） |
| 真机必测 | 真 iPad 分栏手势、硬件键盘全键位、指针 hover/右键、Stage Manager（1 台，DEFERRED 至有设备） |

### 验收演示脚本（F-style）

- **F-iPad-1** iPad 全屏 → 左 sidebar 会话列表 + 右 detail 终端同屏；选另一会话 → detail 即时切换（回放恢复）。
- **F-iPad-2** 横竖屏旋转 → 布局自适应、终端 resize 重绘、无错位。
- **F-iPad-3** 拉出 Slide Over（compact）→ 自动退化为 stack（== iPhone 布局）；推回全屏 → 恢复分栏；全程会话不断、终端不重建。
- **F-iPad-4** iPad 接硬件键盘 → KeyBar 自动隐、`UIKeyCommand` 全键位可用；拔键盘 → KeyBar 回来。
- **F-iPad-5** 指针右键会话行/终端 → 上下文菜单（kill/新建/cwd 开）生效。
- **F-iPad-6** iPad 全屏终端列数明显多于 iPhone（接近桌面），宽 TUI 折行大幅缓解。
- **F-iPad-7** Stage Manager/切后台 → detail 终端被隐私遮罩覆盖（快照不泄露）。
- **F-iPad-8**（回归）iPhone 16 逐屏与适配前目视一致、全套件绿。

---

## 7. 风险与开放问题

| 风险 / 问题 | 影响 | 缓解 / 待定 |
|---|---|---|
| `NavigationSplitView` 在 size-class 频繁切换（Slide Over 反复拉）时状态/选中丢失 | 中 | 选中态提升到 AppCoordinator（设备无关单一真值），T-iPad-2 专项测切换保持；终端 `.id(controller.id)` 稳定性已在 P1 验证 |
| 隐私遮罩在 split detail 漏遮 | 高（安全） | 遮罩上提到 AdaptiveRootView 两分支共享 ZStack 顶层；T-iPad-2/5 双重目视 |
| iPhone 回归（compact 分支被无意改动） | 高 | 每任务验收前置 iPhone 全绿 + 目视；compact 分支复用现有 StackRootView 原样 |
| SwiftTerm 在超宽 detail 的性能/选择手感 | 低 | 复用 iPhone 已测路径；真机目视 |
| 多窗口诱惑导致 scope 膨胀 | 中 | 本期明确单场景（§0 非目标）；多窗口另立计划 |

**待你拍板**：
1. iPad 最低系统版本：iPadOS 17（与 iPhone 一致）还是抬到 18/26？
2. 本期是否要指针右键上下文菜单（T-iPad-3 后半）——纯锦上添花，可砍到后续。
3. 多窗口（拖会话开新窗并排两终端）确认放到下一期？

**工作量合计**：W0 0.5 + W1 2 + W2（3∥4）2 + W3 0.5 ≈ **5 人日**（并行后墙钟更短）。

---

## 8. 与现有文档的关系

- 本文只改 `ios/App/WebTerm/**` + `project.yml`/`ios.yml`；`ios/Packages/**`、`src/`、`public/` 零改动。
- 是 [PLAN_IOS_CLIENT.md](./PLAN_IOS_CLIENT.md) 的**布局适配层**，复用其 §3 契约/§4 标准/§5 安全/§6 并行规则；把该计划 §0 非目标里的「iPad 优化布局」提取为本期。
- 多设备共享 PTY 的尺寸张力（[PROGRESS_LOG](./PROGRESS_LOG.md) 记录的「宽桌面+窄手机折行」）：iPad 因自身是宽屏 writer 而**天然缓解**，非本计划新机制。
- 冲突裁决：iPhone 计划管协议/会话/安全模型；本文只加自适应布局，不改之。
