Skip to content
dsh.fish
Bundle

dsh-session-toc

A floating right-side table of contents (TOC) for the DeepSeek Harness Web UI: one entry per user question, click to scroll to that message.

Source
notload
stars
2 stars
License
MIT
Updated
Updated 4 days ago

Readme

# dsh-session-toc

<p align="center">
  <a href="https://www.npmjs.com/package/dsh-session-toc"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-session-toc?label=npm&color=blue"></a>
  <a href="https://www.npmjs.com/package/dsh-session-toc"><img alt="npm monthly downloads" src="https://img.shields.io/npm/dm/dsh-session-toc?label=月下载&color=brightgreen"></a>
  <a href="https://github.com/notload/dsh-session-toc/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/github/license/notload/dsh-session-toc?color=orange"></a>
  <img alt="platform" src="https://img.shields.io/badge/platform-DeepSeek%20Harness%20Web-8A2BE2">
</p>

> 右侧会话目录(Table of Contents)插件,为 DeepSeek Harness Web UI 的每个会话页加一个**右侧中间常驻、可折叠**的目录栏:把会话里的每个「用户提问」作为一条目录项列出,点一下尽量把会话滚动到对应消息,并高亮当前条目。
>
> 对应 DeepSeek 官网网页版里"右侧目录索引,点一下跳转到对应内容"的能力。

## 特性

- **常驻右侧**:`shell.overlay` 浮动层,垂直居中靠右,默认 click-through,条目可交互。
- **每问一条**:目录条目来自**完整会话日志**(host 端 `sessionQuery.readSession`),覆盖整段会话(含早期未加载进内存的消息)。
- **点击跳转**:点目录项用 DSH 节点稳定 key(`data-chat-anchor-key`)精确定位并滚动;早期消息自动 `loadOlder` 补加载后定位;定位不可靠时降级为"仅高亮当前条目"。
- **可折叠**:可收起成右侧一个窄条按钮,再点展开。
- **样式自切换 + 主题跟随**(`data-dsh-toc-theme`):目录栏**明/暗皮肤三档**`auto`(默认,跟随 DSH 全局主题,读不到则退化系统 `prefers-color-scheme`)/ `light` / `dark`,通过卡片顶部**齿轮(设置抽屉)**一键切换;并可调节**背景不透明度**(只影响背景、文字始终不透明)。选择持久化到 `localStorage`(`dsh-session-toc.theme` / `dsh-session-toc.bgAlpha`),跨会话、刷新保留,独立于 DSH 全局主题。
- **自动隐藏**:当前会话的用户提问不足阈值(默认 3 条)时目录栏不显示,避免噪音。
- **长会话友好**:目录列表超过 200 条时启用固定行高虚拟列表,只渲染可见窗口 ± 缓冲,避免长会话撑大 DOM 内存;条目数据全量保留,点击定位不受影响。
- **切换不卡顿**:宿主侧做「进程内 LRU 缓存 + 落盘索引」双层降频,切换会话不再反复触发全量 `readSession`(磁盘读 + 深拷贝 + 全量 replay);persisted 会话按 revision 失效、live 会话按消息序号增量失效。
- **重启秒开**:落盘索引(`$DSH_HOME/storages/session-toc/`)跨进程/重启保留,重启后切换会话也能秒开。
- **触发性更新**(issue 本轮):监听会话快照,**新提问 ~1.2s 内自动进入目录**(本地即时插入 + 后台全量矫正),不再等切换/手动重载。
- **美术细节**(issue#9):悬浮圆角化(卡片四角 12px + 右侧 8px 不贴屏、dock 独立圆钮、active 左侧强调条、折叠窄条圆角);设置抽屉化(控制条只留齿轮,popover 收纳主题/透明度/重载,ESC 关闭);可拖拽定位将在下一版本(v0.1.10)加入。
- **零侵入**:不改动任何 `@deepseek-ai/*` 内置包,仅以 bundle 插件方式挂载。

## 安装

需要本机已安装 `pnpm`,且 DSH 的 web profile 存在(如 `~/.dsh/profiles/web`)。

### 方式一:从 GitHub 克隆后以 link 方式安装(推荐)

```bash
git clone https://github.com/notload/dsh-session-toc.git
cd dsh-session-toc
dsh plugin --profile web add link:$(pwd)
```

> 如果 `$(pwd)` 在你的 shell 里不生效,直接写完整的绝对路径即可,例如 Windows:
> `dsh plugin --profile web add link:C:\Users\<你的用户名>\dsh-session-toc`

### 方式二:安装已发布版本(若已发布到 npm)

```bash
dsh plugin --profile web add dsh-session-toc
```

安装后会写入 profile 的 `package.json` 的 `dependencies`,并自动进入 `dsh.profile.bundles` 层栈(`dsh plugin` 会自动 reconcile)。

确认进入层栈:

```bash
dsh plugin --profile web list
```

然后**重启** `dsh web`,浏览器访问同一 URL,即可在会话页右侧看到目录栏。

## 配置

目录参数通过 `cordis.patch.yml` 传给 host 半侧(`apply` 的 `config`)。注意:DSH 客户端配置管线当前尚未打通,浏览器半侧收到的 config 是空对象,因此以下参数目前以**代码默认值**为准(与 `dsh-pet` 同样的限制)。

配置项(对照 `lib/client.js` 的默认值):

| key         | 默认值 | 说明 |
|-------------|--------|------|
| `minEntries`| `1`    | 当前会话用户提问少于该值时隐藏目录栏 |
| `maxChars`  | `48`   | 单条目录文案的最大字符数(超出加省略号) |
| `collapsed` | `false`| 初始是否折叠 |

> 当前这些值在 client 里不可被用户覆盖。若需要可配置,需等 DSH 打通客户端配置管线,或改为硬编码默认值。

## 行为与限制

### 目录条目来源
- 条目来自**完整会话日志**:浏览器侧请求 host 端 `/session-toc/questions?sessionId=…`,host 用 `ctx.sessionQuery.readSession` 读取整个会话日志并提取全部 `user/message` 事件,再返回给浏览器生成目录。
- **覆盖整段会话**:不依赖浏览器"已加载到内存"的节点。DSH 会话视图是按需加载的(有"加载更早"按钮,早期消息默认不在内存),但目录仍能列出全部历史提问。
- **图片/文件配文字时用文字**:如果一条用户消息**带有文字说明**,目录文案直接用文字(忽略图片/文件名);只有**纯图片/文件、无任何文字**时,才用文件名代替(`🖼 文件名`,无文件名用媒体类型兜底)。这样就不会出现"文字说明 + 文件名"混杂的目录条目。
- **排除上下文注入**:只保留 `source.kind === 'user'` 的事件,`session-reference`、`workspace` 等注入式上下文不会进目录。
- **不受 DSH 上下文压缩影响**:目录始终由 host 读取**磁盘上的完整会话日志**(`readSession` / 落盘索引)生成;DSH 上下文压缩(thresholdRatio/retainRatio 的早期消息摘要化)只改变主会话的"记忆",不动日志压缩后目录条目完整无损。

### 加载失败降级(长会话 / 格式不兼容)
- host 读取失败(如会话日志含本 harness 不认识的 `background-agents/*` 新事件类型,报 `SessionFormatUnsupportedError`)时,返回结构化错误码(`SESSION_FORMAT_UNSUPPORTED` / `SESSION_READ_FAILED`)。
- 前端收到失败后**不再静默清空**:会**降级为"浏览器已加载节点"的目录**(部分可用),并在目录里显示"目录加载失败(已显示部分已加载内容)+ 重试"提示条。
- host 对读取结果做「进程内 LRU 缓存 + 落盘索引」双层降频:
  - 进程内 LRU 缓存(上限 64 会话 + 8MB 总字节预算)命中直接返回已序列化 JSON;长对话多个大缓存共存时按字节预算再多淘汰,兜住进程堆内存;
  - 落盘索引(`$DSH_HOME/storages/session-toc/<id>.json`)跨进程/重启保留,仅对 persisted 会话生效;
  - 失效信号:persisted 会话按 `revision`(`sessionPersistence` 的 stat 级变更 token),live 会话按消息序号(lastSeq)零 IO 增量校验。
  - 这样切换会话 / 重启后都不再反复触发 `readSession`(磁盘读 + 多次深拷贝 + 全量 replay 校验);失败结果不缓存。
- 安全:路由不携带 CORS 头(由浏览器同源策略拦截跨源读取);并校验请求携带浏览器来源信号(`sec-fetch-site` 或同源 `Origin`),curl/脚本/局域网主机等裸请求一律 403;sessionId 还需属于当前 host 可见会话(live 或 persisted),否则拒绝;错误响应不回显内部错误详情。

### 滚动定位
- DSH 会话视图的每个 chat 节点在 DOM 上有稳定属性 **`data-chat-anchor-key`**(= 节点的 engine 稳定 key)。本插件优先用它来**精确定位**:点击条目时 `document.querySelector('[data-chat-anchor-key="<key>"]')` 找到节点并 `scrollIntoView({block:'center'})`。这比文本/图片匹配可靠得多,图片消息也能准确定位(图片节点同样有该属性)。
- 节点 key = `conversationContextKey("input-message", userMessageId)` = `"13:input-message" + id`,其中 `id` 是 host 从会话日志取出的用户消息 id。
- **早期消息自动补加载(渐进、不卡顿)**:DSH 会话是虚拟列表(每页约 50 条),早期消息默认不在 DOM。点击条目时若**目标已加载则立即定位(秒回)**;若需补加载,插件**先把视图滚到当前已加载的最早边界**给即时反馈,再逐页调用 `session.loadOlder()`,**每页之间用 `requestAnimationFrame` 让出主线程**(不再用 `requestIdleCallback` 排队叠加,避免浏览器忙碌时越卡);guard 按目标距离估算(`estimateLoadOlderPages`,封顶 40 页)。浏览器 console 会打印 `[dsh-session-toc] jump {targetSeq,pages,ms}` 供量化。
- 回退:节点始终未找到(如会话正在运行、`loadOlder` 不可用)或拿不到 `id` 时,降级为文本匹配;仍失败则仅高亮条目。

## 开发与测试

```bash
npm test   # 等价于 node --test --test-isolation=none --expose-gc(全量)
```

- **内存/泄漏探针的判据依赖强制 GC**:`host-memory` / `memory-leak-probe` 用「双 GC + 线性回归/中位数」区分真泄漏与低 GC 频率机器的堆未回收。测试文件自带兜底不带 `--expose-gc` 直接跑(如 `node --test test/memory-leak-probe.test.js`)会自动以强制 GC 重跑自身,任何跑法下判据都有效,不会误报红灯。
- `memory-leak-probe` 除回归判据外,还有一条「FinalizationRegistry 正面证明」用例:LRU 淘汰的对象必须可被 GC 回收,直接证明缓存无引用残留。
- 本机跑法注意:Windows 下 test runner 需 `--test-isolation=none`(否则 spawn EPERM)。

## 目录结构

```
dsh-session-toc
├── package.json          # bundle + client 声明
├── cordis.patch.yml      # 挂载声明(insert 行)
├── lib
│   ├── index.js          # host 半侧占位插件
│   ├── client.js         # 浏览器半侧:右侧目录栏 UI + 滚动定位
│   └── types
│       ├── index.d.ts    # host 侧类型
│       └── client
│           └── index.d.ts # 浏览器侧类型
└── README.md
```

## License

MIT © notload

---

如果这个插件对你有帮助,欢迎点个 ⭐ **Star** 支持一下~

Install

dsh plugin --profile web add github:notload/dsh-session-toc

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source