# Agent Note：已加载 Turn 的聊天导航

Status: implemented

[English](2026-08-25-loaded-turn-chat-navigation.md) | 中文

## 问题

较长的 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 滚动区顶部附近的阅读线。每个滚动帧用一次命中测试解析该行所属 Turn，布局无法作答时退化为一次行扫描，成本不随刻度数量增长。图片加载、工具卡展开等不产生滚动事件的高度变化，通过既有的 column observer 重新同步。滚动更新由 `requestAnimationFrame` 合并；到达底部时选择最后一个已加载 Turn。激活刻度会在现有滚动坐标系中计算目标 node 的位置，移动同一个滚动区，并记录由此产生的 Chat 滚动恢复锚点。

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

## 曾考虑的替代方案

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

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

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

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

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

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

## 后果

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

## 测试

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