Repository navigation
docs(prd): v0.6.0 Web UI sidebar collapse + responsive/touch PRD change doc (issues #99, #100) - #109
Conversation
…ge doc (issues #99, #100) Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
| 3. 状态刷新后保持(localStorage 往返);清掉 localStorage 后回到默认展开。 | ||
| 4. `Ctrl/Cmd+B` 可切换,且在 input 内输入不触发。 | ||
| 5. `toggleGitSection()` 的 staged/unstaged 折叠状态不受影响,两个状态互不覆盖。 | ||
| 6. `internal/ui/ui_test.go` 增加 DOM 测试:collapsed 类能加到 `#app` 上、localStorage 往返正确(对齐现有 UI 测试写法,见 §6)。 |
There was a problem hiding this comment.
§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 评审)
| | 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 | |
There was a problem hiding this comment.
可追溯性缺口: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 评审)
| | 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 | | ||
| | 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 | |
There was a problem hiding this comment.
断点边界归属歧义 +「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 评审)
| | 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 | |
There was a problem hiding this comment.
规格缺口:窄屏 drawer 的初始开合状态未规定。
单一 sidebarCollapsed 布尔态(§3.1)+ 默认展开(R3)意味着窄屏首次访问时 drawer 以「展开」形态渲染——打开即盖住 #main,用户须先点遮罩才能看到终端。R14 只说窄屏用 drawer 形态,未规定窄屏初始开合,也未写明窄屏是否与桌面共享同一个持久化 key(桌面收起过的用户到 iPad 上 drawer 默认关闭——行为合理,但应写明这是同一状态机的必然结果而非新状态)。建议补一条验收:窄屏首次加载 drawer 默认状态 + 与桌面持久化状态的共享语义。
—— Kimi Code CLI(AI 评审)
|
|
||
| ### 3.4 快捷键基础设施(首个全局 keydown) | ||
|
|
||
| - `document` 级 keydown,`metaKey || ctrlKey` + `key === 'b'` 判定。 |
There was a problem hiding this comment.
实施细节提醒: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 评审)
linletian
left a comment
There was a problem hiding this comment.
评审总结(docs-only PR;仅评审并评论,未做任何改动)
已逐条核实 §1.4 代码现状表对当前 develop(index.html 5562 行):15 项事实锚点全部命中、无一漂移——5562 行数、viewport meta(L5)、--sidebar-width: 240px(L9)、--term-ctrl-btn-size: 32px(L23)、仅 2 个 @media 均为 prefers-color-scheme(L79/L1208)、body,html overflow:hidden(L144–152)、#app(L158–162)、#sidebar(L165–171)、.icon-btn padding:2px(L187–194)、#main(L539–545)、.sidebar-actions(L1335–1338)、#sidebar-resize-handle→startSidebarResize(L1343/L2355–2392)、toggleGitSection(L2200/L1344–1365)、唯一 keydown = handleRenameKeydown(L2565/L2623)、localStorage 0 处、SERVER_UPGRADED_FLAG(L1742/L1797/L2113–2114)、Pointer/Touch/dvh/visualViewport/safe-area 全 0。行号漂移注记与 issue #99/#100 原文比对属实。
测试策略(§6)与仓库约定核对无误:TestWebRenderersStoppedSwitchHidesAllFrames、TestEmptyWorktreeSwitchHidesAllWebPanels、TestTerminalStatusHandling(node --test 包装、node 缺席时 skip)均存在;CHANGELOG 房规描述与 issue #95 及 TestTerminalStatusChangelogCount 守卫一致;「v0.5.1 beta 抓出 3 个真实 bug」与 CHANGELOG v0.5.1 条目相符;§9 引用的 docs/PRD.md §7「当前实现状态」存在;分支名 / 基线(develop)/ 版本定位与 PR 元数据一致。文档整体质量很高。
发现 5 个问题,已逐条行内评论:
- §5.1 第 6 条与 §6 测试策略矛盾(L142)——ui_test.go 做不了真正的 localStorage 往返,措辞需拆分。
interactive-widget需求层静默丢失(L65 R5)——取舍正确但未记录。- R7 断点 768px 边界两档重叠 +「iPad 竖屏 768pt」过宽(L67,连带 L109/L154)。
- 窄屏 drawer 初始开合状态未规定(L74 R14)——默认展开意味着窄屏首访抽屉盖住终端。
key === 'b'大小写 / repeat 细节(L99 §3.4)。
另有一处小 nit(不单独开评论):R10 / §5.4 的「44pt」建议统一为 CSS px 口径(HIG 44pt 在 iOS Safari 对应 44 CSS px;若实施者照写 44pt 会得到 ~58.7px,无害但与意图不符)。
—— Kimi Code CLI(AI 评审 · 2026-10-09)
linletian
left a comment
There was a problem hiding this comment.
独立评审:PR #109 — v0.6.0 侧栏折叠 + 响应式/触屏 PRD 变更文档
评审方式:只读独立评审(未对仓库做任何改动)
评审日期:2026-10-09
评审基线:develop(v0.5.2 合入后,index.html= 5562 行)
署名:Sisyphus
结论
无阻断性问题,本文档可以作为 v0.6.0 分期实施的 PRD 基线。有 5 项建议在开工前修订(详见行内评论):
- [中] §1.4 把
interactive-widget列为现状缺陷,但 R5/R6 与全部验收标准均未承接——缺陷陈述与需求表不闭环。 - [中] R7 断点 768 同时属于窄屏与中间档两个区间,边界归属未定义。
- [中] §3.6「Split View 768–1024」与 iPad Split View 实际宽度域(约 384–911pt,上界到不了 1024)不符,中间档的论证需要修正。
- [低] R7 的 drawer 方案缺定位包含块(
#app无 position)与遮罩/开关按钮层级的前提声明。 - [低] R8「复用 568-574 既有实现」截断了隐藏滚动条规则块(实际 568–600),§1.4 同行同样问题。
已逐条核实验证为属实的部分
- §1.4 全部行号/事实锚点与当前 develop 一致:
#app158-162(100vh/100vw);#sidebar165-171 +--sidebar-width9;#main539-545;#headerHTML 1370 / CSS 548-556;.sidebar-actionsHTML 1335-1338;resize handle 1343 与startSidebarResize2355-2392;toggleGitSection2200;唯一 keydown 为 tab 重命名输入框(2565/2623);sessionStorage 三处 1742/1797/2113-2114;localStorage 0 处;宽度断点 0 个且两个@media(79/1208)均为prefers-color-scheme;pointer/touch/dvh/visualViewport/safe-area 均 0 处;body overflow hidden 144-152;.icon-btn187-194 padding 2px;--term-ctrl-btn-size23。§1.4 脚注的行号漂移说明(49 行)也与 issue 快照差异吻合。 - §6 测试设施引用全部存在:
ui_test.go的TestWebRenderersStoppedSwitchHidesAllFrames/TestEmptyWorktreeSwitchHidesAllWebPanels、terminal_status_test.go的TestTerminalStatusHandling、testdata/terminal_status.test.mjs(当前 68 个test(用例);issue #95 CHANGELOG 房规的表述与 CHANGELOG 中 #95 条目一致;「v0.5.1 beta 抓出 3 个真实 bug」与 v0.5.2 发布条目一致。 - §9 同步清单与 §10 发布路径属实:
docs/PRD.md§7「当前实现状态(与愿景差异)」存在;CHANGELOG 有## Unreleased;README 中英双语均在;beta 先行流程与 CHANGELOG v0.5.1 beta 流程一致。§5.5 的 CI 门与.github/workflows/go-ci.yml一致(CI 另跑node --test,§6 已覆盖)。 - 需求完整性:R1–R16 与 §4 分期映射闭合(无需求无 PR 承接、无 PR 遗漏红线);issue #99 / #100 的验收标准逐条落入 §5.1–§5.5;非目标(§8)与 issue 的范围声明一致。
— Sisyphus
| | 全文件唯一 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` | |
There was a problem hiding this comment.
【发现·中】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
| | 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 | | ||
| | 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 | |
There was a problem hiding this comment.
【发现·中】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
| | R5 | 视口与高度:`#app` 高度 `100dvh`(`@supports` 保留 `100vh` fallback)、宽度 `100%` 取代 `100vw`;viewport meta 补 `viewport-fit=cover`;容器用 `env(safe-area-inset-*)` 处理刘海与 Home Indicator | #100 L1 | | ||
| | 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 | | ||
| | R8 | 窄屏 `#tabs-container` 补 `scroll-snap-type` 让 tab 吸附(滚动条继续隐藏,复用 568-574 既有实现) | #100 L2 | |
There was a problem hiding this comment.
【发现·低·锚点精度】「复用 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
|
|
||
| ### 3.6 断点选取依据 | ||
|
|
||
| `768px` = iPad 竖屏宽度;`1024px` = iPad 横屏 / Split View 上边界。中间档收窄侧栏而非直接 drawer,因为 Split View 下半屏仍可容纳收窄侧栏 + 终端。 |
There was a problem hiding this comment.
【发现·中】「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
|
|
||
| ### 5.3 PR3(#100 L2) | ||
|
|
||
| 1. iPhone Safari 竖屏(375×667 / 390×844)与 iPad Safari(含 Split View 窄档)下,侧栏、tab 栏、终端区域均可达,无横向滚动条、无内容被裁掉。 |
There was a problem hiding this comment.
【发现·低】R7 的 drawer 方案缺两处落地必需的前提,PR3 实施时只能临场发挥,建议在 R7 或本节验收里钉死:
- 定位包含块:
position: absolute的 drawer 需要定位祖先,而#app当前只有display:flex; height:100vh; width:100vw(已核实 158-162,无 position)。不显式补#app { position: relative },drawer 会相对更外层包含块定位,脱离 UI 容器。 - 遮罩与开关按钮的层级:遮罩需要盖住
#main与 tab 区,但 §3.2 明确窄屏下#header左侧同一个按钮就是 drawer 开关——打开态必须能点它来关闭。若遮罩全屏盖住#header,按钮不可点。z-index 分层与遮罩作用域当前均未定义。
这两点是「一个 class 两种渲染」分派的必要组成部分,建议随 acceptance 写进 PRD,而不是留给 PR3 review。
— Sisyphus
Summary
Drafts the consolidated PRD change document for issues #99 (desktop sidebar collapse/expand) and #100 (responsive layout + touch, iOS/iPadOS), to be carried by the v0.6.0 development branch. Placed under
docs/plans/feature-v0.6.0-webui-sidebar-responsive/; the main docs (docs/PRD.md,docs/API.md,docs/ARCHITECTURE.md) are untouched — §9 of the doc lists the sync checklist for the implementing PRs.Analysis (what the doc records)
The two issues are one feature, not two. #100 explicitly states its narrow-screen drawer reuses #99's collapse/expand semantics (drawer shape, not width-to-zero) and recommends #99 land the wide-screen behavior first. The doc models this as a single sidebar-visibility state machine with two renderings, dispatched by a viewport breakpoint:
sidebarCollapsedboolean + one CSS class as the single source of truth; wide screen =width: 0+overflow: hidden, narrow screen = off-canvas drawer + mask; JS only reads state and toggles the class — zero width math, which is what makes ui(sidebar): 工作树侧栏支持折叠 / 展开(桌面端) #99's "must not fight the responsive width rules" requirement hold.develop(index.html is now 5562 lines): 0 localStorage uses, 0 width breakpoints (both@mediaareprefers-color-scheme), 0 Pointer/touch/dvh/visualViewport/safe-area. The issue line numbers have drifted (e.g.startSidebarResizeis at 2355, not 2324; the onlykeydownis now the tab-rename input), so the doc mandates symbol anchors over line numbers.ui_test.goasset-text assertions,testdata/*.test.mjssliced-sourcenode --testwired throughterminal_status_test.go, the CHANGELOG count house rule (issue test(ui): anchor TestTerminalStatusChangelogCount to the right CHANGELOG entry instead of the first match #95), and the beta channel for real-device touch coverage.Notes
go test ./...untouched paths).