Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
69 lines
6.6 KiB
Markdown
69 lines
6.6 KiB
Markdown
# 验收修复单 · 回答定位到开头:无用户行的一轮钉不住、闲置会话被留白(2026-09-17)
|
||
|
||
## 0. 基线
|
||
|
||
- 验收对象:`11c0028d`(`origin/staging` head,执行方变基后快进推送)。任务书:`TASK-consultation-answer-start-anchor-20260917.md`。
|
||
- 分支:`codex/consultation-answer-start-anchor-fix-20260917`,从 `origin/staging` 起。
|
||
- 范围:只有 `frontend/src/hooks/use-conversation-scroll-anchor.ts` 与其测试、`docs/BUG_HISTORY.md`、`docs/tasks/`。不动 `page.tsx`、不动组件、不动 CSS。
|
||
- BUG 段:**BUG-931 起**(基线最大号 BUG-930)。
|
||
- **前置**:staging 自 `e4e73f56`(会话列表单)起 `tsc --noEmit` 与 `next build` 都红(`src/lib/session-sidebar-row.ts(30,41)`、`tests/chart-library-session.test.ts(89,42)`:`Expected 1 arguments, but got 2`),门禁过不了,`/api/health` 仍停在 `dc2f2a16`。那两处属于会话列表单的验收,另行处理;本单执行时若 staging 仍红,先在本分支顺手修这两行并在进度记录写明,否则本单也发不出去。
|
||
|
||
## 1. 验收实证
|
||
|
||
验收环境:Linux,Node 20.19,Google Chrome 151 无头(本机有 Chrome,真实布局;无登录态,所以用真实 hook + 与 `globals.css` 同参数的骨架页做场景,不是 staging 页面)。
|
||
|
||
| 项 | 结果 |
|
||
| --- | --- |
|
||
| `tsc --noEmit` | 2 错,**均继承自 `e4e73f56`**,本提交 0 新增 |
|
||
| `npm run lint` | 0 error / 117 warning |
|
||
| `npm test` | 3455 条,39 红;与父提交 `e4e73f56`(3450 条,39 红)逐条一致,**0 新红、+5 新测试全绿**。`e4e73f56` 自身比 `5203f9f0` 多 4 条红(会话列表单的事,另记) |
|
||
| `next build --webpack` | 编译通过,`Failed to type check`(同上两处继承错误);`/` Static 与 gzip 因此未核 |
|
||
| `page.tsx` 行数 | 1830 → 1830 |
|
||
| 文档 | BUG-930、BUG-041/048 各补指回行、CHANGELOG、DESIGN「Answer start anchor」、真机清单、PROGRESS 齐 |
|
||
| T4 思考块 | 执行方核实「按步骤条数有界、结算后折一行、未改」,与代码一致,接受 |
|
||
|
||
真实布局场景(容器 600px 高,`.message-list` 与 `.message` 用 `globals.css` 同一组 padding,最后一轮的 `min-height` 规则原样):
|
||
|
||
| 场景 | 结果 | 结论 |
|
||
| --- | --- | --- |
|
||
| S1 长历史、读者在底部、发问 | 钉顶后用户行距容器顶 16px;流式增高 6 步 `scrollTop` 纹丝不动;尾部超出 96px 后 `latestBelowFold` 为真;按「跳到最新」后贴底并跟随 | 通过 |
|
||
| S2 短历史(不溢出)、发问 | 留白让用户行到顶(16px),增高不动 | 通过 |
|
||
| S3 读者先滚到顶部再发问 | 钉顶 16px,增高不动 | 通过 |
|
||
| S4 切换会话 | 落底,`anchored` 为真 | 通过 |
|
||
| S5 闲置历史会话,读者上滑 400px,某条旧消息高度变化 30px | **最后一条助手行被写入留白**(底部从 936 变 1320,`--conversation-viewport` / `--latest-turn-head-height` 被写上),`scrollTop` 漂 30px | **P2,BUG-932** |
|
||
| S6 本轮没有用户行(校正点选项 / 开场 / 自动续轮):追加新助手行后 `pinLatestTurn()` | **钉不住**:新助手行停在距顶 480px;`--latest-turn-head-height` 被写成 740px;随后第一步增高 `anchored` 翻成真,视口回到贴底跟随 | **P1,BUG-931** |
|
||
|
||
S6 的机理(按符号):`resolveTurnHead` 正确选到新助手行;但 `applyTurnSpacer(element, head)` 用 `head.offsetHeight` 写 `--latest-turn-head-height`,而这个 head **就是**承载 `min-height: calc(viewport − head)` 的最后一条助手行——前一帧 `follow()` 的非贴底分支已经给它写过一次留白,于是量到的高度是「内容 + 留白」,留白公式自指后坍缩为 0,`scrollTo` 目标被 `scrollHeight` 夹住,头钉不到顶。接着 `measure()` 见 `distance ≤ 96` 解除 `holdUnpin`,`nextAnchorState` 判回 `anchored = true`,后续增高就是 BUG-930 之前的贴底行为。校正面用 `ChatMessageRow`,行也是 `.message-assistant`,所以真实页面同样命中。
|
||
|
||
S5 的机理:`follow()` 的非贴底分支对**任何** `anchored = false` 的容器都 `applyTurnSpacer`,不管有没有正在进行的一轮。读者在旧会话里上滑后,任何子元素尺寸变化(窗口宽度、图片、思考块折叠)都会给最后一条助手行补一段留白,还会把 `latestBelowFold` 顶成真。
|
||
|
||
## 2. 任务分解
|
||
|
||
### F1 头就是留白行时,留白按整视口算(BUG-931)
|
||
|
||
- `applyTurnSpacer(container, head)`:若 `head` 与 `lastTurnTail(container)` 是同一个元素(本轮无用户行),`--latest-turn-head-height` 写 `0px`,让该行 `min-height` = 整个视口;否则照旧写 `head.offsetHeight`。或者在量高之前 `clearTurnSpacer` 再量,两者取其一,进度记录写明选哪种及原因。
|
||
- `pinLatestTurn` 在 `applyTurnSpacer` 之后、`scrollTo` 之前必须能把 head 钉到 `offset − space4`;不得靠 `holdUnpin` 之外的新状态。
|
||
- 验收:
|
||
- 行为测试(fake scroller 或 jsdom)新增:容器内 `[u][a][u][a][a-new]`,`pinLatestTurn()` 后 `scrollTop === offset(a-new) − 16`,`--latest-turn-head-height === "0px"`;随后增高 3 步 `anchored` 仍为假、`scrollTop` 不变。
|
||
- 现有主会话场景(S1–S3)断言不变。
|
||
|
||
### F2 留白只在本轮钉住期间存在(BUG-932)
|
||
|
||
- `follow()` 的非贴底分支只在 `pinnedHeadRef.current !== null` 时 `applyTurnSpacer`;读者单纯上滑(无 pin)不写留白。`anchorToLatest()` 与 `resetKey` 变化时清 `pinnedHeadRef` 与留白(现在已清)。
|
||
- `latestBelowFold` 在无 pin、读者上滑时仍按尾部超出 96px 判定(保留「读者上滑后可跳到最新」)。
|
||
- 验收:行为测试新增:长历史落底 → 上滑 400px → 改某条旧消息高度 → 容器 `style` 无 `--conversation-viewport`,`scrollTop` 不变(允许 ±1px)。
|
||
|
||
### F3 记录
|
||
|
||
- `docs/BUG_HISTORY.md` 新增 BUG-931、BUG-932(关联 BUG-930);BUG-930 的「验证」补一句真实布局场景已由验收方跑过。
|
||
- `docs/tasks/PROGRESS-consultation-answer-start-anchor-fix-20260917.md`;状态板行。
|
||
- 真机清单第 5 条拆成两条:点选项后新助手行在顶部;开场轮在顶部。
|
||
|
||
## 3. 硬红线
|
||
|
||
沿用原单 §4:滚动逻辑只在 hook 内;单一 hook、单一按钮;不 sticky;改断言写三栏;`page.tsx` 不增行。
|
||
|
||
## 4. 验收口径
|
||
|
||
`tsc --noEmit` 0 错(含前置两行修复后);`npm run lint` 0 error;`npm test` 失败清单与 `11c0028d` 逐条一致、新增测试全绿;`next build` 通过、`/` Static、gzip ±2%(此项因门禁红一直没核,本单必须补上)。验收方会用同一套 Chrome 骨架页复跑 S1–S6。
|