fix(chat): 揭示前的定位只做一次,别在流式里反复重滚 - #534
Closed
eeee0717 wants to merge 2 commits into
Closed
Conversation
eeee0717
force-pushed
the
refactor/message-list-hook-layers
branch
from
August 15, 2026 05:16
b638942 to
dc28605
Compare
eeee0717
force-pushed
the
fix/ready-gate-restreams
branch
from
August 15, 2026 05:16
0e2f500 to
c014ce3
Compare
新建话题里发第一条消息时,offset 会在 0 与 24/40 之间来回弹四五次。 根因是 ready-gate 越界。它的 effect 依赖 contentBaseHeight,而流式每来一个 chunk 内容高度就变一次,于是静默窗口反复重启、永不完成,ready 永远报不出来, gate 也就在整段流式里每次重跑都真滚一次。实测三轮:send-anchor 场景 58/45/75 次,stream-scroll 场景 49/25/42 次。这些滚动全落在「内容还不满一屏」的阶段, 此时末端就是顶端,所以每次滚动都把 offset 拽回 0。 续轮发送一次都不出现——它进话题时 ready 早就报过、gate 已关闭。这个不对称本身 就是「gate 越界」的证据,也解释了为什么此前只在第一条消息上看得见。 gate 的两半职责其实是分开的:静默窗口负责把迟到的高度修正挡在遮罩后,这一半没 有问题;而「揭示前把列表放到正确位置」本就是一次性事件,重跑只该重启窗口、不该 再滚一次。位置维护此后归尾随状态机与 anchoredEndSpace。 bench 三轮对比(LAYOUT_BENCH_UDID=iPhone 17 Pro (layout-bench)): - readyGate 程序化滚动 75 → 1 - send-anchor offset 轨迹 24→0→40→0→24→0→24→0→16→80 变为 16→80→104→173→213→233→253→273→293→353→413→433→453→513(严格单调) - offset-reversal 在 send-anchor / stream-scroll 三轮均为 0 - viewport-blank 0/6/0 → 0/0/0 冷进入已有长历史话题(gate 的原始用途,bench 未覆盖)手工像素走查:录像抽帧后 内容首帧可见(f0686)与最终帧(f0909)的行签名在 ±90px 内最佳偏移为 0 且 corr@0 == corr@best,是原地淡入无位移;随后向末端连滑四次画面 0/1888 行变化、 corr=1.0,确认落点即内容末端。无「第一次进入才跳」回归。 新增的守卫测试已做负向验证:移除 !didGateScrollRef.current 后该测试变红。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 新增「ready gate 越界」一节:交互锁只解决「用户在拖时别滚」,没解决「凭什么滚 这么多次」;不对称(follow-up-turn 一次不出现)是根因证据;冷进入靠像素验, 并说明为什么「没跳」单独不够——一直停在错的地方也不跳。 - 确定性表更新为修复后三轮,并补上两个容易被当成缺陷的数字的解释: estimate-collapse 恒 252px 是全新话题的估值冷启动(t≈195ms、content=0、 两行合计 104px 远小于视口,无可滚动距离);follow-up-turn 的 viewport-blank 从 40% 涨到 58% 是钉顶到位更快导致采样落在预留空白更早的时刻,稳态仍为 0% ——但阈值余量已从 20 个百分点缩到 1.4 个,记下下次的判别方法。 - 「不要用逐帧互相关」补上适用边界:该禁令针对「位移是多少」;问「有没有位移」 时同报 corr@best 与 corr@0、两者相等才算数,周期纹理不会让真实零位移失去最优。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eeee0717
force-pushed
the
refactor/message-list-hook-layers
branch
from
August 15, 2026 08:30
dc28605 to
cbc5e04
Compare
eeee0717
force-pushed
the
fix/ready-gate-restreams
branch
from
August 15, 2026 08:30
c014ce3 to
4947354
Compare
Collaborator
Author
|
已合并进 #549 一并 review——这几层都是 bench harness 之后对消息列表的优化,拆开反而不好读。分支保留,需要时可重开。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
在新建话题里发出第一条消息时,列表位移会在 0 与 24/40px 之间来回弹四五次。续轮发送(进入已有话题再发)一次都不出现。
根因
ready gate 越界。
它的 effect 依赖
contentBaseHeight,而流式每来一个 chunk 内容高度就变一次。于是静默窗口反复重启、永远等不到完成,ready报不出来,gate 也就在整段流式里每次重跑都真滚一次:send-anchorstream-scrollfollow-up-turn这些滚动全落在「内容还不满一屏」的阶段,此时末端就是顶端,所以每次滚动都把 offset 拽回 0。
follow-up-turn那个 0 是关键证据:它进话题时ready早就报过、gate 已关闭。这个不对称本身就说明 gate 越了界,也解释了为什么这个抖动此前只在第一条消息上看得见。上一层 PR (#530) 给这个
scrollToEnd补过交互锁,把gesture-conflict修到了 0。但交互锁只回答了「用户手上有动作时该不该滚」,没有回答「凭什么滚这么多次」。修法
把 gate 的两半职责拆开:
anchoredEndSpace。一行
didGateScrollRef。注意它只在真正滚动的分支置位:用户正在拖时跳过滚动的那条路径不消耗这一次,所以「拖动中跳过 → 松手后补滚」的既有行为不变。验证
bench 三轮(
iPhone 17 Pro (layout-bench)):readyGate程序化滚动offset-reversal(send-anchor)offset-reversal(stream-scroll)viewport-blank(send-anchor)viewport-blank(stream-scroll)send-anchor的 offset 轨迹从24→0→40→0→24→0→24→0→16→80变成严格单调的16→80→104→173→213→233→253→273→293→353→413→433→453→513。冷进入的手工像素走查:gate 的原始用途是「冷进入已有长历史话题时挡住迟到的布局修正」,bench 三个场景都不覆盖,所以只能回到像素。录像抽帧后:
corr@0 == corr@best(0.9983 → 1.0000 单调上升)——是原地淡入,无位移。corr=1.0——落点就是内容末端。两条都要有才算数:单看「没跳」不够,一直停在错的地方也不跳。
守卫测试做了负向验证:新增的
positions once before the reveal even while streaming restarts the settle window在移除!didGateScrollRef.current后确实变红。顺带记进文档的两个易误读指标
estimate-collapse在前两个场景恒为 252px,不是漏修的跳动:全新话题里user与assistant-empty都还没有尺寸均值样本,一起回退estimatedItemSize=300,实测 56/48px。它发生在 t≈195ms、content=0、还没有过任何滚动事件的时刻,两行合计 104px 远小于 874px 视口——没有可滚动距离,修正不产生位移。follow-up-turn的viewport-blank从 38/40/40 涨到 45/59/58,不是回归:峰值全落在anchoring相位且数值 ≈endSpace/viewport,量的是预留空白本身;修复后钉顶到位更快(场景总时长 ~10s → ~8s),采样于是落在预留空白还没被填掉的更早时刻。三场景稳态仍全为 0%。但阈值是 60%,余量已从 20 个百分点缩到 1.4 个,文档里记了下次的判别方法。Gate
packages:build+typecheck+test:app(310 套件 / 2897 测试全绿)+lint(改动文件 0 警告)+format+docs:check-links。🤖 Generated with Claude Code