Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
6.6 KiB
验收修复单 · 回答定位到开头:无用户行的一轮钉不住、闲置会话被留白(2026-09-17)
0. 基线
- 验收对象:
11c0028d(origin/staginghead,执行方变基后快进推送)。任务书: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)断言不变。
- 行为测试(fake scroller 或 jsdom)新增:容器内
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。