diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f05a5ad2..86f6a770 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -132,7 +132,8 @@ | `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–T4;延迟落库让步) | `codex/session-list-single-source-20260917` | -| `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 待验收 | `worktree/green-harbor-5be3` | +| `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 验收未通过:主会话钉顶/跳到最新/切会话(Chrome 真实布局 S1–S4)通过;P1 无用户行的一轮(校正点选项/开场)留白自指坍缩、钉不住并回退贴底(BUG-931);P2 闲置会话上滑后子元素尺寸变化被写留白(BUG-932)。tsc/build 红继承自 e4e73f56。修复单 `TASK-consultation-answer-start-anchor-fix-20260917.md` | `worktree/green-harbor-5be3` | +| `TASK-consultation-answer-start-anchor-fix-20260917.md` | — | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 待领取 | — | ### 个人报告 diff --git a/docs/tasks/TASK-consultation-answer-start-anchor-fix-20260917.md b/docs/tasks/TASK-consultation-answer-start-anchor-fix-20260917.md new file mode 100644 index 00000000..e8dd6578 --- /dev/null +++ b/docs/tasks/TASK-consultation-answer-start-anchor-fix-20260917.md @@ -0,0 +1,68 @@ +# 验收修复单 · 回答定位到开头:无用户行的一轮钉不住、闲置会话被留白(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。