- A tail request counts only for closing rows that can start a batch, using the same membership rule as the batch, and detaching clears it. - A batch keeps the reader landing while reading movement awaits sampling, so an unsampled scroll away from the tail is not undone. - Reader scrolling during a reader landing moves the reference row's recorded position, so the final correction keeps only layout changes. - The blank-area matrix releases a partly opened world, fails a case on cleanup errors, mounts only the provider catalog for the Turn that fails before its model call, names its gap as a layout gap, and labels the steering case as a no-fold control. - The note records the pending-sampling rule and the opacity of a row that joins the travelling set after fading.
8.6 KiB
Agent Note: 「下条消息时收起」只用一条折叠动画
Status: implemented
English | 中文
Problem
「何时收起工作详情」选「下条消息时收起」时,上一个交互段的过程行在下一条人类输入到来时收起。滚动层原本有两条折叠路径,按 ownInput(use-chat-scroll.ts)二选一:
- 输入折叠路径:折叠开始时测量收起行,算出最终落点和所需的底部补白,收起与滚动在同一个帧时钟上推进,一次落定。
- 通用路径:scrollTop 不动,把收起的高度整块预留为底部补白,折叠结束后再按跟随状态补滚一次。它只对折叠发生在读者下方的情况成立。折叠区在读者上方时,读者正在看的内容整体上移,视口里只剩补白。
ownInput 只在三种信号下成立:对话流里的本地回显(submissionId)、steer 回显(steeringId)、列表最后一行是用户消息(lastIsUser)。Host 写回开场输入的那次提交里,该轮的过程控制行排在输入之后,lastIsUser 不成立。于是,凡是不经过本页对话流回显的人类输入都走通用路径:
| 来源 | 本页回显 | 读者在底部 | 读者在正文中段 |
|---|---|---|---|
| 主会话输入框空闲发送 | 有,在对话流 | 正常 | 正常 |
| 主会话排队消息在上一轮结束后自动投递 | 有,但在排队区,不参与判定 | 整屏空白 | 整屏空白 |
| 另一个标签页发送 | 无 | 整屏空白 | 整屏空白 |
| 右侧栏子会话输入框发送 | 无(composer 跳过 beginSubmission) |
整屏空白 | 整屏空白 |
| 子会话里父消息唤醒多轮后人类发送 | 无 | 整屏空白(补白约 6000 px) | — |
通知唤醒的 Turn 已不再触发折叠(分组树位置与交互段);上表是仍经由通用路径折叠的人类输入。数据来自 fold-blank-matrix.e2e.ts。
子会话不显示本地回显,是因为引入回显时 subagents.prompt 不携带 request id,回显无法与子会话写回的消息对应。此后子 agent 控制迁到 Remote,SubagentPromptRequest.requestId 已被 Host 写入子会话用户消息的 source.rpcId;客户端 Session.prompt 却仍另行生成 id。
Decision
「下条消息时收起」只保留一条折叠动画:flow-motion.ts 把每一批收起交给 viewport 提供的 InputFoldDriver,use-chat-viewport.ts 在批次开始时选择两种落点之一。
批次
-
同一次提交中开始收起的行,连同同批开始展开的行,组成一个批次。驱动器的四步依次为:
begin:测量这些盒子,选定落点;prepare:在写盒子之前扩大补白,避免中间帧被浏览器截断;frame:同步写盒子高度和 scrollTop;finish:安装最终补白。
批次中途新增的内容按分段规则更新剩余运动。
-
只含展开行的批次不需要补白,仍用 CSS 过渡。viewport 未挂载阅读策略时,收起行也用 CSS 过渡;减少动态效果时立即生效。
-
收起行先提交 hidden,驱动器再安装最终几何,之后才通知 fold-idle 监听者(例如切换展示模式后清空补白)。
-
通用路径已删除:
reserve回调、pendingReserve与pendingGrowth的预留计算、reserveBelow、折叠结束后再跟随的followAfterFold。保留reclaimBelow(新内容到来或读者滚动后归还补白)与resetBelow(切换展示模式后清空补白)。
落点
记 start 为批次开始时的 scrollTop,above 为视口顶部以上收起行的高度减去展开行的高度,base = max(0, start − above),bottom 为折叠后的底部位置。
| 落点 | 何时 | 目标 scrollTop | 结束后 |
|---|---|---|---|
| 到底部 | 本客户端的自己输入,或批次开始时读者正在跟随底部、且没有尚待采样的阅读滚动 | max(base, bottom) |
提交跟随状态 |
| 读者内容不动 | 其余情况 | base |
保持读者的阅读状态,确认新位置 |
- 两种落点都只安装精确差额作为补白:
max(0, 目标 + 视口高 − 折叠后高度)。 - 「内容只上不下」:已经在视口顶部以上的内容不会回滑。保留的正文和新消息不足一屏时留出差额,新内容到来后填满。
- 批次进行中出现自己输入时,从当前位置开新段,改为到底部;即使阅读手势已取消自动目标,也由新输入接管。只有批次正在运行或已排定时才接受到底部请求;批次结束后仍在用 CSS 过渡收起的行,不接管自己的输入,交给普通跟随。
- 读者的滚动手势会取消「到底部」的自动目标,保留读者位置。「读者内容不动」时,帧间读者自己的滚动会并入批次保持的位置。
- 「读者内容不动」时,驱动器对有序的顶层行二分查找,选取视口里第一行不属于本批次的可见顶层行作为参照。批次开始和收起行提交 hidden 时,相邻行的间距会瞬间改变;驱动器在这两处测量参照行的位移,抵消到 scrollTop 上。
- 「到底部」且需要向前滚过视口顶部以下的收起行时,这些行保持不透明、随内容滚动,在收缩中裁剪显示,嵌套的收起行在批次结束前保持原尺寸。其余收起行原地淡出。新的到底部请求接管运行中的批次时,会把仍在视口顶部以下的收起行加入随行;已经淡出的行会以剩余高度重新变为不透明。这样向前滚动不会穿过一片已经透明、还没收完的空位。
自己输入的判定
| 来源 | 判定 | 落点 |
|---|---|---|
| 主会话输入框空闲发送 | 对话流回显 | 到底部 |
| 子会话输入框空闲发送 | 子会话同样注册本地回显;subagents.prompt 使用回显的 requestId |
到底部 |
| 运行中排队、上一轮结束后自动投递 | 不是自己输入 | 跟随底部时到底部,否则内容不动 |
| 另一个标签页发送 | 不是自己输入 | 同上 |
| 父 agent 给子会话发消息 | 通知不开启交互段,不折叠 | — |
「完成时收起」不在动画作用域内,折叠立即生效,不受影响。
Testing
fold-blank-matrix.e2e.ts 覆盖主会话 19 例与右侧栏子会话 8 例,维度包括读者位置(底部、正文中段、正文开头、步骤中间、上一轮)、输入来源(输入框、回合在写入消息前失败、另一个标签页、排队投递、steer、父消息、一次折叠多轮)、正文长短,以及 Standard 与「完成时收起」对照。布局空洞指视口顶部到输入框之间没有任何可见行的盒子覆盖的最大连续高度:落定后不超过 120 px,动画中至多 1 帧超过。它按盒子计算,已经淡出但尚未收完的收起行仍算作覆盖。「到底部」的用例断言落到底部;「内容不动」的用例断言读者锚定行的屏幕位置变化不超过 2 px。正文很短的用例只断言回复到达后不再留空。
Alternatives considered
保留通用路径,只修自己输入的识别。 两条路径的几何规则继续分叉;非自己输入时,中段读者要么被拉到底部,要么仍然看到空白。
通用路径改为按读者位置补偿,但与输入路径分开实现。 补偿与补白的几何代码重复一份,两边容易再次不一致。
用开启交互段的人类输入节点识别自己输入,子会话不补回显。 另一个标签页的输入会被当成自己输入,把中段读者拉到底部;子会话发送后仍然不能立即看到自己的消息。
允许内容回滑,以填满短正文留下的差额。 同一次动画里内容先上后下。
Consequences
- 收益:整屏空白的来源(通用路径)不再存在;落点只看「是不是自己的输入」和「读者是否在跟随」,与消息从哪个页面、何时投递无关。子会话发送立即显示自己的消息。
- 代价:正文很短时,「内容只上不下」仍会留出差额,直到新内容填满。
- 焦点停在正在收起的行里时,焦点保护会让该行保持展开。若它是被封顶高度的过程组,正文会立即缩到封顶高度。这次缩小发生在批次开始之前,「读者内容不动」的落点测不到,读者内容会移动一次。