# MusePi 移动端设计规范（Mobile Companion）

> **状态（2026-08-31 核对）**：壳已构建（`packages/mobile` Capacitor Android + desktop-web 移动入口，CI mobile job 活跃；托盘/会话导入/usage/win32 玻璃均已在）。2026-08-31 完成移动端交互专项核对并修复 6 项缺陷（见 §12）：返回键层栈完整化（back-stack 统一调度）、SessionsSheet 常挂载退场动画、ask 多选 pending 语义、连接成功后才记录/写 hash、≤520px 面板折叠。
>
> 2026-08-24 定稿。范围：`packages/mobile`（Capacitor Android 壳）+ `packages/desktop-web` 的
> `mobile.html` / `mobile.tsx` / `mobile.css` 移动入口。桌面 web（`index.html`）与本规范无关。
> 参考：openchamber `packages/mobile`（HANDOFF.md + `apps/MobileApp.tsx`）、musepi GUI 设计语言
> （`docs/gui-design.md`）、高星移动端设计惯例（Linear / Obsidian / Arc / Material3 / iOS HIG）。
> 结构：产品定位 → 信息架构 → 屏幕规格 → 组件与交互 → 视觉与动效 → 原生集成 → 无障碍 → 性能 → 验收 → 路线图。

## 1. 产品定位与设计原则

MusePi 移动端是 **桌面 agent 的"随身遥控器"**，不是桌面客户端的缩小版。用户在手机上做的事：
看桌面 agent 正在做什么、被询问时快速回应、会话结束后收通知回来继续。双手被占用的场景（手机放
支架上、单手拇指操作）占比高，因此一切交互围绕 **拇指可达 + 内容可读** 设计。

设计原则（按优先级）：

1. **内容优先** — 手机屏幕的首要价值是"看见 agent 的工作现场"（transcript / workspace 状态）。
   所有 chrome（header 按钮、面板入口）必须可折叠、可隐藏，不抢占内容空间。
2. **拇指可达** — 主要操作（发送、停止、返回）放屏幕下半区；上半区只放只读信息与次要入口。
   触控目标 ≥ 44×44 CSS px（iOS HIG 44pt / Material3 48dp 的下限取 CSS px）。
3. **一次一跳** — 任何操作最多一次导航到达；不存在超过两层的页面栈（例外：设置类全屏面，见 §3）。
4. **断线韧性** — LAN 场景 Wi-Fi 切换/daemon 重启常见。连接层必须自动重连（已有 phase 状态机），
   UI 必须明确表达 `connecting / waiting / reconnecting / ended` 四种非 live 状态，且任一状态可一键恢复。
5. **原生质感** — 壳层（键盘、状态栏、安全区、返回键、通知）走 Capacitor 原生能力；内容层复用
   musepi web 设计 token（oklch 深紫表面 + 祖母绿 accent + 弹簧动效），不引入第二套视觉语言。
6. **节能** — 移动端是观察者，不是轮询器。所有订阅走 guest 协议帧（16ms 批量合并），后台不做
   定时轮询；通知由事件驱动，非前台时由原生通知呈现。

与桌面 GUI（`gui-design.md`）的关系：同一 token 体系（`tokens.css`）、同一 i18n 契约、同一弹簧动效
语言；差异只在 **布局适配**（单栏 vs 多栏）与 **原生集成面**（Capacitor 插件）。

## 2. 参考基准（设计来源）

### 2.1 openchamber 移动端（primary parity）

| 模式 | openchamber 做法 | musepi 对应/取舍 |
|---|---|---|
| 壳层 | Capacitor 包 web 构建；`mobile.html → index.html` 免运行时重定向 | ✅ 同款（`prepare-web-assets.mjs`） |
| 键盘 | `resize: "none"` + CSS inset 变量驱动（非原生 adjustResize 动画滞后） | ✅ 同款（`--mp-keyboard-inset`） |
| 状态栏 | overlay + safe-area inset + 主题色 | ✅ 同款（`setupImmersiveSystemBars`） |
| 首帧主题 | 阻塞脚本预置 `color-scheme` + 背景色，杜绝白闪 | ✅ 同款（`mobile.html` 内联脚本） |
| 会话切换 | 手机：底部 sheet；平板：常驻左侧栏；SIZE 类而非设备检测 | ✅ 底部 sheet（§11.1 `SessionsSheet`，header 标题触发）|
| 工作区抽屉 | 右缘滑动 → 抽屉（Changes/Files/Terminal/Notes/MCP 多 tab） | ⏳ 现状：header 面板按钮 → 全屏 panel；P3 抽屉化（§10）|
| 返回键 | 分层关闭：plan → surface → drawer → chat | ✅ 已实现（capacitor.ts setupAndroidBackHandler + app.tsx onBack 分层） |
| 推送 | APNs/FCM + relay + presence 抑制 | ⏳ 本架构无云端：本地通知（§6.4） |
| 深链 | `openchamber://` 意图词汇表（通知/小组件/Control Center 复用） | ✅ 原生 `musepi://` 深链（§11.3）+ 通知点击跳会话 |
| QR 配对 | mlkit 捆绑 barcode 模型离线扫描 | ✅ 同款（`@capacitor-mlkit/barcode-scanning`） |
| 安全存储 | `@aparajita/capacitor-secure-storage` 存连接 token | ✅ 同款（`secure-store.ts`） |

### 2.2 高星项目移动端惯例（采纳项）

- **Linear mobile** — 底部 sheet 承担导航（会话/工作区），全屏面只保留"设置"类；我们的面板（board/
  scheduled/files）在手机上应保持全屏但可从 header 一键往返（现状已符合），P3 迁移到抽屉。
- **Obsidian mobile** — 安全区纪律：`env(safe-area-inset-*)` 必须兜底 + 旧 WebView 固定值降级
  （已实现 `setupSafeAreaFallback`）；内容滚动容器 `overscroll-behavior: contain` 防滚动链
  （已实现）；`-webkit-overflow-scrolling: touch` 惯性滚动（已实现）。
- **Arc / 手势优先** — 边缘滑动返回、下拉刷新属于"锦上添花"，必须有且仅有一种显式等价操作
  （back 按钮 / 重连按钮）。手势永远不承载唯一入口。
- **Material3 / iOS HIG** — 触控目标 44px、输入框 ≥16px 防 iOS 聚焦缩放、IME 合成期不误触发送
  （`shouldSubmitOnEnter` + composition guard 已实现）、系统返回键与 UI 层栈一致。

### 2.3 musepi 客户端设计语言（继承，不复制）

- token：`--bg / --bg-raised / --accent(emerald) / --ring(cyan)` 等 oklch 体系，`--spring*` 弹簧曲线；
- 交互：accent 仅存在于语义强调，表面中性暖暗色（openchamber/zcode 式）；hover 态在触屏上不承担
  功能（触屏无 hover，`tr-actions` 的 hover 显示需移动端常显或点按替代 —— §5.4）；
- i18n 契约：`TranslationKey = keyof typeof zhCN`，新增文案必须进 zh-CN 与 en-US（en 即 key 直通）。

## 3. 信息架构与导航模型

### 3.1 屏幕地图

```
Connect（连接引导）                    ← 未连接 / 退出会话
  ├─ QR 扫描（原生壳）
  ├─ 配对码 + 地址（纯 ws）
  ├─ 粘贴 /collab 链接
  └─ 最近连接列表（点按直连 / 删除）
      │
      ▼ connect
Workspace（工作区，可选）              ← 多会话 host 才出现（workspace !== null）
  ├─ 项目分组侧栏（可折叠）
  └─ 会话卡片网格（working/paused/idle + 消息数 + 相对时间）
      │
      ▼ 聚焦会话
Session（会话）
  ├─ Header：返回 / 标题 / cwd / 状态（dot·gauge·avatars）/ 面板入口 / 连接切换 / 退出
  ├─ Transcript：消息流 + 工具卡片 + 图片 + mermaid + 附件
  ├─ Composer：输入 / 发送 / 停止 / queued 计数；ask 模式（select/editor）
  └─ 覆盖层：AgentsRail（右侧抽屉）· AgentDrawer（agent 详情）· 面板（board/scheduled/files）
        · ServerSwitcher popover · Toasts · Banners
```

### 3.2 层叠模型（z-order + back 层栈，从底到顶）

```
0   sh-app（header + main + composer 常驻骨架）
1   sh-main 内：workspace / 面板 / transcript（互斥三态）
2   sh-rail + backdrop（agents 抽屉，≤768px 绝对定位覆盖）
3   ag-drawer + backdrop（agent 详情，全屏宽 min(440px, 92vw)）
4   sh-ended / sh-banner（连接态横幅，常驻可感知）
5   sh-toasts（瞬时，4 条封顶）
```

**模态覆盖层**（覆盖于 transcript 之上，同一时刻最多一层叠加，但 drawer 可叠于 rail 上）。

**返回键（Android）层栈优先级**（由 `lib/back-stack.ts` 统一调度，`dispatchBack()` 从高到低尝试，第一个消费的停止）：

| 优先级 | 层 | 说明 |
|---|---|---|
| 100 | AgentDrawer | 最顶层，agent 详情 |
| 95 | QrScanner | 扫码全屏覆盖（ConnectScreen） |
| 90 | SessionsSheet | 会话切换底部 sheet |
| 85 | ServerSwitcher | 多服务器切换 popover |
| 84 | PanelMenu（窄屏） | ≤520px 折叠面板菜单 |
| 80 | AgentsRail | 子代理抽屉 |
| 60 | 面板（board/scheduled/files） | 全屏面板 |
| 40 | Workspace 返回 | 会话聚焦 → 工作区目录 |

规则：
- 同一时刻最多一层"模态覆盖"（rail / drawer / 面板三选一互斥，但 drawer 可叠于 rail 上）；
- 返回键按优先级从高到低处理，一次按压只关闭最上层，绝不关闭两层；
- 手机上 workspace 与 session 是**平级切换**（header 返回按钮 + workspace 内聚焦），不叠加。

### 3.3 自适应规则（SIZE 类，非设备检测）

| 断点 | 布局 |
|---|---|
| > 1024px | 桌面 web 全功能（本规范不覆盖） |
| 600–1024px | 平板/折叠屏展开：connect 卡片 480px 封顶；workspace 侧栏保留桌面形态；面板全屏 |
| ≤ 640px | 手机：header 收窄、chips/度量隐藏、按钮标签隐藏、输入 16px、触控目标 44px |
| ≤ 520px | 手机窄：header 装饰控件（accent/language/avatars/dot）折叠，只留核心控制；面板按钮折叠为"面板"菜单（§4.3） |
| ≤ 420px | 小屏：connect 卡片全宽、配对行纵向堆叠 |

断点由 CSS `@media (max-width)` 驱动；`--safe-top/--safe-bottom/--mp-keyboard-inset` 由 JS/插件
注入（§6.1）。未来 foldable 形态（展开/折叠运行中切换）由容器查询或 SIZE 类承担，不做设备 UA 判断。

## 4. 屏幕规格

### 4.1 Connect（连接引导）

目标：**3 秒内到达最近会话**。层级：最近连接（一键）> QR / 配对码（日常）> 粘贴链接（兜底）。

- 头部：品牌 lockup + 主题/accent/语言切换（与桌面一致，`--order` 语义化排序）；
- 最近连接：`secure-store` 优先、localStorage 镜像（防隐私模式空转）；删除即 `✕`，无需确认
  （可随时重连恢复）；**仅成功连接（welcome 帧到达后）才记录到最近列表**，失败/超时不记录；
- 方法卡片：QR（仅原生壳，懒加载 mlkit 保 bundle 体积）→ 配对码（纯 ws 常可用）→ 粘贴链接；
  手风琴 `useCollapseHeight` 保持挂载可动画（aria-hidden + inert 折叠态）；
- 配对流程：6 位码 + 电脑地址 → `pair.resolve`（6s 超时，友好错误文案）→ 记住地址（secure）；
- 跳过态：`sh-connect-card--empty` 空态 + "connect to a computer" 返回引导；`SKIP_KEY` 持久化；
- 错误：`localError ?? error` 单行展示，`key={shown}` 触发重渲染动画。

验收：断网时打开配对码 → 6s 内出"cannot reach the computer — same Wi-Fi?"；QR 权限拒绝 →
"scan failed — use the pair code instead"；跳过态重启后仍跳过。

### 4.2 Workspace（工作区）

- 顶部：标题 + 副文案"tap one to watch it live"；
- 卡片：标题 / working(旋转) · paused · idle 状态徽标 / 相对时间 / 消息数 / cwd（shortenPath）/ history chip；
- 项目分组侧栏：≤768px 转横向顶部限高 220px 可折叠条（`.sh-ws-sidebar`）；
- 空态："no sessions yet"。
- 交互：点卡片 → `selectWorkspaceSession(id)` 聚焦；header 返回键 → `selectWorkspaceSession(null)`。

### 4.3 Session（会话）

#### Header（移动端收敛）

桌面 header 的元素在 ≤640px 的取舍矩阵：

| 元素 | ≤640px | ≤520px | 理由 |
|---|---|---|---|
| 返回（面板/workspace） | ✅ 常显 | ✅ | 导航必需 |
| 标题 + cwd | 标题 ✅ / cwd ❌ | 标题 ✅ | cwd 冗余（卡片已示） |
| read-only / model / thinking chips | ❌ | ❌ | 只读有 banner 兜底 |
| context gauge | 只显示百分比数字（track 隐藏） | 百分比 ❌ | 数字即信息 |
| avatars / dot | ✅ | ❌ | 会话状态已由 banner/dot 表达，窄屏删 |
| 面板按钮（board/scheduled/files） | ✅ 4 按钮 | ✅ 折叠为 1 个"面板"菜单 | 核心功能入口，窄屏折叠保入口控溢出 |
| theme / accent / language 切换 | ✅ | ❌ | 装饰性；connect 屏仍可达 |
| server switcher / rail / leave | ✅ | ✅ | 会话级操作，保留 |

触控目标：`sh-btn-icon` ≤640px 时 `min-width/min-height: 44px`（§5.3）。

#### Transcript（移动端）

- 消息行：≤640px 转纵向 flex（`.tr-row` 现有规则），gutter 上移为行首小字；
- 用户消息图片走共享叠卡（craft-agents 式）；assistant 消息无图片块（wire 类型限制）；
- **hover-only 操作（`.tr-actions`）在触屏上不可达** → 移动端常显（opacity 1）或长按菜单；
  TTS 播放按钮（`.tr-action--speaking`）恒显，不受影响；
- 滚动：`.sh-transcript` 为滚动容器，`overscroll-behavior: contain` + 惯性滚动（已实现）；
- 时间戳锚定工作计时器（`use-working-now`）以 `message.timestamp` 为准，切会话不重置（已有约束）。

#### Composer（移动端）

- 布局：输入框 + 发送/停止按钮；≤640px 隐藏文字标签（`sh-btn-label`），纯图标 44px 热区；
- 键盘：`--mp-keyboard-inset` 垫底（§6.1），输入框 16px 防 iOS 聚焦缩放（已实现）；
- IME：Enter 提交必须经过 composition guard（已实现），中文输入法候选确认不触发发送；
- ask 模式：`select` 选项大按钮（≥44px）、`editor` 预填输入框 —— 是移动端"被询问时快速回应"
  的核心路径，选项按钮必须全宽可点；
- **ask 多选（checkbox）**：host 端是逐项 toggle 循环（每次 `ui-response` 切换一个选项，host 以
  新 reqId 重发带更新 `checkedIndices` 的请求，直到点 "Next →" 提交）。移动端点选项后 ask 卡
  **保持挂载**（`uiRequestPending` 期间选项禁用防双击），显示多选提示；host 重发同 title 请求
  时无缝替换选中态。单选（radio）点选项立即提交；
- queued 计数：`×N` 徽标（标签在 ≤640px 隐藏，数字保留）。

### 4.4 面板（board / scheduled / files）

移动端全屏（`sh-panel`），header 返回按钮（MessageSquare 图标）回 transcript；与 rail 互斥。
面板内滚动与键盘处理同 transcript 契约。P3 迁入工作区抽屉（§10）。

### 4.5 覆盖层

- **AgentsRail**：≤768px 右侧绝对定位抽屉（280px / 85vw）+ backdrop；子代理首次出现自动展开
  （autoOpenedRef，已有）；
- **AgentDrawer**：`min(440px, 92vw)` 右滑入（`ag-drawer-in` 150ms）；含 agent 名/生命周期/进度；
- **Banners**：connecting/waiting/reconnecting 横幅 + ended 全屏卡片（Rejoin / New link）；
- **Toasts**：右上堆叠，info 4s / warning 8s / error 常驻可关，4 条封顶。

## 5. 组件与交互规范

### 5.1 手势清单

| 手势 | 行为 | 等价显式操作 | 状态 |
|---|---|---|---|
| 点按 | 全部分发 | — | ✅ |
| 返回（系统键） | 分层关闭（§3.2） | header 返回按钮 | 本次补 |
| 边缘滑动（左/右） | 会话/工作区抽屉 | header 按钮 | P3 |
| 下拉刷新 | 重连/重新拉快照 | Rejoin 按钮 | P3 |
| 长按 | （预留）复制/操作菜单 | 行内按钮 | P3 |

原则：手势从不承载唯一入口（Arc 惯例）。

### 5.2 触觉反馈（haptics）

原生壳内关键动作轻震（`navigator.vibrate`，Android WebView 支持；桌面/浏览器静默跳过）：

| 动作 | 时长 |
|---|---|
| 连接成功（QR/配对/链接） | 12ms |
| 发送消息 | 8ms |
| 停止 turn | 15ms |
| 配对失败/连接错误 | 双脉冲 30ms |

`prefers-reduced-motion` 或非原生壳 → 全部跳过。WebView 无权限 API，包 try/catch 静默。

### 5.3 触控目标与间距

- 图标按钮（header / composer 动作）≤640px：`min-width/height: 44px`；`sh-btn` 保持视觉 22px 内边距，
  热区扩展不改变视觉；
- 选项按钮（ask select / connect method / workspace 卡片）：min-height 44px（已有 connect-method）；
- 输入框：16px 字体（已有），`caret-color` 跟随 accent；
- 行距：消息行 ≥ 8px 垂直间距（已有），群组按钮间 ≥ 4px 缝隙防误触。

### 5.4 触屏无 hover 的处理

- `.tr-actions`（消息操作行）在 `(hover: none)` 设备上 `opacity: 1` 常显；
- 所有"hover 提示"信息必须同时有 `title` 属性（长按可读）；
- 卡片 hover 上浮（translateY）在触屏为 no-op，不承担功能。

## 6. 原生集成（Capacitor）

### 6.1 键盘（Keyboard）

- 配置：`resize: "none"` + `resizeOnFullScreen: true` + `autoBackdropColor: "dom"`（capacitor.config.ts）；
- 事件：`keyboardWillShow/Hide` → `--mp-keyboard-inset`（mobile.tsx `setupCapacitorKeyboardInset`）；
- 兜底：无插件壳（浏览器/旧 WebView）→ `visualViewport` 差值（>60px 才生效，`setupVisualViewportKeyboardFallback`）；
- 消费方：`.sh-composer` padding-bottom、`.sh-connect` padding（IME 顶起输入框 + connect 内
  `scrollIntoView({ block: "center" })` 提升聚焦输入）；
- 桌面 web 不加载 mobile.tsx，`--mp-keyboard-inset` 恒 0。

### 6.2 状态栏与安全区

- `StatusBar.setOverlaysWebView({ overlay: true })` + `setStyle`（跟随 `data-theme`）+ 透明背景
  （`setupImmersiveSystemBars`，boot 即执行，覆盖所有 Android 版本/ROM 的首帧闪色）；
- `--safe-top/--safe-bottom`：`env(safe-area-inset-*)` 原生支持时由 CSS 承担；旧 WebView
  （卓易通类兼容层）无 env → JS 固定 24/12px 降级 + StatusBar.getInfo().height() 精确覆盖；
- 消费方：header `padding-top`、composer/connect `padding-bottom`。

### 6.3 返回键分层导航（Android back）

- 壳：`@capacitor/app` 的 `backButton` 事件在原生壳内拦截（`mobile.tsx`）；
- 分发：`lib/back-stack.ts` 层栈注册表（2026-08-31 重构）。每个模态覆盖层
  （AgentDrawer / SessionsSheet / ServerSwitcher / 窄屏面板菜单 / AgentsRail / 面板 /
  Workspace 返回）用 `useBackLayer(priority, active, handler)` 注册自己的关闭 handler；
  `setupAndroidBackHandler` 调 `dispatchBack()` 从最高优先级到最低逐个尝试，第一个
  消费的停止——**一次 back 只关一层**，不再广播 CustomEvent；
- 桌面 web 不加载该监听，浏览器历史不受影响；
- 边界：无任何层打开时 `dispatchBack()` 返回 false，走 history.back() / 系统退出。

### 6.4 本地通知（无云端架构的推送等价）

- 触发：会话后台（`document.hidden`）+ 新 assistant 消息落定（timestamp 去重）；
- 内容：标题"musepi session update" + 正文 `msgText` 截 140 字符；`smallIcon: ic_stat_musepi`；
- 权限：**Android 13+ 必须先 requestPermissions，否则 schedule 静默丢弃** —— 首次连接成功即请求
  （原生壳内），拒绝后不再打扰；浏览器/桌面静默跳过；
- 前台抑制：`!document.hidden` 直接 return（转写本身即通知）；等价 openchamber presence 抑制的本地实现；
- 边界：`@capacitor/local-notifications` 懒加载（`import()`），非原生 bundle 不含插件代码。

### 6.5 QR 配对

- `@capacitor-mlkit/barcode-scanning`，barcode 模型捆绑进 APK（离线可用，无 Google Play 依赖）；
- `CAMERA` 权限 manifest 声明（`uses-permission` + 可选 `uses-feature`）；
- 结果 `displayValue` 即 collab 链接 → 直接 connect；异常 → 友好错误 + 配对码兜底。

### 6.6 深链

- 浏览器：hash 深链（`window.location.hash = link`，加载时自动连接，已有）；
- 原生：✅ —— `musepi://` scheme + 冷启动 intent stash（openchamber `deepLinks.ts` 模式），
  用于通知点击跳回对应会话。

## 7. 无障碍

| 项 | 标准 | 现状 |
|---|---|---|
| 触控目标 | ≥44px（≤640px） | 本次补 |
| 对比度 | 文本 ≥ 4.5:1（token 体系已满足，验证 `--fg-muted` on `--bg`） | ✅ |
| 焦点可见 | `--ring` cyan 焦点环（键盘导航） | ✅ |
| 语义 | `role="alert/status/alertdialog/menu"`、`aria-label`、`aria-hidden + inert` 折叠区 | ✅ |
| 动效 | `prefers-reduced-motion` 全量覆盖（sh-fade-in / drawer / reveal） | ✅ |
| 触屏无 hover | 操作行常显（§5.4） | 本次补 |
| IME | composition guard 防误提交 | ✅ |
| 缩放 | 输入 16px 防 iOS 聚焦缩放；`maximum-scale=1` 禁双击缩放（移动壳） | ✅ |
| 读屏 | 消息文本即内容（无 canvas 化 transcript） | ✅ |

## 8. 性能预算

| 指标 | 预算 | 实现手段 |
|---|---|---|
| 冷启动首帧（原生） | < 1.2s（中端 Android） | 无外部资源依赖；bundled 静态资源；阻塞脚本只做主题预置 |
| 首屏交互（connect） | < 1s | 无网络依赖，纯本地状态 |
| 连接后 transcript 首帧 | < 300ms（快照 10k 消息） | 快照分块 + 进度超时（30s）；行渲染 memo |
| 流式帧 | ≤ 16ms 批量（BATCH_WINDOW_MS） | guest 协议帧合并；message 平面隔离（TranscriptPane 独占订阅） |
| 后台（通知） | 零轮询 | 事件驱动；`document.hidden` 前台抑制 |
| 包体 | 原生插件全部懒加载（mlkit / notifications / status-bar / keyboard） | `import()` 动态导入，桌面 bundle 不含 |
| 内存 | transcript 封顶（MAX_NOTICES=50 等） | 已有 caps |

## 9. 验收标准（回归清单）

**构建**
- [ ] `bun run check:types`（desktop-web）零错误；`bun run build` 产出 `mobile.html` 入口
- [ ] `bunx cap sync` 后 Android 工程 `assembleDebug` 通过；APK 内 `index.html` = mobile 入口

**连接**
- [ ] 手机浏览器打开 `mobile.html`：配对码路径可用；QR 按钮不显示（非原生壳）
- [ ] 原生壳：QR 扫描 → 连接成功（振动 12ms）；配对码 6s 超时错误；粘贴链接提交
- [ ] 最近连接：连接一次 → 列表出现；删除即消失；重启应用仍在（secure-store）

**会话**
- [ ] transcript 流式渲染；中文输入法 Enter 确认合成、不误发消息
- [ ] 停止按钮中断 turn；queued 计数显示
- [ ] 面板往返、rail 展开/收起、agent 详情 drawer 关闭路径全部可达（含系统返回键）
- [ ] 断网 → reconnecting 横幅 → 恢复网络 → live（自动重连）；ended → Rejoin 生效

**原生**
- [ ] Android 13+ 首次连接弹通知权限；拒绝后无崩溃、无重复请求；授权后后台消息触发本地通知
- [ ] 系统返回键：drawer → rail → 面板 → workspace 逐层关闭，最后退出
- [ ] 键盘弹出时 composer 上移不遮挡；输入 16px 无聚焦缩放；状态栏图标随主题变色
- [ ] 窄屏 header 无溢出（390px 基准）；44px 触控目标热区

## 10. 路线图

### P0（现状基线，本规范固化）
connect / workspace / session / 面板 / rail / drawer / toasts / banners 全链路 + 键盘/状态栏/
安全区/QR/本地通知 + 断线状态机。

### P1（本次打磨）
通知权限请求（§6.4）、系统返回键分层导航（§6.3）、44px 触控目标 + 窄屏 header 收敛（§4.3）、
触觉反馈（§5.2）、hover-only 操作触屏常显（§5.4）、README 补设计文档入口。

### P2（2026-08-25 全部落地）
- 底部 sheet 会话切换 ✅（`SessionsSheet`，iOS 26 / 鸿蒙 6.1 悬浮毛玻璃圆角卡片，
  header 标题触发，见 §11.1）；
- `musepi://` 原生深链 + 通知点击跳会话 ✅（intent filter + `appUrlOpen`/`getLaunchUrl`
  双通道 + 冷启动 stash；通知 `extra.link` 经 `localNotificationActionPerformed` 路由，见 §11.3）；
- 应用图标角标（未读会话数）✅（`@capawesome/capacitor-badge` ShortcutBadger，小米/华为/OPPO
  启动器支持；前台恢复自动清零；`Capacitor.Plugins.Badge` 直取，规避 bundle 动态 import 解析）；
- 平板/折叠屏布局 ✅（≥768px rail 常驻为左侧栏，transcript 全宽，见 §11.4）；
- PWA 清单 ✅（`mobile.webmanifest` + mobile.html manifest link，可安装 standalone）。

### P3（后续）
- 工作区抽屉多 tab（Changes/Files/…，右缘滑动 + header 入口双通道）；
- 边缘滑动返回手势 + 下拉刷新（§5.1）；
- PWA service worker（离线缓存 connect 壳 + 前台推送抑制）。
## 11. 模拟器验证记录（2026-08-24，API 35 / Pixel 6 AVD / WHPX）

全链路真机验证（独立 collab host 桩 + local-relay，E2E 密封帧）通过项：connect 屏三种方式、
会话视图（header/transcript/composer）、≤520px header 折叠（`:has()` 生效）、44×44 触控目标、
返回键分层（面板/rail 逐层关闭，无层退出）、通知权限请求（首次连接自动触发）、后台推送 →
本地通知呈现。

模拟器调试发现并修复的真机专属 bug（桌面浏览器无法暴露）：

1. **`window.Capacitor.plugins`（小写）在真机 WebView 上恒为 undefined** —— 真实注册表是
   `window.Capacitor.Plugins`（大写），且只含 JS 模块已 import 的插件。`setupAndroidBackHandler`
   因此从未注册（返回键直接退应用）。修复：改 `await import("@capacitor/app")`（与 StatusBar/
   LocalNotifications 同模式），desktop-web 与 mobile 各补 `@capacitor/app` 依赖。
2. **同类隐患**：`setupCapacitorKeyboardInset` 原用 `window.Capacitor?.plugins?.Keyboard`，真机上
   键盘事件从不触发，`--mp-keyboard-inset` 只靠 visualViewport 兜底（精度差）。已改模块 import
   （`@capacitor/keyboard`）。

验证遗留（未修，记录在案）：

- `SecureStorage.then() is not implemented on android` —— `@aparajita/capacitor-secure-storage`
  的调用形状与插件 API 不匹配，Android 上 secure 存储静默失败；localStorage 镜像兜底可用。
- 通知调度降级 inexact alarm（无 SCHEDULE_EXACT_ALARM）——对"会话更新"通知无影响。
- 触觉反馈 `navigator.vibrate` 在模拟器无震动硬件，真机可测。

验证工具（保留，供回归）：

- `scripts/collab-host-stub.ts`（desktop-web）—— 固定 key 的 collab host 桩（E2E 密封），8s 后
  推送后台通知触发消息；配合 `scripts/local-relay.ts` 使用。
- 驱动方式：`adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>` → CDP 直接操作
  WebView DOM（uiautomator 无法读 WebView 内容）。

### 11.1 P2 底部 sheet（SessionsSheet，2026-08-24）

iOS 26 / 鸿蒙 6.1 悬浮毛玻璃圆角卡片。多会话工作区（本次 stub 扩展为 2 个 session）时，header 标题
变为触发按钮；点开浮动底部卡片：`blur(24px) saturate(150%)`（`--blur-3xl`）毛玻璃 + 24px 大圆角 +
顶部高光细边 + grabber 指示条 + 弹性上滑入场。实机验证通过：列表（2 items，状态 dot + cwd +
相对时间 + 消息数）、当前会话 accent 高亮 + check、点选聚焦（"Joining session…"）、点遮罩关闭、
拖拽向下 >120px 关闭。`prefers-reduced-motion` 跳过动效。

真机调试备忘：Capacitor Android 的 `androidScheme: "https"` 下，WebView 会按 mixed-content 拦截
`ws://`（LAN 明文）——但本项目实际验证 ws 可达，说明 `allowMixedContent: true` 生效；真正拦住的
是跨域 fetch（CORS，非 ws）。ws 握手在本机经 `10.0.2.2` 可达，**前提是 relay 绑定 IPv4**
（`Bun.serve` 默认可能在 Windows 绑 IPv6-only `[::]`，模拟器 IPv4 不可达——local-relay.ts 加
`hostname: "0.0.0.0"` 修复）。另修 `ROOM_PATH_RE`：collab 链接路径是 `/r/<roomId>.<key>`，原正则
`[A-Za-z0-9_-]{10,64}` 不含 `.` 导致 room key 含点时 upgrade 404/1006——扩为
`(?:[.][A-Za-z0-9_-]+)?$`，`match[1]` 仍为 roomId（E2E 密钥不参与 relay 路由）。
### 11.2 SecureStorage 修复（2026-08-25）

`@aparajita/capacitor-secure-storage` v8 导出的 `SecureStorage` 是 Capacitor 插件 Proxy：其 get
trap 拦截**所有**属性访问（含 `then`）并路由到桥接层。原 `secure-store.ts` 的 `nativeStore()` 直接
返回该 Proxy，调用方 `await nativeStore()` → `Promise.resolve(proxy)` 读取 `proxy.then` →
`createPluginMethodWrapper("then")` → 抛 `"SecureStorage.then() is not implemented on android"`
（每次启动 logcat 报错，secure 层静默回退 localStorage 镜像）。修复：`nativeStore()` 把三个
`internal*` 方法绑定进普通对象返回，调用方永远不会 await 到 Proxy 本体。真机验证：Keystore
写/读/重启持久化/删除全链路通过（`internalSetItem` → `internalGetItem` 返回原值，force-stop 后仍在）。

### 11.3 深链 + 通知点击跳转 + 角标（2026-08-25）

- `musepi://connect?link=<url-encoded collab link>` intent filter（AndroidManifest）→
  `@capacitor/app` `appUrlOpen`（热启动）+ `getLaunchUrl`（冷启动，启动画面延迟 React 挂载时事件
  已先到）→ 模块级 stash + `DEEP_LINK_EVENT`。冷启动实测：`am start -a VIEW -d musepi://...`
  直接进 Session/工作区（跳过 connect 屏），stub 收到 guest hello。
- 通知 `extra.link` 经 `localNotificationActionPerformed` → 同一 DEEP_LINK_EVENT 路由（冷启动
  通知点击跳回对应会话）。
- 角标：`@capawesome/capacitor-badge`（ShortcutBadger），后台通知时 `incrementBadge()`，
  `visibilitychange` 前台恢复 `clearBadge()`。**注意**：`await import("@capawesome/...")` 在 bundle
  运行时解析失败（bundler 未建 chunk 映射），必须走 `window.Capacitor.Plugins.Badge`（大写 Plugins，
  与既有 `setupAndroidBackHandler` 教训一致）。Pixel launcher 不支持 badge（ShortcutBadger
  `supported=false`），API 本身验证通过（set 3 → get 3 → clear 0）；小米/华为/OPPO 启动器可用。

### 11.4 平板布局 + PWA（2026-08-25）

- ≥768px：agents rail 从全屏覆盖层变为常驻左侧列（`position: static; width: 300px`，backdrop 隐藏），
  transcript 保持全宽 —— 两栏工作布局（内容 + rail），面板/工作区仍全屏（内容密集场景）。
- PWA：`public/mobile.webmanifest`（MusePi 品牌，standalone，4 尺寸图标）+ mobile.html
  `<link rel="manifest">`——安卓 Chrome 可"添加到主屏幕"。
### 11.5 引导界面重做 + 真机专属 bug 修复（2026-08-25）

用户真机反馈：扫码不可用、收起态按钮不自适应宽度、语言切换不即时、"像打开网页"。

根因与修复（模拟器 API 35 CDP 实测验收）：

1. **扫码不可用 = 缺 CAMERA 权限**：AndroidManifest 此前只有 INTERNET。补
   `<uses-permission android:name="android.permission.CAMERA" />` + scanQr 内先
   `BarcodeScanner.requestPermissions()`（拒绝则显示友好错误）。实测点击扫码 →
   系统 GrantPermissionsActivity 弹出。注意 mlkit 依赖 Google Play Services，无 GMS
   的国产 ROM（华为等）后续需加 JS 解码兜底。
2. **收起态按钮半宽竖排 = 容器布局 bug**：accordion 容器 div 复用 `.sh-connect-method`
   （flex ROW），收起时 height:0 的 collapse 体仍占 flex 位，把头部按钮挤成半宽；且容器
   与按钮双层卡面。修复：`.sh-connect-card div.sh-connect-method` 容器仅布局
   （column、透明、padding 0），头部按钮（收起）或整个容器（展开）单面承载卡面。
3. **语言切换不即时 = ConnectScreen 未订阅 locale**：t() 非响应式读取 store，切语言只
   重渲染 LanguageToggle 自身。补 `useLocale()`。实测点 toggle 后副标题
   "Connect to a computer…" 立即变 "连接同一网络的电脑"，localStorage 写入 zh-CN。
4. **质感重做（桌面语言 + 原生材质）**：背景加第三层 accent 洗光 + MusePi dot-matrix
   纹理（`radial-gradient` 22px 网格）；卡片 blur 18→28px saturate 160%、圆角 14→20px、
   24px→400px 宽、双层阴影 + 顶部高光；brand mark 16→22px；method 磁贴图标 34→40px
   squircle、圆角 10→14px、`:active` scale(0.98) 按压反馈、hover 抬升阴影、scan 磁贴
   渐变 accent 面 + 辉光；新增 accordion chevron（"›" 收起→旋转 90° 展开）；pair-row
   自适应宽度（96px 码位 + host 弹性吸收 + 按钮整行换行）；提交按钮 44px 主按钮 +
   accent 辉光；错误提示升级为玻璃警示条。实测几何：闭合态三磁贴全宽 339px、图标左置
   水平布局；展开态表单 319px 全宽在 ring 内。
### 11.6 沉浸式修复 + 减法扫码重写（2026-08-25）

用户反馈卓易通无沉浸、扫码不可用、引导界面动效不自然。

**沉浸式（三阶段修复，CDP 逐级验证）**：
1. 主题声明透明栏 + `setDecorFitsSystemWindows(false)`（onCreate + onWindowFocusChanged 双时机）→ WebView 全屏 915dp ✅
2. 消费全部窗口 insets（`WindowInsetsCompat.CONSUMED`）→ 内容真正铺到 bar 下 ✅
3. Insets 原生插件（`InsetsPlugin`，`Capacitor.nativePromise` 桥调用）读取真实 `statusBars.top(49dp)` / `navigationBars.bottom(24dp)` → 注入 `--safe-top`/`--safe-bottom` CSS 变量 → 连接卡 padding-top 正确避开状态栏时钟 ✅

**扫码（弃 mlkit，改用纯 JS + getUserMedia）**：
- 根因：`@capacitor-mlkit/barcode-scanning` 需 Google Play Services Barcode 模块，卓易通/无 GMS 设备无法使用
- 替换为 `jsQR`（纯 JS QR 解码器）+ `navigator.mediaDevices.getUserMedia`（Capacitor WebView 已验证可用，480p back camera）
- 全屏动画取景框：4 个 glowing 角标（accent 色 + 辉光）、扫描线匀速上下 sweep（2.4s cycle）、暗色遮罩外场、手电筒切换、成功率振动反馈
- 无 GMS 依赖，任何设备均可工作

**引导动效**：方法磁贴按压反馈（`:active` scale(0.98)）、hover 抬升阴影、chevron 展开旋转（200ms spring）、入场错峰动画（70ms stagger）、折叠体高度过渡（240ms spring）

### 11.7 HarmonyOS WebView 壳（2026-08-25）

用户诉"卓易通没沉浸"，本质是抵触兼容层。方案：`packages/harmony/` 用 ArkTS `Web` 组件加载现有 desktop-web 移动 bundle，一等地壳（非兼容层），沉浸/相机/权限全部原生。

**桥接设计**：`capacitor.ts` 扩为单一桥路由器——`isMobileShell()` 同时识别 Capacitor 与 `window.harmonyNative`（ArkTS javaScriptProxy）。同一 dist 双壳通用，`mobile.tsx` 零改动（仅 insets 提取为共享 `getSystemBarInsets()`）。JavaProxy 方法同步返回 JSON 字符串。

**HarmonyNative 桥表面**：`getSystemBars()`（avoid area → vp，vp==CSS px）、`getBadge`/`setBadgeCount`/`clearBadge`（notificationManager）、`consumeDeepLink()`（冷启动深链）。JS 侧回调：`window.__harmonyKeyboard(h)`（onKeyboardHeightChange 推送）、`window.__harmonyDeepLink(uri)`（onNewWant 推送）。

**深链**：`module.json5` ability skills → uris 注册 `musepi://connect?link=`；EntryAbility onCreate 存冷启动 URI、onNewWant 推热启动。

**构建**：desktop-web `bun run build`（产出 index.html=桌面 / mobile.html=移动壳）→ `node scripts/copy-web-assets.js` 把 **mobile.html** 重命名为 rawfile/index.html（桌面入口弃用）→ DevEco Studio 打开 packages/harmony 运行。rawfile gitignored。

**取舍**：凭证回退 localStorage（未接 `@ohos.security.asset`，P3）；通知/语音/原生图标均为 P3。若用户要真原生体验，connect+会话列表 ArkTS 重写是后续独立工程。

### 11.8 GUI 视觉吸收（2026-08-25，持续）

按"协议兼容 + 零依赖"原则把 gui 客户端组件吸收进移动壳（desktop-web 的 shell 组件），每批模拟器 CDP 实测。

**批 1（716b5bf3e4）— 品牌动效**：
- DotMatrixMark（canvas 点阵品牌底纹）作为 connect 卡背景（masked 渐隐）；注意 canvas 必须 CSS 容器约束，否则 ResizeObserver↔bitmap attribute 反馈环撑爆像素
- ShinyText 副标题扫光、BlurText 品牌逐字入场、SpotlightCard 聚光卡（触控适配 pointer 事件）
- collab-host-stub 补 workspace-select 分支 + sessionId 字段名，支撑全链路 E2E 验证

**批 2（e5f71bbb60）— 空态推荐 chips**：
- SuggestionChips（openchamber DraftPresetChips parity）：8 默认 + 7 展开，i18n keyed（chip */suggest * 双语言已存在）
- 集成点 = Composer 空态（live && !readOnly && entries==0）：点击填入草稿 + 聚焦（gui parity，用户改后发送），+ 展开 staggered blur-in / 收起 fade-out
- 协议零改动——chips 只是 draft 填充，发送仍走 prompt 帧
- 协议约束检查：guest 无 set-model/附件帧 → AttachMenu / ThinkingSelector / SlashRow(daemon commands.list) 不吸收，避免半成品

**E2E 验证**：连接 → 选空会话（Board cleanup）→ 9 chips（8+more）→ 点击 "探索代码库" 填入 "带我了解这个代码库的结构和关键模块" + 聚焦 + 发送可用 → 展开 16 chips 全部渲染。

### 11.9 空态问候 + 真实 daemon 验证 + Harmony 壳落地（2026-08-25）

**批 3（a160d75025）— 空态问候层**：
- WelcomeHint（gui WelcomeComposer 吸收）：七时段问候（凌晨/清晨/早上/中午/下午/晚上/深夜）+ 旋转 tips（6s 轮换、blur-in 刷新）；桌面专属 tip（{mod}N）从移动列表剔除
- Transcript 新增可选 `emptySlot` prop 替代裸 "no activity yet"；TranscriptPane 仅在 `isMobileShell()` 时注入（桌面 collab web 不受影响）
- 实测：空会话显示 "夜深了，注意休息"（03:xx 时段正确）+ tip + 9 chips 共存；8s 推送到达后空态正确让位于消息

**真实 daemon E2E（协议桩之上的最终验证）**：
- 隔离 daemon（MUSEPI_DAEMON_DIR + PI_CONFIG_DIR 独立目录，`musepi serve --port 8310`）→ RPC `session.create` + `collab.start`（workspace 模式）→ LAN relay 7654
- 模拟器粘贴真实链接 → workspace 目录（真实 journal：项目分组/cwd/会话数）→ 会话卡 → composer live → 真实 prompt → **真实 LLM 回复**（deepseek 思考块+回答）全链路 ✅
- 协议栈端到端（E2E 加密、workspace-select、快照水合、prompt/steer、entry 推送）在真实 host 语义下确认

**HarmonyOS 壳（2352d2ea0c）**：
- `packages/harmony/` DevEco 工程（API 12 / 5.0.0）：EntryAbility（musepi:// 深链冷/暖启动）+ Index.ets（Web 组件 + `harmonyNative` javaScriptProxy）
- 桥面与 JS 侧 capacitor.ts 的类型/路由完全对齐（getSystemBars via getWindowAvoidArea px→vp、badge、consumeDeepLink、__harmonyKeyboard/__harmonyDeepLink 推送）
- `@StorageLink + @Watch` 标准模式做暖启动深链推送；module.json5 注册 INTERNET/CAMERA + musepi:// skill（phone+tablet）
- `scripts/copy-web-assets.js`：desktop-web dist → rawfile（mobile.html 提升为 index.html）；rawfile gitignored
- 构建路径：desktop-web build → copy-web-assets → DevEco Studio 打开签名运行（本机无 DevEco，ArkTS 未编译验证）

### 11.10 旋转/断点过渡动效（2026-08-25，模拟器双向实测）

横竖屏切换跨越宽度断点（768/640/520）时布局原为硬切。新增**断点几何过渡**：

- **机制**：纯 CSS `transition`（320ms `cubic-bezier(0.22,1,0.36,1)` 项目 spring 曲线）作用于
  `padding / gap / max-width / min-width / min-height / font-size`，注册在基础规则上，
  断点跨越时属性变化自动平滑过渡——**零 JS**（旋转 → 媒体查询重评估 → 属性变化 → 过渡）。
  覆盖：`.sh-header(-left/-right)`、`.sh-composer(-inner/-input)`、`.sh-btn(-icon)`、
  `.sh-connect-card/method/submit`、`.sh-workspace`、`.sh-ws-sidebar/card`、`.sh-transcript/content`。
- **保留特例**：`.sh-composer` 的键盘 inset 走原 220ms spring（padding-bottom 不被通用曲线覆盖）；
  `.sh-ws-card` 合并 hover 交互过渡（border-color/transform/box-shadow 120ms + 几何 320ms）。
- **不可过渡、保持硬切**：`flex-direction`（workspace 侧栏 column↔row）、`display:none`
  （chips/avatars/gauge 等隐藏控件）——布局正确性优先，硬切表现为即时重排而非闪烁。
- **prefers-reduced-motion**：两个 reduced-motion 块之一清零全部旋转过渡（transition: none）。
- **实测证据**（模拟器 CDP，connect 卡 max-width）：
  - 竖→横：`100% → calc(0.0246882% + 479.882px)`（106ms 中间态）→ 480px 封顶 ✅
  - 横→竖：`calc(0% + 480px)`（76ms 中间态）→ `100%` ✅
  - 双向平滑 morph，非硬切；transitionProperty 含 padding/gap/max-width/min-width/min-height/font-size ✅
- **鸿蒙侧**：ArkWeb 同 Chromium 内核共享同一套 CSS——壳无需额外代码；ArkUI 窗口旋转动画为系统级，
  Web 内容过渡由 CSS 承担（双层动效，无冲突）。

### 11.11 PWA 离线壳 + 连接文案人性化（2026-08-25，Electron 验证）

**PWA service worker（§10 P3 项完成）**：
- `public/sw.js`：静态资源 cache-first（hash 文件名不可变）、HTML 壳 network-first + 离线回退缓存；
  预缓存 index/mobile.html、双 manifest、favicons；版本化缓存键 `musepi-collab-v1`、
  skipWaiting + clients.claim、activate 清理旧缓存
- index.html + mobile.html 内联注册（bun 构建保留内联脚本）；build 脚本显式
  `cp public/sw.js dist/sw.js`（bun build 把 public/ 复制成子目录而非根级——坑）
- **Electron 验证**：缓存 `musepi-collab-v1` 创建 ✓；同一进程内杀服务器后 reload
  从缓存加载 shell + React 完整渲染（connect 卡）✓；跨进程持久化在 Electron 默认
  session 不生效（其 session 行为，真实浏览器/WebView 正常持久化）
- 离线提示条 `.sh-connect-offline`（navigator.onLine + online/offline 事件），
  明确"已显示缓存内容，连接需要网络"

**连接文案人性化（回答"二维码能不能直接扫码用"式困惑）**：
- 扫码磁贴："扫桌面分享面板的二维码 — 本 App 或任意手机浏览器都能用"
  （原"用相机扫描桌面分享的二维码"未说明浏览器也可用）
- 配对码磁贴："6 位配对码 + 电脑地址 — 都在桌面分享面板上"（原未说明地址来源）
- 配对表单底部 hint："看不到电脑地址？改用公网隧道分享，直接扫码链接"
  （隧道模式免地址——等价于"只输授权码"的体验）

### 11.12 实例切换器 + agent 自主共享（2026-08-25，Electron E2E 验证）

**实例切换器（openchamber DesktopHostSwitcher parity）**：顶栏实例按钮从"单本地 daemon 信息菜单"
升级为多 host 切换器。
- 数据：`RemoteHost { id, label, url, token? }`，localStorage `musepi-gui-hosts` 持久化；
  `buildWsUrl` 把 token 拼进 WS URL（`?token=`，浏览器 WebSocket 不能设 header）
- 菜单：本地行 + 已保存远程行（状态点/current 徽标/hover 删除）+ 添加表单
  （label/url/token）；点击行切换 → `musepi-gui-url` 持久化 + 重 boot
- 安全边界：Electron 版本门（daemon 版本不匹配自动重启）与本地 probe/spawn 回退
  只对 loopback 且不带 token 的 URL 生效——带 token 的远程实例绝不自动重启

**daemon 远程访问**：`musepi serve --remote-token <token>`（≥16 字符）→ WS 绑 0.0.0.0 +
全连接 bearer 鉴权（Authorization: Bearer 或 `?token=`，常量时间比较，401 拒绝）；
未设置 → 保持 loopback-only 零认证（原行为）。E2E：无 token 401 / 带 token ping 通 /
远程 daemon 完整渲染 GUI 会话树；切换器"添加→持久化→切换→连接成功"全链验证。

**agent collab 工具（用户"让 musepi 自己开配对"需求落地）**：
- `collab` 工具：action=start/stop/status；mode=lan（默认）/tunnel/workspace
- 动态 approval：LAN → write tier（write 模式自动过）；tunnel → exec + override
  （强制确认："公网隧道 — 任何人拿到链接都能加入"）；stop → write；status → read
- daemon 注入（session.create/resume 的 agent 会话都有）：tools/context.ts 声明合并
  AgentToolContext.collab；DaemonServer 经 setCollabToolProvider 接线；handle 直接
  复用 collab.* RPC（dummy conn——collab case 不写连接）
- 无 daemon 环境（TUI/CLI）：工具报"Remote sharing is unavailable"，提示用 /collab
- 返回：LAN → link + 6 位配对码；tunnel → 公网链接 + ⚠️ 停止共享提示
- `collab.start` RPC 新增 mode "tunnel"（cloudflared/ngrok 公网 URL）；GUI CollabDialog
  加 Public tunnel 分段选项 + 公网警告 hint

**可吸收项待办（dsh-mobile-remote 30+ API 范本）**：移动端完整会话管理/审批桥/通知悬浮球
（见 §11.9 吸收清单），远程 daemon 连接打通后 RPC `session.*`/`jobs.*`/`subagent.*` 已可用。

## 12. 交互缺陷修复（2026-08-31）

2026-08-31 对移动端交互设计进行专项核对，发现并修复 6 项实现与设计文档不一致的缺陷。全部在 `packages/desktop-web/src/` 内完成，不动 wire 协议/daemon/原生壳；每项附带契约测试。

### 12.1 A1 — 返回键层栈完整化（back-stack）

**缺陷**：`musepi:back` 是 window CustomEvent，`Session` 组件集中监听但只处理 drawer/rail/panel/workspace 四层，SessionsSheet/QrScanner/ServerSwitcher 三个模态覆盖层未进层栈，导致"一次 back 关两层"或"扫码时按返回直接退应用"。

**修复**：新建 `lib/back-stack.ts` 模块级层栈注册表。每个模态组件用 `useBackLayer(priority, active, handler)` hook 注册自己的关闭 handler。`setupAndroidBackHandler` 改调 `dispatchBack()` 从最高优先级到最低逐个尝试，第一个返回 true 的消费并停止，确保一次 back 关一层。优先级（从最上层到最下层）：AgentDrawer(100) > QrScanner(95) > SessionsSheet(90) > ServerSwitcher(85) > PanelMenu(84) > AgentsRail(80) > Panel(60) > Workspace(40)。删除原 `BACK_EVENT` CustomEvent 机制。

**契约测试**：`test/back-stack.test.ts`（6 个用例：优先级排序、消费后注销、同优先顺序、跨层消费、整体重置）。

### 12.2 A2 — SessionsSheet 常挂载退场动画

**缺陷**：`if (!open) return null` 条件挂载，关闭时瞬间消失无退场动画。

**修复**：改用 visible/closing 状态机。`stage: "hidden" | "open" | "closing"`：open → "open"（入场动画）；onClose → "closing"（退场动画，280ms fallback 定时器兜底 onAnimationEnd）；关闭后 → "hidden"（卸载 DOM）。新增 `.ss-backdrop.ss-closing` 退场 CSS（`ss-backdrop-out` 180ms + `ss-card-out` 280ms，forwards 保持终点态）。`prefers-reduced-motion` 跳过动画。

**契约测试**：`test/sessions-sheet.test.tsx`（3 个用例：open 渲染、closing 未出现、hidden 卸载）。

### 12.3 A3 — ask checkbox 多选语义（Design A）

**缺陷**：移动端 Composer 渲染 checkbox 勾选框（暗示可多选），但点击任一选项立即 `sendUiResponse` 提交并关闭 ask UI，wire 单值承载无法完成多选。实际 host 端多选是逐项 toggle 循环（每次 `ui-response` 切换一个选项，host 以新 reqId 重发带更新 `checkedIndices` 的请求，直到点 "Nex→"），不要求 wire 变更。

**修复**：`client.ts` `sendUiResponse` 中 checkbox 模式不立即 `#showNextUiRequest()`，而是保持 `#uiRequestPending = true`（ask UI 常驻不闪）。host 重发同 title 请求时替换并清除 pending。`ui-request-end` 彻底清空。`GuestSnapshot` 新增 `uiRequestPending` 字段。Composer 在 pending 时禁用选项按钮防重复点击，渲染多选提示文案。

**契约测试**：`test/ask-multi.test.tsx`（5 个用例：pending 保持、host 重发替换、end 清空、hint 渲染+锁定、不同 title 入队）。

### 12.4 A4 — 连接记录时机

**缺陷**：`ConnectScreen.connect()` 在 `onConnect` 前调用 `rememberConnection`，失败连接（bad link、pair 超时、QR 坏链接）仍进入最近列表，用户需手动删除。

**修复**：`GuestClient` 新增 `onWelcome` 回调，仅在首个 welcome 帧（首次连接成功）触发。`App.connect` 在 `onWelcome` 里调用 `rememberConnection(link, name)`。ConnectScreen 移除提前 `rememberConnection`。`#welcomed` 已 true 时重连 welcome 不重复触发。

**契约测试**：`test/welcome-hook.test.ts`（4 个用例：首次 welcome 触发、重连不触发、无 welcome 不触发、坏链接构造抛异常）。

### 12.5 A5 — hash 写入时机

**缺陷**：`App.connect` 同步设置 `window.location.hash = link`，失败连接也污染 URL，刷新自动重连坏链接且 E2E key 留在历史中。

**修复**：`window.location.hash = link` 移到 `onWelcome` 回调中（与 A4 同处）。失败连接不写 hash，`leave()` 原有 `history.replaceState` 清 hash 逻辑未变。

**契约测试**：同 `test/welcome-hook.test.ts`（A4 测试覆盖）。

### 12.6 B1 — ≤520px 窄屏 header 溢出

**缺陷**：≤520px 时 header-right 有 4 个面板按钮（44px×4）+ theme/server/rail/leave 共约 382px + 左侧标题约 124px = 506px > 390px 基准屏，必然水平溢出。

**修复**：HeaderBar 在 ≤520px 时把 4 个面板按钮折叠成 1 个"面板"菜单按钮（LayoutDashboard 图标），点击弹出 popover 列出 4 项（复用 ServerSwitcher popover 视觉）。Back 层注册优先级 84。CSS 补 `.sh-header-nav` 定位上下文、`.sh-panel-menu-item` 按钮样式。

**契约测试**：`test/header-bar-narrow.test.tsx`（2 个用例：宽屏 4 按钮、窄屏折叠菜单 + 原按钮消失）。

### 关键文件变更清单

| 文件 | 变更 |
|---|---|
| `src/lib/back-stack.ts` | 新建：层栈注册表 + useBackLayer hook |
| `src/lib/capacitor.ts` | setupAndroidBackHandler 改调 dispatchBack，删除 BACK_EVENT |
| `src/lib/client.ts` | add onWelcome, uiRequestPending, checkbox pending 逻辑 |
| `src/lib/host-client.ts` | snapshot 补 uiRequestPending |
| `src/app.tsx` | Session 四层迁移到 useBackLayer；connect 改 onWelcome 写 hash+记录 |
| `src/components/shell/Composer.tsx` | checkbox pending 禁用 + helpText |
| `src/components/shell/SessionsSheet.tsx` | 常挂载 + visible/closing 状态机 + back 层注册 |
| `src/components/shell/ServerSwitcher.tsx` | useBackLayer 注册 |
| `src/components/shell/ConnectScreen.tsx` | useBackLayer scanner + 移除提前 rememberConnection |
| `src/components/shell/HeaderBar.tsx` | ≤520px 面板折叠 + useBackLayer panelMenu |
| `src/components/shell/shell.css` | 退场动画 keyframes + reduced-motion 豁免 + panel-menu 样式 |
| `test/back-stack.test.ts` | 新建 |
| `test/sessions-sheet.test.tsx` | 新建 |
| `test/ask-multi.test.tsx` | 新建 |
| `test/welcome-hook.test.ts` | 新建 |
| `test/header-bar-narrow.test.tsx` | 新建 |
