Files
Jyotisha/docs/tasks/TASK-consultation-answer-start-anchor-fix-20260917.md
T

6.6 KiB
Raw Blame History

验收修复单 · 回答定位到开头:无用户行的一轮钉不住、闲置会话被留白(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。