# dsh-strata · 会话地层

[![npm 版本](https://img.shields.io/npm/v/dsh-strata?logo=npm&color=cb3837)](https://www.npmjs.com/package/dsh-strata)
[![开源协议](https://img.shields.io/npm/l/dsh-strata?color=blue)](LICENSE)
[![已收录 awesome-dsh-plugin](https://img.shields.io/badge/awesome--dsh--plugin-%E5%B7%B2%E6%94%B6%E5%BD%95-6f42c1)](https://awesome-dsh-plugin.com/p/jsdvjx/dsh-strata/)

[English](README.md) | 中文

**给 DeepSeek Harness Web GUI 的会话轨迹缩略图** —— 它接管对话区原本的滚动条位置，
把整段已加载的会话按真实比例压成一张按事件类型着色的图，你自己的消息重点标出，旁边还有一列可点的锚点。

它不是导航条。别的会话导航插件都是"一条提问一个等距圆点"，而这恰好丢掉了长会话里唯一过剩的信息：
**体量**。这里每个色块的高度是那一行**真实渲染高度**的等比压缩，所以地图就是滚动区的缩影 ——
三百行的回答看起来就是三百行，四十次工具调用看起来就是四十次，视口方框和滚动条 1:1 对应。
一眼就能看出这次跑动的形状：你在哪儿说了话、每一轮花了多少工作量、哪里出了问题。

<p align="center">
  <img src="https://raw.githubusercontent.com/jsdvjx/dsh-strata/main/docs/demo.gif" alt="实录演示：悬停展开轨道并点亮所指色块，点击色块或锚点跳转，拖动视口框刮擦整段会话" width="840">
</p>

## 图上画了什么

| 色块 | 含义 |
|---|---|
| **整宽蓝条** | 你的消息（含插话）—— 永远最宽最亮，最小 5px，绝不消失 |
| 灰色块 | 模型回复，块高就是它写了多少 |
| 细灰刻度 | 一次工具调用 |
| 绿色 | 斜杠命令 |
| 琥珀色 | 模型重试，或被输出上限截断的回合 |
| 红色 | 失败的回合 —— 以及任何报错的工具/命令行 |
| 横线 | 上下文压缩点：模型从这里往上就看不见历史了 |
| 圆角框 | 当前视口（可拖动） |

左边缘的锯齿就是索引：每条蓝杠是你发起的一轮，它下面那坨 agent 工作就是这轮的代价。

## 它替代了滚动条

轨道就落在对话区自己的滚动条槽位里，地图在时原生滑块被抑制 —— 只有一个滚动控件，不是两个；
槽位本来就一直保留，所以没有任何布局抖动。这是接管而不是霸占：地图一旦隐身（轨迹页签、没有会话、
对话根本不滚动），原生滚动条立刻回来；卸载插件则永久还原。

## 也替代了旁边那条轮次导航

DSH 0.1.2-rc.1 起自带了一条导航：对话区右缘一条等间距的轮次刻度轨。那正是这张地图占的那条竖条，
两者会叠在一起，原生那条还压住锚点圆点。地图对它的处理和对滚动条一样：地图在时把它隐藏，
地图一隐身（轨迹页签、没有会话、对话根本不滚动）立刻还回去。两边都不会引起重排 ——
那条轨道是零高度 sticky 槽里的绝对定位元素，隐藏它不占布局。

想两条都要？

```js
localStorage['dsh-strata.native-turn-rail'] = 'keep'
```

刷新即可保留原生轮次导航。没有这条导航的 DSH（0.1.1-rc.2 及更早）本来就没得隐藏，行为不变。

## 锚点

轨道左侧是一列可点的锚点圆点 —— **蓝色是你发过的每一条消息，红色是每一次失败的工具调用或命令**。
点一下就滚过去。当前阅读位置对应的那个点会保持放大，所以锚点同时也是位置指示器。
挨太近的锚点会自动合并以保持这一列清爽；失败锚点永不合并——它通常正是你打开地图的原因。

## 线索墙

**悬停用户锚点圆点**，线索墙浮现在屏幕右半侧：**这个会话里你说过的每一句话，
包括还没加载进窗口的**（读取会话自己的导出日志）——像证物板一样拼贴：
每张卡按自己内容裁出刚好合适的框（一句话是小纸条，长段落是大纸片），瀑布流装箱，
能装下就一板放完整个会话。每张卡一条贝塞尔连线接回自己的锚点圆点，对话滚动时连线
实时跟随；未加载的用虚线框、"未加载"标记和虚连线，你进来时对应的那条高亮聚光。
只有真装不下时，顶部和底部才出现 **↑/↓ 翻页按钮**。**点击卡片**跳转——未加载的先自动
连锁载入缺失历史再落位。指针离开面板按宽限期收起，**Esc** 立即关闭；点击圆点则
直接跳转到那条消息。

## 地图尺度

轨道苏醒时，下方出现一枚 **近 / 中 / 全** 切换药丸：

- **近**：还原会话刚打开时的视野——之后无论加载了多少历史，地图保持这个尺度，
  窗口随阅读位置滑动；
- **中**：两倍初始视野，差的部分按需补载；
- **全**：映射整个会话——有未加载历史时先自动补载（复用轨道进度条）。

选择按浏览器持久化。缩放档位的尺度是钉死的：加载更多历史只移动窗口，不改比例。

## 用法

- **点锚点圆点**：跳到那条消息或那次失败。
- **悬停**轨道：轨道变宽，光标所指的色块亮起；指到你自己的消息时线索墙打开（锚点圆点的提示里有类型和 `第 n/总数`）。
- **点击**色块：滚动到该行并停在阅读位置，落点会闪一下。
- **点空白处或拖动**：像滚动条一样按比例滑动。
- **滚轮**：在轨道或锚点上滚动即滚动对话。
- **键盘**：轨道可聚焦——方向键微调、PageUp/PageDown 翻页、Home/End 跳到两端
  （`role=scrollbar`，`aria-valuenow` 实时跟随）。
- **双击**：钉住展开状态（按浏览器持久化）。
- 旧历史自动加载：滚动（或拖视口框）到顶部就会触发对话自己的"加载更早"，地图随之重新缩放。
  轨道上方淡淡的 `⌃` 只是提示"上面还有"，全部载入后自动消失。

没东西可导航时地图自动隐藏：没有会话、对话根本不滚动、或当前是"轨迹"等非对话视图。

## 兼容性

| DSH | 状态 |
|---|---|
| 0.1.2-rc.1 | 已实测 —— 轮次折叠、轮次导航、正文宽度拖柄、语言切换都已对上（见下） |
| 0.1.1-rc.2 | 已实测 —— 不依赖任何 0.1.2 才有的东西，每个接缝都能退回旧行为 |

0.1.2 带来了什么，地图怎么应对：

- **轮次折叠**（精简模式原地隐藏已完成轮次的过程行）：折起的那一帧行就从图上消失，展开的那一帧回来。
- **轮次导航**：地图在时隐藏，地图隐身时还回去（`localStorage['dsh-strata.native-turn-rail'] = 'keep'` 可两条都留）。
- **正文宽度拖柄**在笔记本宽度下正好压在锚点圆点列下面：这 14px 归地图所有，两个圆点之间的空隙不再是拖柄。
- **语言切换**：文案走宿主的 locale 服务，在「设置 → 通用设置」切语言即时生效不用刷新；语言包可以把 `dsh-strata` 命名空间翻成第三方语言。没有该服务的宿主退回内置中英表（按文档语言）。
- **分页按钮生命周期**：0.1.2 在加载一页期间会卸载「加载更早」按钮，加载链会等它回来而不是判定历史已到头；进展按落地的行数算，不看滚动高度。

## 安装

```sh
dsh plugin --profile web add dsh-strata
```

或直接装仓库版：

```sh
dsh plugin --profile web add "github:jsdvjx/dsh-strata#main"
```

然后重启 `dsh web`。卸载：

```sh
dsh plugin --profile web remove dsh-strata
```

## 实现

纯浏览器半边，Node 半边是空的，不为它走任何会话数据通道。几何和语义都来自对话视图本就公开的锚点 ——
`[data-conversation-scroll]`（滚动容器）、`[data-chat-anchor-key]`（每个流式行 —— 取**最外层**那个：
工具渲染器会在行内再嵌一个自己的 `call:<id>` 锚点，那是这一行的一部分，不是另一行）、
`data-chat-flow-kind`（该行注册的 Chat Node 类型）、`data-state="error"`（失败的工具/命令行）、
行上的 `hidden`（DSH 0.1.2 精简模式下折叠的轮次过程）、`[data-composer-seat]`（避开吸底输入框）。它只往全局 `shell.overlay` 列表插槽里加一个条目，
是**加**一个面而不是替换任何面，卸载后原生 UI 分毫不动。文案以 `dsh-strata` 命名空间注册进宿主的 locale
服务、经它的 `t` 翻译，所以随界面语言即时切换。滚动条接管走主题自己写明的口子 ——
在滚动容器上把 `--dsh-scrollbar-thumb` 重绑为 `transparent`（ui-sidebar 用的就是这个机制），
WebKit 与 Firefox 两条渲染路径都覆盖，不覆写任何样式表。

渲染走 canvas + rAF：只有对话结构变化时才重新量行 —— 流式回复长高由该行自己的 ResizeObserver
报上来只改一个色块，纯滚动只移动视口方框，地层本身缓存成一层底图，悬停只重画一个色块而不是整段会话。颜色全部读主题自己的 `--dsw-alias-*` token，明暗主题都对；
`prefers-reduced-motion` 下关闭过渡动画。

## 边界

- **地图只覆盖已加载的窗口。** DSH 的历史是按需分页加载的，没加载的部分没有布局可测。顶端的 `⌃`
  就是"上面还有"的诚实提示，滚到顶部它就会自动被拉进来。
- **只作用于对话视图。** "轨迹"页签用的是另一套事件锚点，地图在那里主动隐身，不去猜。
- 轨道占用对话区右侧内边距约 14px 的竖条，这条竖条上的点击会落到地图上。
- 隐藏原生轮次导航是认它自己那张样式表里声明 `--turn-rail-band` 的那条规则。哪天 DSH 不再以这个
  名字发它，结果是那条导航回来，而不是地图坏掉。

## 开发

```sh
npm test                      # node --test 跑无 DOM 的内部逻辑
node test/replay/replay.mjs   # 无头 Chrome 对着一个活的 `dsh web` 回放（见文件头）
```

几何、缓存和历史加载状态机以 `internals` 从 `client.js` 导出，不用浏览器就能测；回放脚本通过
Chrome DevTools 协议驱动一个真实会话（滚动一致性、拖动、线索墙分页、加载链、会话切换）。挂载后的
地图在根元素上留了一个只读缝 `root.__dshStrata`（量出来的色块、观察集合、热路径计数）给回放用 ——
是测试钩子，不是 API。回放时让 DSH 页签保持前台：后台页签拿不到动画帧，地图会如实地显示为已隐身。

## 许可

MIT
