docs(tasks): 回答定位开头单扩到生时校正面

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
This commit is contained in:
Jesse_Chen
2026-09-17 13:10:33 +00:00
co-authored by Claude Fable 5.1
parent a9043d5542
commit 5203f9f0c3
2 changed files with 27 additions and 25 deletions
+1 -1
View File
@@ -132,7 +132,7 @@
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal``dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
| `TASK-consult-followup-tool-contract-20260917.md` | `PROGRESS-consult-followup-tool-contract-20260917.md` | 真机:申报时段会话连发「?」「你在说什么鬼」都 `run.failed runtime_contract_incomplete`,回执无任何 `tool` 步骤。根因是 Agent 系统指令写明「简单追问可复用已有 packet / context、不调工具」,而 `contractReady()` 要求每次请求恰好一次成功排盘调用;「已有 packet」跨请求并不存在(缓存只在单次请求内)。本命与窗口两个 Agent 同构。**产品拍板方案 1**:每轮必调工具(BUG-922 删例外句 + BUG-923 第 0 步 `toolChoice: required`);否决「没调工具就走不扣点纯对话」。第一轮正经问题为何失败留 T4 取证(回执只在 web 容器日志) | 待验收 | `codex/consult-followup-tool-contract-20260917` |
| `TASK-session-list-single-source-20260917.md` | `PROGRESS-session-list-single-source-20260917.md` | 会话列表一处数据源:本地 PG 兼容层 `order()` 只保留最后一键,`/api/sessions` 实际按 `id` 排、与游标不一致;`/` 与次级页两份数据源、`/` 每次回来重启动(产品拍板:首页与三个次级页进同一路由组,侧栏外壳与列表 provider 常驻 layout,不做 store);空「新对话」落库堆积(首页 50 条里 28 条);标题类别在后、同名靠墙钟 HH:MM。串行在 composer-guard 单之后。BUG 段 926 起 | 执行中(T1 已合入) | `codex/session-list-single-source-20260917` |
| `TASK-consultation-answer-start-anchor-20260917.md` | — | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;校正面贴底不变(hook 加模式)。BUG 段 930 起 | 待领取 | — |
| `TASK-consultation-answer-start-anchor-20260917.md` | — | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 待领取 | — |
### 个人报告
@@ -1,10 +1,10 @@
# 任务书 · 回答落在开头而不是结尾:会话滚动改为「问题钉在顶部、回答向下长」(2026-09-17)
# 任务书 · 回答落在开头而不是结尾:两个会话滚动改为「本轮开头钉在顶部、回答向下长」(2026-09-17)
## 0. 基线
- 基线 commit`f6db3c77``origin/staging` head;代码基线仍是 `dc2f2a16`,其后只有文档)。
- 分支:`codex/consultation-answer-start-anchor-20260917``git worktree add -b codex/consultation-answer-start-anchor-20260917 .worktrees/consultation-answer-start-anchor-20260917 origin/staging`
- 范围:`frontend/src/hooks/use-conversation-scroll-anchor.ts``frontend/src/hooks/use-consultation-run.ts`(一处调用改名)、`frontend/src/components/jump-to-latest-button.tsx``frontend/src/app/globals.css``frontend/DESIGN.md``frontend/tests/chat-notice-and-scroll-contract.test.ts``page.tsx` 只允许改 hook 调用参数(不增行)。**生时校正面不改行为**`rectification-agentic-chat.tsx` 只跟着 hook 签名改一个参数)。
- 范围:`frontend/src/hooks/use-conversation-scroll-anchor.ts``frontend/src/hooks/use-consultation-run.ts`(一处调用改名)、`frontend/src/components/jump-to-latest-button.tsx``frontend/src/app/globals.css``frontend/DESIGN.md``frontend/tests/chat-notice-and-scroll-contract.test.ts``page.tsx` 只允许改 hook 调用参数(不增行)`frontend/src/components/rectification-agentic-chat.tsx`(提交与新轮到达处各加一次 `pinLatestTurn()` 调用)及其滚动相关测试。**两个会话面同一语义。**
- 与同日其他单的关系:不碰 `use-session-management.ts`、不碰路由组,可与 composer-guard 单并行;若 session-list 单先合入且把 `page.tsx` 移到 `(app)/page.tsx`,本单在新路径上改。
- BUG 段:**BUG-930 起**924–929 已被同日两单占用;开工时核对 `docs/BUG_HISTORY.md` 最大号)。
@@ -18,9 +18,9 @@
- `frontend/src/hooks/use-consultation-run.ts` `send()`:用户消息乐观入列后调用 `conversationAnchor.anchorToLatest()`,把 `anchored` 强制置真并 `scrollTo` 底部——即使读者刚才手动上滑,新一轮也重新贴底。
- `frontend/src/app/page.tsx``jumpToLatestVisible = … && !conversationAnchor.anchored && messages.length > 0`,「跳到最新」只在读者**主动上滑**离开底部 96px 之后出现;回答本身长出视口不算。
- 主会话回答的 DOM 顺序(`chat-message-row.tsx`):`.message-assistant``.message-bubble` → 先 `.consultation-thinking-report`(思考块)再 `.consultation-report-analysis`(回答正文)。「回答开头」在视觉上就是**用户问题行的下一块**。
- 生时校正面用同一个 hook`rectification-agentic-chat.tsx`),它每一轮末尾是题目或选择卡,贴底是刻意的(BUG-041BUG-048 的防复发要求「生成中即时跟随到底部」
- 生时校正面用同一个 hook`rectification-agentic-chat.tsx` 第 469 行附近,`resetKey = caseId`,没有自己的 `anchorToLatest` 调用,完全靠 hook 的贴底跟随)。它每一轮是旁白 + 题目 + 选择卡,卡在末尾;长旁白同样把本轮开头推出视口。用户打字或点「发送」会生成 `role: "user"` 行;点选项不生成用户行,新一轮直接以助手行开始;开场轮也没有用户行。BUG-041 / BUG-048 当年要求校正面「生成中即时跟随到底部」。
Bug 历史检索:BUG-478 把两个会话面的滚动跟随并成这一个 hook,防复发是「跟随只经 `useConversationScrollAnchor`,跳到最新只经 `JumpToLatestButton`」;BUG-041 / BUG-048 要求校正面贴底。本单不是复发,是主会话的**跟随语义**本身要改,且必须在同一个 hook 里改、校正面语义不变
Bug 历史检索:BUG-478 把两个会话面的滚动跟随并成这一个 hook,防复发是「跟随只经 `useConversationScrollAnchor`,跳到最新只经 `JumpToLatestButton`,本单保持BUG-041 / BUG-048 要求校正面贴底,**本单按产品决策推翻这两条的跟随语义**(见 §3),它们「必须验证容器自身 `scrollTop`」「新增滚动容器必须覆盖三类触发源」的测试要求保留。本单不是复发,是两个面的**跟随语义**本身要改,且必须在同一个 hook 里改。
## 2. 根因
@@ -28,46 +28,48 @@ Bug 历史检索:BUG-478 把两个会话面的滚动跟随并成这一个 hook
## 3. 决策记录
- 产品负责人 2026-09-17 提出并授权:主会话改为**定位到回答开头**。
- 产品负责人 2026-09-17 提出并授权:主会话改为**定位到回答开头**;同日追加拍板:**生时校正面也改**,两个面同一语义,不留贴底模式
- Claude 定口径:
- **主会话**:发送时把这一轮的用户问题行滚到滚动容器顶部(留 `space-4` 顶边距),之后**流式期间不再跟随**;回答在问题下方向下生长。回答高度超过视口时显示「跳到最新」,按下才贴底并恢复跟随到本轮结束。读者自己滚到底部(96px 内)也恢复跟随,与现在一致。
- **两个面同一规则**:新一轮开始时把**本轮开头**滚到滚动容器顶部(留 `space-4` 顶边距),之后**流式期间不再跟随**;内容在它下方向下生长。本轮开头 = 这一轮的用户行;这一轮没有用户行(校正面点选项、开场轮、服务端自动续轮)就是新到达的助手行顶部。内容超过视口时显示「跳到最新」,按下才贴底并恢复跟随到本轮结束。读者自己滚到底部(96px 内)也恢复跟随,与现在一致。
- 为了让短回答也能把问题钉到顶部,滚动容器末尾需要一段**动态留白**(最后一轮的最小高度 = 容器可视高度 − 问题行高度),留白保留到下一轮发送为止;这是 claude.ai / ChatGPT 的同一做法,不算「空白页」。
- **切换会话 / 打开历史**:仍落在底部(最新内容);这一点不变。
- **生时校正面**行为完全不变(贴底跟随、卡片可见)。hook 加一个模式参数,两个面各选一种;不得写第二套滚动逻辑(BUG-478 红线)。
- **生时校正面**选择卡不再保证一开始就在视口里;旁白长时读者读完旁白往下滚或按「跳到最新」到卡。这是产品明确接受的取舍。hook 加模式参数,两个面调同一套;不得写第二套滚动逻辑(BUG-478 红线)。
- **思考块**:回答开头定义为用户问题行下面的第一块。若开工核实发现 `.consultation-thinking-report` 在流式期间或结算后**默认展开**且高度不可控,改为默认折叠、只留一行状态;若已是折叠/单行,不动。
- 不推翻 BUG-478(单一 hook、单一按钮)、BUG-041/048(校正面贴底)、BUG-218/252sticky 与 `padding-bottom` 的老坑:留白不得用 sticky 实现)。
- 不推翻 BUG-478(单一 hook、单一按钮)、BUG-218/252sticky 与 `padding-bottom` 的老坑:留白不得用 sticky 实现)。**推翻 BUG-041 / BUG-048 的「生成中即时跟随到底部」**,改为「每轮定位到本轮开头」;两条记录在 Bug 历史里补一行「2026-09-17 语义被 BUG-930 取代」,不改旧记录正文。
- AGENTS.md §6:不得出现第二套滚动跟随;揭幕后不得出现 spinner / 骨架。
## 4. 硬红线
1. 滚动逻辑只在 `useConversationScrollAnchor` 内;`page.tsx``use-consultation-run.ts`、消息组件不得自己 `scrollTo` / 监听 scroll。
2. 校正面的 `scrollTop` 行为在既有测试(`birth-time-mobile-scroll-contract``rectification-candidate-offer-anchor`)下不变
2. `birth-time-mobile-scroll-contract`「welcome content starts at the scroll origin」保持绿(开场轮在顶部与新语义一致);`rectification-candidate-offer-anchor` 是数据挂载测试,不涉滚动,不动。校正面既有的贴底断言按三栏改写成「本轮开头在顶部」
3. `prefers-reduced-motion` 继续尊重;scroll 监听继续 passive。
4. 既有断言改动写「原值 / 新值 / 原因」三栏;`chat-notice-and-scroll-contract.test.ts` 里「anchors the streaming scroll instead of following every token」等条目按新语义改写而不是删除。
5. `page.tsx` 不增行;`globals.css` 不新造常数(留白用现有 `--composer-reserve` / `space-*`)。
## 5. 任务分解
### T1 hook 加模式:`follow-bottom` 与 `pin-question`BUG-930
### T1 hook 改语义:从贴底跟随到钉住本轮开头BUG-930
- `useConversationScrollAnchor(container, active, resetKey, mode)`
- `follow-bottom`:现状,一行不改
- `pin-question``anchored` 初始为假(切换会话时 `resetKey` 变化仍先落底一次,然后置假);`ResizeObserver``follow()` 只在 `anchored` 为真时贴底(逻辑不变,只是初值不同);新增 `pinLatestQuestion()`:找到容器内最后一个 `.message-user``scrollTo({ top: row.offsetTop - space4 })`,并把 `anchored` 置假。`anchorToLatest()` 保持贴底并置真
- `use-consultation-run.ts` `send()``anchorToLatest()` 改为 `pinLatestQuestion()`(乐观用户消息入列后的下一帧执行,保证行已在 DOM)。排队草稿发出时同样
- 验收(`chat-notice-and-scroll-contract.test.ts`,源码合同 + jsdom 行为):
- `pin-question` 模式下,模拟流式增高 5 次,`scrollTop` 保持在问题行位置不变;`follow-bottom` 模式下 `scrollTop === scrollHeight - clientHeight`
- `send()` 调用的是 `pinLatestQuestion` 而不是 `anchorToLatest`(源码断言)。
- `rectification-agentic-chat.tsx` 传的是 `follow-bottom`(源码断言)
- `useConversationScrollAnchor(container, active, resetKey)` 签名不变,语义改
- `anchored` 初始为假;`resetKey` 变化(切换会话 / 换案例)仍先落底一次,然后置假
- `ResizeObserver``follow()` 只在 `anchored` 为真时贴底(逻辑不变,只是初值不同)
- 新增 `pinLatestTurn(target?: HTMLElement)`:不传参时取容器内**最后一个 `.message-user`**,若它后面已经有助手行(说明这一轮没有用户行)则取最后一个助手行;`scrollTo({ top: row.offsetTop - space4 })`,并把 `anchored` 置假。`anchorToLatest()` 保持贴底并置真
- 主会话:`use-consultation-run.ts` `send()``anchorToLatest()` 改为 `pinLatestTurn()`(乐观用户消息入列后的下一帧执行,保证行已在 DOM);排队草稿发出时同样。
- 校正面:`rectification-agentic-chat.tsx` 在(a)用户提交文字、(b)点选项、(c)新助手轮到达且本轮没有用户行(开场、自动续轮)三处调用 `pinLatestTurn()`;每轮只调一次,流式增量不再调
- 验收(`chat-notice-and-scroll-contract.test.ts` 与校正面滚动测试,源码合同 + jsdom 行为):
- 模拟流式增高 5 次,`scrollTop` 保持在本轮开头位置不变;`anchorToLatest()``scrollTop === scrollHeight - clientHeight` 且继续跟随
- `send()` 调用的是 `pinLatestTurn` 而不是 `anchorToLatest`(源码断言);`rectification-agentic-chat.tsx` 含三处 `pinLatestTurn()` 且不含 `scrollTo`(源码断言)。
- 校正面 jsdom:点选项后新助手行顶部在视口顶部;开场轮在顶部。
### T2 「跳到最新」在回答长出视口时出现(BUG-930)
- `page.tsx``jumpToLatestVisible` 改为 hook 返回的 `latestBelowFold`(滚动容器底部距离 > 96px **且**本轮有新内容或正在流式)决定,不再只看「读者上滑过」。按下后贴底并恢复跟随。
- `page.tsx``jumpToLatestVisible` `rectification-agentic-chat.tsx` 第 1815 行附近的 `!conversationAnchor.anchored`改为 hook 返回的 `latestBelowFold`(滚动容器底部距离 > 96px **且**本轮有新内容或正在流式)决定,不再只看「读者上滑过」。按下后贴底并恢复跟随。
- 视觉不变(`DESIGN.md` Jump to latest 一节的结构、位置、表面都不动),只改 Visibility 一条的文案。
- 验收:jsdom 行为测试:流式增高到超出视口后 `latestBelowFold` 为真;按下 `anchorToLatest()` 后为假且 `scrollTop` 贴底;读者自己滚回 96px 内也为假。
### T3 末尾动态留白(BUG-930
- 滚动容器最后一轮(最后一个 `.message-assistant`,或流式占位)加 `min-height: calc(var(--conversation-viewport) - var(--latest-question-height))`,两个变量由 hook 在 `ResizeObserver` 回调里写到容器 `style`(只在 `pin-question` 模式)。不得用 sticky、不得改 `.conversation``padding-bottom`
- 两个面的滚动容器最后一轮(最后一个助手行,或流式占位)加 `min-height: calc(var(--conversation-viewport) - var(--latest-turn-head-height))`,两个变量由 hook 在 `ResizeObserver` 回调里写到容器 `style`。不得用 sticky、不得改 `.conversation``padding-bottom`校正面的时间轴条在滚动容器之外(DESIGN.md 时间轴一节),不参与计算。
- 留白保留到下一轮发送;切换会话时清零。
- 验收:源码合同:`globals.css` 含该规则且不含新增 sticky;jsdom:短回答(一行)发送后 `scrollTop` 等于问题行 `offsetTop - space4`(说明留白足够把问题顶到顶部)。
@@ -77,12 +79,12 @@ Bug 历史检索:BUG-478 把两个会话面的滚动跟随并成这一个 hook
### T5 记录
- `docs/BUG_HISTORY.md` 新增 BUG-930(关联 BUG-478BUG-041BUG-048);`CHANGELOG.md` 一句「回答从开头开始读:发送后问题钉在顶部,回答向下生长,长出视口时显示跳到最新」;`frontend/DESIGN.md` 新增「Answer start anchor」小节并改 Jump to latest 的 Visibility`docs/tasks/PROGRESS-consultation-answer-start-anchor-20260917.md`;状态板行。
- `docs/testing/` 真机条目:① 问一个会有长回答的问题(例如「详细分析我的事业格局」):发送后问题行在顶部、回答从它下面开始、视口不动;② 回答超过一屏后出现「跳到最新」,按下贴底并跟随到结束;③ 短回答(一两句)问题仍在顶部、下方留白、不抖动;④ 切换到别的会话落在底部;⑤ 生时校正面每轮仍贴底、选择卡可见;⑥ iPhone 键盘弹起收起后位置不跳(配合 BUG-920 单一起看)。
- `docs/BUG_HISTORY.md` 新增 BUG-930(关联 BUG-478;写明取代 BUG-041 / BUG-048 的跟随语义,并在那两条末尾各补一行指回 BUG-930);`CHANGELOG.md` 一句「回答从开头开始读:发送后问题钉在顶部,回答向下生长,长出视口时显示跳到最新」;`frontend/DESIGN.md` 新增「Answer start anchor」小节并改 Jump to latest 的 Visibility`docs/tasks/PROGRESS-consultation-answer-start-anchor-20260917.md`;状态板行。
- `docs/testing/` 真机条目:① 问一个会有长回答的问题(例如「详细分析我的事业格局」):发送后问题行在顶部、回答从它下面开始、视口不动;② 回答超过一屏后出现「跳到最新」,按下贴底并跟随到结束;③ 短回答(一两句)问题仍在顶部、下方留白、不抖动;④ 切换到别的会话落在底部;⑤ 生时校正面:打字提交、点选项、开场三种情况下本轮开头都在顶部,长旁白时出现「跳到最新」、按下能看到选择卡;⑥ iPhone 键盘弹起收起后位置不跳(配合 BUG-920 单一起看)。
## 6. 让步顺序
T1 > T2 > T3 > T4。T3 若在 iOS Safari 上留白引起抖动,可退为「留白只在回答未结束时存在,结算后移除」,写进进度记录。
T1 > T2 > T3 > T4。T3 若在 iOS Safari 上留白引起抖动,可退为「留白只在回答未结束时存在,结算后移除」,写进进度记录。校正面与主会话不得让步为两种语义。
## 7. 开工前置命令
@@ -96,4 +98,4 @@ grep -n "^## BUG-" ../docs/BUG_HISTORY.md | tail -1
## 8. 验收口径
`tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build``/` 仍 Static;首屏 gzip ±2%`page.tsx` 行数 ≤ 开工时;校正面滚动测试全绿。真机六条由产品负责人在 staging 走。
`tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致、新增测试全绿、总数不降;`next build``/` 仍 Static;首屏 gzip ±2%`page.tsx` 行数 ≤ 开工时;校正面滚动测试按新语义全绿。真机六条由产品负责人在 staging 走。