deepseek-harness/.agents/notes/implemented/feature/2026-08-25-loaded-turn-chat-navigation.zh.md

5.9 KiB
Raw Blame History

Agent Note:已加载 Turn 的聊天导航

Status: implemented

English | 中文

问题

较长的 Chat transcript 需要反复滚动才能回看更早的 Turn。Session 历史采用分页加载,因此浏览器可能只持有会话后缀,首个已加载 Turn 也可能从用户消息之后开始。如果导航暗示自己知道未加载的 Turn,或者按当前数组位置给刻度设置 key,Session Controller 前插上一页 event 后,导航就会产生误导或变得不稳定。

决定

Chat snapshot 构建层为当前已加载且含可见 transcript node 的每个 Turn 累积一项导航。每项使用 Turn 编号作为稳定的 React key,并以首个已加载用户 node 为滚动锚点;没有用户 node 时回退到该 Turn 的首个已加载 node。这只是已加载 Chat 状态的纯投影:本功能不新增 Session event、持久化索引或分页请求。

累积而非渲染期扫描:结构性 upsert 重算已加载 Turn 集合,仅内容变化的 upsert 只重算受影响 Turn,每条预览截断到 160 字符,导航状态因此不会持有 transcript 副本。发布的数组在条目未变时保持引用不变,ChatView 直接选取它,既作为轨道数据也作为变化信号——渲染层不遍历已加载窗口,流式回复的预览也跟随节点原地更新,而不是停在上一次结构性发布。

导航轨道以 10px 自然间距渲染完整的已加载 Turn 集合,永不显示省略号或未加载历史占位。集合较小时轨道随内容收缩;已加载集合超过可用高度后,百分比位置会把所有刻度压缩到设定上限内。更早一页到达后,已有 Turn 的 key 和 DOM 元素保持不变,最终位置随之变化;CSS transition 为这次重排添加动画。如果一个 Turn 被分页边界截断,预览最初显示其 Turn 编号与已加载的助手回复,上一页补齐后再显示用户问题。

轨道紧贴滚动视口右缘,并在粘性输入区之外的可见区间内垂直居中。该区间等于滚动视口自身高度减去输入区高度,因此 ConversationRoot 在同一元素上除已有的 --dsh-composer-height 外再发布 --dsh-conversation-viewport-height,轨道按两者之差居中,而不是按忽略 Session 头部的视口高度居中。

活跃刻度跟随共享 Chat 滚动区顶部附近的阅读线。跟随底部的 frame 会先按滚动距离选中最后一个已加载 Turn,不读取任何行几何;流式输出及其他被 observer 捕获的高度变化因此无需命中测试或扫描即可追随底部。离开底部后,每个滚动 frame 用一次命中测试解析阅读线所属 Turn,布局无法作答时退化为一次行扫描,成本不随刻度数量增长。不产生滚动事件却让行跨过阅读线的高度变化通过既有的 column observer 重新同步。滚动更新由 requestAnimationFrame 合并。激活刻度会在现有滚动坐标系中计算目标 node 的位置,移动同一个滚动区,并记录由此产生的 Chat 滚动恢复锚点。

即使密集刻度在视觉上重叠,每个 Turn 仍是可访问的按钮。轨道把指针高度映射到最近的已加载 Turn,键盘聚焦和激活则作用于各个按钮。悬停或聚焦显示紧凑的问题与回复预览,活跃刻度更长、更深;Chat 容器宽度不超过 900px 时隐藏轨道,用户偏好减少动态效果时关闭重排和刻度入场动画。

曾考虑的替代方案

**在已加载 Session 页之外持久化完整 Turn 索引。**否决:当前客户端必须先物化历史记录,才能导航到未加载的 transcript 锚点;第二套索引还会重复 Session 投影状态。

**为未加载历史显示省略号。**否决:分页只暴露 hasMore,不提供更早 Turn 的数量或分布,因此省略号不是可操作的目的地。加载一页并重排真实的已加载集合,才能精确表达当前可导航范围。

**始终把刻度铺满可用高度。**否决:已加载集合较小时,各刻度会被大片空白隔开,在视觉上失去关联。固定自然间距保持紧凑索引,百分比压缩仍能容纳密集历史。

**在渲染层从 Chat snapshot 派生轨道。**否决:渲染层不扫描已加载的 Chat Nodes(见 client 纪律)。渲染期投影还会在每次结构性发布时重新复制每个 Turn 的问题与回复文本,且看不到流式回复的节点原地更新,预览会停在首个 chunk。

**按已加载数组位置给刻度设置 key。**否决:前插一页会让每个 DOM 元素改为代表另一个 Turn,丢失焦点与预览身份,也无法让已有刻度移动到新位置。

**对 Turn 行调用 scrollIntoView。**否决:Chat 拥有共享滚动区、底部跟随状态、分页锚点与持久化恢复坐标。浏览器的黑盒滚动会绕过这些状态更新。

后果

桌面宽度的 Chat 视图可以在当前所有已加载 Turn 之间跳转,并在不展开 transcript 内容的情况下查看短预览。分页会前插新目的地,但不会虚构覆盖范围或重新挂载已有刻度。分页边界截断 Turn 时,首个已加载刻度可能暂时缺少问题;在上一页补齐数据前,Turn 标签仍可用于导航。如果未来 transcript 虚拟列表会卸载已加载锚点,导航需要在滚动前执行显式物化操作,而不应改变这里的已加载 Turn 投影。

测试

构建层测试固定累积投影、预览截断,以及原地 chunk 更新后的预览新鲜度。组件测试固定已发布条目、可访问预览、滚动坐标跳转、DOM 身份、前插后的百分比重排,以及拒绝任何行几何读取的底部跟随 ResizeObserver 更新。长交互 Chromium 场景固定真实分页边界、加载更早 后补齐问题、稳定刻度移动、键盘激活、活跃状态更新与窄容器隐藏。多 Turn 的 Web 录制快照包含导航 landmark 和按钮。