Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions docs/plans/feature-v0.6.0-webui-sidebar-responsive/PRD-CHANGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# PRD 变更文档:v0.6.0 — Web UI 侧栏折叠与响应式 / 触屏适配

> **状态**:草案(待评审;本文档不构成实施指令,评审通过后按 §4 分期开工)
> **日期**:2026-10-09
> **目标版本**:v0.6.0(当前 `develop` = v0.5.2 release 合入 + 未发布变更)
> **来源 issue**:#99(`ui(sidebar): 工作树侧栏支持折叠 / 展开(桌面端)`)、#100(`ui(responsive): Web UI 响应式布局 + 触屏交互(iOS / iPadOS)`)
> **拟定分支名**:`feature/v0.6.0-webui-sidebar-responsive`
> **本文档位置**:`docs/plans/feature-v0.6.0-webui-sidebar-responsive/PRD-CHANGE.md`
> **主文档边界**:`docs/PRD.md`、`docs/API.md`、`docs/ARCHITECTURE.md` **本次不改**;本文档登记拟变更点,落地时由各实施 PR 按 §9 清单同步。

---

## 1. 背景与问题陈述

issue #99 与 #100 是同一产品问题的两个切面:**myworktree 的 Web UI 完全按「桌面鼠标 + 大屏」设计,既不能在桌面端临时把终端拉满,也不能在 iPad / iPhone 上真正使用**。

### 1.1 #99:桌面端侧栏折叠(优先级 低-中)

主界面为单文件 vanilla JS + CSS(`internal/ui/static/index.html`,HTML/CSS/JS 全部内联),布局是一层水平 flex。侧栏同时承载 worktree 列表 + git staged / unstaged 两个面板,240px 常驻宽度在全屏终端场景(agent 正在跑 TUI)下挤压终端可视列数。用户需要的是**临时折叠**,而非永久改布局。

### 1.2 #100:响应式布局 + 触屏交互(优先级 中)

零宽度断点、viewport 声明不完整、固定 px 布局在窄屏塌陷(iPhone 竖屏扣掉 240px 侧栏后 `#main` 只剩 ~150pt)、零 Pointer/Touch 事件、触控目标远小于 44pt、hover-only 样式在触屏上粘滞、iPadOS Split View 320pt~1366pt 全宽度域出现、iOS 软键盘遮挡。**若产品定位含「在 iPad 上真正能用」,第 1 层(视口与高度)成本极低、收益立竿见影。**

### 1.3 两个 issue 的合并点

#100 明确:窄屏 drawer **复用 #99 的展开/收起语义**,但形态是 drawer 而非宽度归零;并建议 #99 先落地宽屏行为、#100 只补窄屏。因此二者**不是两个独立需求,而是一个「侧栏可见性状态机」的两种渲染形态**,必须合并设计、分期实施。

### 1.4 代码现状(已对当前 `develop` 核实)

| 事实 | 锚点(当前 develop) |
|---|---|
| 单文件 vanilla JS + CSS,HTML/CSS/JS 全内联 | `internal/ui/static/index.html`(5562 行) |
| `#app` 一层水平 flex,`100vh` / `100vw` | `index.html:158-162` |
| `#sidebar` 固定宽 `var(--sidebar-width)` = 240px,纯 CSS 常量无 JS 改写 | `index.html:9`、`165-171` |
| `#main` `flex:1` + `min-width:0` | `index.html:539-545` |
| `#header` / `#tabs-container`(已有 `overflow-x:auto`、隐藏滚动条) | `index.html:1370`、`548-574` |
| 新建 / 导入按钮 `.sidebar-actions`(chevron 按钮天然宿主) | `index.html:1335-1338` |
| 侧栏垂直分栏拖拽 `#sidebar-resize-handle` → `startSidebarResize()`(mousedown/mousemove/mouseup,仅调 top/bottom 高度,与侧栏总宽无关) | `index.html:1343`、`2355-2392` |
| git staged/unstaged 互锁折叠 `toggleGitSection()` | `index.html:2200`、`1344-1365` |
| 全文件唯一 keydown:tab 重命名输入框 `handleRenameKeydown` | `index.html:2623`、`2565` |
| **localStorage:0 处**;sessionStorage 仅 `SERVER_UPGRADED_FLAG` | `index.html:1742`、`1797`、`2113-2114` |
| **宽度断点:0 个**(全文件仅 2 个 `@media`,均为 `prefers-color-scheme`) | `index.html:79`、`1208` |
| viewport meta 缺 `viewport-fit=cover` / `interactive-widget` | `index.html:5` |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【发现·中】interactive-widget 被列为现状缺陷,但没有任何需求或验收标准承接,需求表不闭环。

本行说 viewport meta「缺 viewport-fit=cover / interactive-widget」,但:

  • R5(§2.1)只要求补 viewport-fit=cover,未提 interactive-widget=resizes-content;
  • R6 用 visualViewport 路线解决软键盘,与该 meta 是替代关系而非互补;
  • §5.2 验收只检查 viewport-fit=cover。

这个缺口源自 issue #100:其「背景」第 2 条点了 interactive-widget,但「方案 L1」并没有列它——本文档照抄了背景、没有对方案做显式决断。结果是 PR2 实施者无法从需求表判断它是否在范围内:补,超出 R5 字面;不补,本行的缺陷陈述永远悬空。建议二选一:(a) R5 显式加上 interactive-widget=resizes-content 并在 §5.2 补一条验收;(b) 本行改为「viewport meta 缺 viewport-fit=cover;interactive-widget 缺口由 R6 的 visualViewport 方案替代,不引入」。

— Sisyphus

| Pointer/Touch 事件、`dvh`、`visualViewport`、`env(safe-area-inset-*)`:均 0 处 | 全文件检索确认 |
| `body, html { overflow: hidden }`(页面本身不滚,软键盘遮挡无法靠滚动救回) | `index.html:144-152` |
| `--term-ctrl-btn-size: 32px`;`.icon-btn { padding: 2px }` | `index.html:23`、`187-194` |

> 注意:issue 中的行号是 2026-10-08 提交时快照,与当前 develop 有少量漂移(如 `SERVER_UPGRADED_FLAG` 1742≠1766、`startSidebarResize` 2355≠2324、唯一 keydown 已从 modal 内 Enter/Escape 变为 tab 重命名输入框)。**实施时以符号锚点为准,不追行号。**

---

## 2. 需求综合(#99 + #100 合并视图)

**统一产品需求:交互式终端在桌面端可临时全宽;Web UI 在 iPad / iPhone 上真正可用。** 共用一个侧栏可见性状态,两种渲染形态,按视口断点分派。

### 2.1 需求条目(合并编号,供验收 / PR 引用)

| # | 需求 | 来源 |
|---|---|---|
| R1 | 桌面折叠态:`#app` 挂 collapsed CSS 类,`#sidebar` 宽度归零 + `overflow: hidden`,`#main` 因 `flex:1` 自动吃满;**JS 不做任何宽度计算** | #99 |
| R2 | 开关按钮:chevron 图标按钮,**放在 `#header` 左侧**(折叠态常驻可见,不给侧栏留宽度) | #99 |
| R3 | 持久化:`localStorage`,命名空间化 key `mw.ui.sidebarCollapsed`;文件首个 localStorage 用例,**默认展开**;旧浏览器 / 隐私模式失败时降级为不持久化 | #99 |
| R4 | 快捷键 `Ctrl/Cmd+B` 切换:首个全局 keydown;不在 input / textarea / contenteditable 内触发;焦点在终端 iframe 内时父页面捕获不到按键(跨 frame 限制,注释说明,不试图绕过) | #99 |
| R5 | 视口与高度:`#app` 高度 `100dvh`(`@supports` 保留 `100vh` fallback)、宽度 `100%` 取代 `100vw`;viewport meta 补 `viewport-fit=cover`;容器用 `env(safe-area-inset-*)` 处理刘海与 Home Indicator | #100 L1 |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可追溯性缺口:interactive-widget 在需求层静默丢失。

§1.4 已记录 viewport meta 同时缺 viewport-fit=cover 与 interactive-widget(issue #100 原文亦两项并列),但 R5 只接了前者,interactive-widget 在 R5–R16 及 §8 非目标中均无去向。若取舍是「interactive-widget 仅 Chromium/Android 支持、iOS Safari 忽略,软键盘由 R6 的 visualViewport 覆盖,本期 iOS/iPadOS 目标不需要它」——这个判断本身是对的,但建议在 §3 显式记录该决策;否则对照 issue 阅读时会认为 R5 静默缩窄了范围。

—— Kimi Code CLI(AI 评审)

| R6 | iOS 软键盘:监听 `visualViewport` 的 `resize` / `scroll`,键盘弹出时收窄终端可视高度,保证输入区可见 | #100 L1 |
| R7 | 断点与形态(`max-width: 768px` 为界,对应 iPad 竖屏 768pt):窄屏侧栏改覆盖式 drawer(`position: absolute` + `transform: translateX`),打开带遮罩、点遮罩关闭;宽屏维持并排;中间宽度(iPad Split View,768–1024)侧栏收窄或限上限(如 `max-width: 200px`),并排仍可用 | #100 L2 |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

断点边界归属歧义 +「iPad 竖屏 768pt」表述过宽。

窄屏定义为 max-width: 768px(含 768),同句中间档又写「768–1024」,768px 同时落在两档;§5.3 第 3 条同样写「中间宽度(768–1024)」。另外「对应 iPad 竖屏 768pt」仅对 9.7"/10.2" iPad 成立(mini 竖屏 744pt、10.9"/11" 为 820/834pt),§3.6 的断点依据也按 768 笼统表述。建议明确 768 归哪一档(如 max-width: 767.98px,或注明 768 归 drawer),并把断点依据改写为具体机型档。

—— Kimi Code CLI(AI 评审)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【发现·中】768 同时落在两个区间,边界归属未定义。

R7 以 max-width: 768px 划窄屏 drawer,同一条又写「中间宽度(iPad Split View,768–1024)」——按字面 768 同时属于窄屏与中间档。若 PR3 实现时把中间档写成 @media (min-width: 768px) and (max-width: 1024px),就会与 max-width: 768px 在恰好 768px 时同时命中,走哪个分支取决于 CSS 书写顺序——这是可以避免的歧义。R14 已写「窄屏(≤768px)」,建议中间档统一改为开区间(769–1024)或在本条明确「768 归窄屏 drawer」,并与 §3.6 的断点表述保持一致。

— Sisyphus

| R8 | 窄屏 `#tabs-container` 补 `scroll-snap-type` 让 tab 吸附(滚动条继续隐藏,复用 568-574 既有实现) | #100 L2 |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【发现·低·锚点精度】「复用 568-574 既有实现」引用范围偏小。

#tabs-container 隐藏滚动条的 ::-webkit-scrollbar* 规则族实际从 568 行延伸到 600 行:568-574 只是第一条规则,574-587 是 scrollbar 主体(track/track-piece),589-590 是 thumb hover/active,594-600 是 scrollbar-button/corner。§1.4 事实表同一行的「548-574」同样在滚动条规则块中间截断。建议统一改为 568-600,避免 PR3 实施者以为只需参考前 7 行。

— Sisyphus

| R9 | 侧栏分栏拖拽从 mouse 事件迁到 **Pointer Events**(`pointerdown`/`pointermove`/`pointerup` + `setPointerCapture`);拖拽时 handle `touch-action: none`,拖拽元素 `user-select: none` / `-webkit-touch-callout: none` | #100 L3 |
| R10 | 触控目标:`.icon-btn` 与 `--term-ctrl-btn-size` 在 `@media (pointer: coarse)` 下放大到 ≥ 44pt,用 `min-width`/`min-height` 而非固定 width/height,避免撑破现有布局 | #100 L3 |
| R11 | hover 样式用 `@media (hover: hover)` 包裹,纯触屏设备无粘滞高亮 | #100 L3 |
| R12 | iframe 重排:折叠 / 展开 / 断点切换后,终端 iframe(xterm + xterm-addon-fit)与 reasonix / opencode / dsh 内嵌 iframe 不出现滚动条、错位、尺寸错误 | #99+#100 |
| R13 | 与 `toggleGitSection()` 的侧栏内折叠**状态互不覆盖**(两个正交状态) | #99 |
| R14 | 窄屏让位:窄屏(≤768px)使用 drawer 形态,不要求桌面折叠态在小屏生效 | #99+#100 |

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

规格缺口:窄屏 drawer 的初始开合状态未规定。

单一 sidebarCollapsed 布尔态(§3.1)+ 默认展开(R3)意味着窄屏首次访问时 drawer 以「展开」形态渲染——打开即盖住 #main,用户须先点遮罩才能看到终端。R14 只说窄屏用 drawer 形态,未规定窄屏初始开合,也未写明窄屏是否与桌面共享同一个持久化 key(桌面收起过的用户到 iPad 上 drawer 默认关闭——行为合理,但应写明这是同一状态机的必然结果而非新状态)。建议补一条验收:窄屏首次加载 drawer 默认状态 + 与桌面持久化状态的共享语义。

—— Kimi Code CLI(AI 评审)

| R15 | 范围边界:只覆盖 myworktree 自己的 Web UI(`internal/ui/static/index.html`);iframe 内页面(vendor xterm、`internal/instance/*` 各内嵌 web UI)不在范围,但本 UI 必须保证 iframe 容器拿到正确的高 / 宽 | #100 |
| R16 | 键盘语义隔离:`Ctrl/Cmd+B` 不会与 TUI 的 Ctrl+B 冲突(iframe 内按键到不了父文档) | #99 |

---

## 3. 关键设计决策

### 3.1 单一状态机,两种渲染形态(合并设计核心)

- 唯一真源:布尔态 `sidebarCollapsed`(R3 持久化)。
- 宽屏渲染:`width: 0` + `overflow: hidden`(R1)。
- 窄屏渲染:drawer off-canvas(`translateX(-100%)`)+ 遮罩(R7)。
- 分派完全由 CSS 断点 + 同一个 class 完成,**JS 只有「读状态 → 挂/摘 class」一条路径**,没有第二处宽度计算。这是 issue #99「不与响应式宽度规则互相干扰」要求的落地方式。

### 3.2 按钮宿主:`#header` 左侧

采纳 #99 推荐方案(非备选的 24px 竖条把手):折叠态常驻可见、不给侧栏留任何宽度;窄屏 drawer 形态下同一个按钮即 drawer 开关,无需第二套控件。备选方案(侧栏保留竖条)拒绝理由:与 drawer 形态重复,且仍占宽度。

### 3.3 持久化:首次引入 localStorage

`mw.ui.sidebarCollapsed`,命名空间化与 issue 给的 key 一致。与既有 `sessionStorage` 的 `SERVER_UPGRADED_FLAG` 语义分工:后者是一次性升级标记(session 级),前者是用户偏好(持久级)。**默认展开**(key 缺失 / 解析失败 / 抛异常均视为展开)。

### 3.4 快捷键基础设施(首个全局 keydown)

- `document` 级 keydown,`metaKey || ctrlKey` + `key === 'b'` 判定。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

实施细节提醒:key === 'b' 的大小写与 repeat。

event.key 在 Caps Lock 或 Ctrl/Cmd+Shift+B 时为 'B',key === 'b' 会静默不触发,建议判定写 key.toLowerCase() === 'b';另可考虑忽略 event.repeat(长按 B 导致反复切换)。属于实施 PR 易踩的坑,文档可先写明。

—— Kimi Code CLI(AI 评审)

- 输入守卫:`event.target` 落在 input / textarea / `[contenteditable]` 内直接 return(含 tab 重命名输入框)。
- iframe 焦点时父页面收不到按键——**写注释说明,不做 postMessage 之类的绕过**(跨 frame 限制是浏览器安全模型,绕过反而引入新攻击面)。

### 3.5 dvh / 宽度基线与折叠逻辑解耦

R5 动的是 `#app` 的**高度与宽度基准**(100dvh、100% 宽),R1 动的是 `#sidebar` 在 flex 行里的**宽度占比**,两者互不依赖,但必须在同一轮验证(折叠 + 键盘弹出同时发生)。

### 3.6 断点选取依据

`768px` = iPad 竖屏宽度;`1024px` = iPad 横屏 / Split View 上边界。中间档收窄侧栏而非直接 drawer,因为 Split View 下半屏仍可容纳收窄侧栏 + 终端。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【发现·中】「Split View 768–1024」与 iPad Split View 的实际宽度域不符,据此给出的中间档论证站不住。

iPad Split View 的档位是整屏宽的 1/3 / 1/2 / 2/3:12.9" 横屏约 455 / 683 / 911pt,11" 横屏约 398 / 597 / 796pt——最大档(12.9" 的 2/3,1366pt×2/3)约 911pt,到不了 1024;而所有 1/2 档(约 384–683pt)和各机型的低档 2/3 全部 ≤768,按 R7 会进入窄屏 drawer 分支,不是「中间档收窄侧栏并排」。因此「1024px = iPad 横屏 / Split View 上边界」两处都不准确:1024 只是 9.7" iPad 的全屏横屏宽,Split View 任何档位都到不了它。

该表述源自 issue #100 方案层的原文,但其背景第 7 条自己给的宽度域是「320pt ~ 1366pt」——issue 内部就互相矛盾,本文档 §3.6 逐字继承了方案层说法而没有消解它。三档分派的设计本身没问题,问题在论证。建议改写 §3.6:中间档实际覆盖的是 iPad 全屏竖屏宽度(768–1024)到 9.7" 全屏横屏之间的过渡宽度,外加 11"/12.9" 横屏的 2/3 分屏档(796 / 911pt);≤768 的 Split View 档位明确归 drawer(R7/R14 已隐含,写明即可),避免实施者误以为「所有 Split View 都走中间档」。

— Sisyphus


### 3.7 分期边界(每个 issue 的显式范围声明)

- #99:纯本地偏好,无后端改动、无 WS/HTTP 协议变更,可独立 PR。
- #100:纯前端,不涉及后端协议;第 3 层(触屏)可拆出单独排期;边缘滑出 drawer 是加分项,1、2 落地后再评估(**本期不做**)。

---

## 4. 分期实施计划(每期可独立合并、独立回滚)

| PR | 内容 | 覆盖需求 | 来源 | 依赖 |
|---|---|---|---|---|
| PR1 | 桌面侧栏折叠 / 展开 | R1–R4、R12(桌面部分)、R13、R16 | #99 全量 | 无 |
| PR2 | 视口与高度(最小改动,建议紧随 PR1) | R5、R6、R12 | #100 L1 | 无 |
| PR3 | 断点与 drawer 形态 | R7、R8、R14、R2(窄屏复用)、R12 | #100 L2 | PR1(语义复用) |
| PR4 | 触屏交互 | R9、R10、R11 | #100 L3 | PR2 |

- 顺序依据:#99 独立且风险集中在 iframe 重排,先落地宽屏行为;#100 至少先做 L1。
- PR3 的 drawer 是 collapsed 语义的窄屏渲染,**不是新状态**——review 时重点检查有没有偷偷引入第二个状态变量。
- 范围红线 R15 适用于全部 PR。

---

## 5. 验收标准

### 5.1 PR1(#99)

1. 折叠 / 展开过程中无内容溢出、`#main` 无抖动。
2. 折叠态终端 iframe(xterm + xterm-addon-fit)重排后不出现滚动条或错位。
3. 状态刷新后保持(localStorage 往返);清掉 localStorage 后回到默认展开。
4. `Ctrl/Cmd+B` 可切换,且在 input 内输入不触发。
5. `toggleGitSection()` 的 staged/unstaged 折叠状态不受影响,两个状态互不覆盖。
6. `internal/ui/ui_test.go` 增加 DOM 测试:collapsed 类能加到 `#app` 上、localStorage 往返正确(对齐现有 UI 测试写法,见 §6)。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

§5.1 第 6 条与 §6 测试策略自相矛盾。

§6 已明确 ui_test.go 的既有写法是 httptest 取 served 源码后 strings.Contains 文本断言(不能执行 JS),并把「localStorage 往返的状态读写」明确分给 node testdata/*.test.mjs 线。而本条原样继承了 issue #99 的验收措辞,要求 ui_test.go 断言「localStorage 往返正确」——照字面实施,只能写出名为「往返正确」实则只查字符串的断言,与 §6 自己的分工冲突。建议拆成两条写清:ui_test.go 锚点文本断言(collapsed class 名、key 名、相对顺序)+ node 行为用例(localStorage 往返、默认值降级)。

—— Kimi Code CLI(AI 评审)


### 5.2 PR2(#100 L1)

1. `100dvh` + `@supports` fallback;`100%` 宽度取代 `100vw`,无竖向滚动条导致的横向溢出。
2. viewport meta 含 `viewport-fit=cover`;safe-area inset 已处理。
3. 地址栏展开 / 收起、软键盘弹出 / 收起时布局不跳动,终端输入区始终可见。

### 5.3 PR3(#100 L2)

1. iPhone Safari 竖屏(375×667 / 390×844)与 iPad Safari(含 Split View 窄档)下,侧栏、tab 栏、终端区域均可达,无横向滚动条、无内容被裁掉。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【发现·低】R7 的 drawer 方案缺两处落地必需的前提,PR3 实施时只能临场发挥,建议在 R7 或本节验收里钉死:

  1. 定位包含块:position: absolute 的 drawer 需要定位祖先,而 #app 当前只有 display:flex; height:100vh; width:100vw(已核实 158-162,无 position)。不显式补 #app { position: relative },drawer 会相对更外层包含块定位,脱离 UI 容器。
  2. 遮罩与开关按钮的层级:遮罩需要盖住 #main 与 tab 区,但 §3.2 明确窄屏下 #header 左侧同一个按钮就是 drawer 开关——打开态必须能点它来关闭。若遮罩全屏盖住 #header,按钮不可点。z-index 分层与遮罩作用域当前均未定义。

这两点是「一个 class 两种渲染」分派的必要组成部分,建议随 acceptance 写进 PRD,而不是留给 PR3 review。

— Sisyphus

2. 窄屏 drawer 开合正常,点遮罩关闭;tab 滚动吸附生效。
3. 中间宽度(768–1024)侧栏收窄后并排仍可用。

### 5.4 PR4(#100 L3)

1. 侧栏上下分栏拖拽在 iOS / iPadOS 上可用,且拖拽过程中不触发页面滚动。
2. 所有可点击控件在触屏下有效命中区 ≥ 44×44pt(`pointer: coarse` 下验证)。
3. hover 样式不再粘滞。

### 5.5 全期通用(最大风险项)

1. **桌面行为零回归**:`index.html` 是 5562 行单文件、没有真机触屏回归手段,至少桌面 400px 窄窗口人工过检一轮 + `pointer: coarse` 模拟验证样式分支。
2. 现有测试全绿:`internal/ui/ui_test.go`、`internal/ui/terminal_status_test.go`;CI 门(`gofmt`、`go test ./...`、双二进制 build)通过。

---

## 6. 测试策略(对齐仓库现有约定)

- **Go 资源文本断言**(`internal/ui/ui_test.go` 既有写法):通过 `httptest` 取到 served 的 `index.html` / `static/kinds/*.js`,用 `strings.Contains` 断言 DOM 锚点、class 名、函数名与**相对顺序**(参考 `TestWebRenderersStoppedSwitchHidesAllFrames`、`TestEmptyWorktreeSwitchHidesAllWebPanels` 的 slicing + position 断言风格)。
- **node 行为测试**(`internal/ui/testdata/*.test.mjs`,`node --test`,由 `terminal_status_test.go` 的 `TestTerminalStatusHandling` 包进 `go test`):需要真正执行逻辑的用例(localStorage 往返的状态读写、快捷键守卫、断点分派函数)走这条线——**从真实 served 源码切片、打桩浏览器状态**,不断言副本。
- **CHANGELOG 房规**(issue #95):若用 "Pinned by N `node --test` cases" 声明覆盖数,每一处出现的数字都必须等于 `grep -c '^test('` 的导出总数;子集计数必须换措辞(如 "四个 #99 场景")。
- **人工清单**(每期结束过一遍):桌面宽屏 / 400px 窄窗;刷新持久化;清 localStorage 恢复默认;iframe 内焦点按 Ctrl+B(不应切换,注释解释);git section 折叠互不覆盖。
- **beta 渠道**:v0.6.0 发布前走 beta(v0.5.1 beta 曾抓出 3 个真实 bug 的先例),触屏相关问题主要靠该渠道收敛。

---

## 7. 风险

| 风险 | 影响 | 缓解 |
|---|---|---|
| 5562 行单文件,回归面大 | 桌面样式 / 终端重排被无意破坏 | 小步 PR + 资源文本断言锚定关键结构;每 PR 独立可回滚 |
| 无真机触屏回归手段 | iOS/iPadOS 问题漏到发布 | 桌面窄窗 + `pointer: coarse` 模拟 + beta 渠道真机验证 |
| collapsed flex 逻辑与响应式宽度规则干扰 | 抖动 / 双重状态 | 单一 class 真源、JS 零宽度计算(§3.1) |
| iframe 重排 | 终端 / 各内嵌 web UI 滚动条、错位 | R12 列为每轮必验收项;折叠态 fit 触发顺序显式固定 |
| 首次引入 localStorage | 隐私模式 / 旧浏览器抛异常 | try/catch 降级为不持久化,默认展开 |
| `100dvh` 兼容 | 旧 Safari 高度塌陷 | `@supports (height: 100dvh)` 渐进增强 |
| Ctrl/Cmd+B 与浏览器扩展 / 系统快捷键冲突 | 偶发不触发 | 属用户环境问题,不绕过;在注释与文档中说明 |

---

## 8. 非目标(明确不做)

- iframe 内页面(vendor xterm、reasonix / opencode / dsh 内嵌 web UI)的内部自适应——各自的问题,本 UI 只保证容器尺寸正确(R15)。
- 边缘滑出 drawer(#100 标注的加分项,后续评估)。
- 任何后端 / WS / HTTP 协议变更。
- PWA、安装、离线能力。

---

## 9. 落地时对主文档的同步清单(**本 PR 之外,实施期执行**)

- `docs/PRD.md` §7「当前实现状态」:新增 v0.6.0 条目(侧栏折叠 + 响应式 / 触屏适配,注明来源 #99/#100 与 `docs/plans/feature-v0.6.0-webui-sidebar-responsive/`)。
- `CHANGELOG.md` `## Unreleased`:每 PR 一条,遵守 §6 房规。
- `README.md` / `README.zh-CN.md` Features:可选,折叠 + iPad 可用性值得一句。
- `docs/API.md`、`docs/ARCHITECTURE.md`:**预计不动**(无协议 / 架构变更);如 PR 发现需要记录,单独评估。

---

## 10. 分支与版本

- **分支名**:`feature/v0.6.0-webui-sidebar-responsive`(承载 #99 + #100 的完整 v0.6.0 版本开发,非文档变更分支)。
- **版本**:v0.6.0,minor(新功能、无破坏性变更、无协议变更)。
- **基线**:`develop`(v0.5.2 已合入)。
- **发布**:beta 渠道先行(`develop` 出 beta tag),稳定后按既有 release 流程切 `main`。
Loading