# dsh-nested-followups

[English](README.md) | 中文

![dsh-nested-followups —— 为 DeepSeek Harness 提供的嵌套追问会话树](https://raw.githubusercontent.com/sluminositys/dsh-nested-followups/main/assets/banner.png)

这是一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
插件，为其网页界面增加会话树。你可以针对此前的任意一条回答提问，新的问题会作为
一条独立分支展开，而不是被追加到主对话的末尾。

[![许可证: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.19-brightgreen.svg)](https://nodejs.org)
[![npm](https://img.shields.io/npm/v/dsh-nested-followups.svg)](https://www.npmjs.com/package/dsh-nested-followups)
[![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-0.1.1--rc.2-orange.svg)](https://github.com/deepseek-ai/deepseek-harness)

## 要解决的问题

DeepSeek Harness 的对话是线性的。当你正在完成一项工程任务，想问清楚早先某条回答
里的一个概念时，只有两个都不理想的选择：在主对话里直接问，这会把无关内容混进任务
上下文，并且此后每次请求都会继续携带它；或者新开一个对话，这又会丢掉那些让这个
问题有意义的上下文。

本插件提供了第三种选择：把追问变成一条分支。分支只继承到你所提问的那条回答为止的
对话内容，不包含其他任何东西，主对话也完全不会看到分支里发生的事。

## 功能

- **把当前对话显示为树。** 每条用户消息和助手回答各自成为一张卡片。主对话向下延伸，
  分支向右生长。
- **可以无限层追问。** 分支里的回答可以再次被追问，嵌套层数没有限制。
- **真正的上下文隔离。** 每条分支都是用官方的会话分叉机制创建的独立会话，而不是靠
  提示词要求模型"忽略某些内容"。
- **分支只读。** 分支可以读取工作区，但不能修改，因此追问绝不会干扰主对话里正在
  进行的工作。
- **大树可渐进折叠。** 每个锚点可收成一枚点位，点开后每条分支一枚胶囊，再逐级
  展开，深层树也能保持可读。折叠中的分支照常生成，并显示活动标记。
- **不改动原有界面。** 原生的对话视图、侧边栏和消息渲染都保持不变，分支也不会出现在
  会话列表中。
- **复用模型服务已缓存的上下文。** 分支发出的请求前缀与主对话一致，因此无需重新
  读取继承来的历史。

## 环境要求

| 项目 | 版本 |
| --- | --- |
| DeepSeek Harness | `0.1.x`（已在 `0.1.0-rc.7`、`0.1.0-rc.8` 和 `0.1.1-rc.2` 上验证） |
| Node.js | 22.19 及以上 |
| 包管理器 | pnpm |

## 安装

```sh
dsh plugin --profile web add dsh-nested-followups
```

如果 DeepSeek Harness 的网页服务已在运行，请在安装后重启。

<details>
<summary>改用源码安装</summary>

```sh
git clone https://github.com/sluminositys/dsh-nested-followups.git
cd dsh-nested-followups
pnpm install
pnpm run check
dsh plugin --profile web add .
```

</details>

卸载插件：

```sh
dsh plugin --profile web remove dsh-nested-followups
```

卸载只会移除插件的界面和服务，不会修改你的主对话，也不会删除分支的历史记录。

## 使用方法

照常打开一个对话，然后点击对话顶栏的 **Tree View**。在 **Chat** 和 **Tree View**
之间切换只改变对话的显示方式，不会复制或转换任何数据。

进入树视图后：

1. 把鼠标移到一条已完成的助手回答上，点击 **Ask follow-up**；
2. 输入你的问题。它会作为一张卡片出现在所提问回答的右侧，回复在其下方生成；
3. 想在这条分支里继续对话，就在该分支最新的回答上点击 **Continue this branch**；
   想再隔离一层上下文，则再次点击 **Ask follow-up**；
4. 点击任意卡片可以阅读完整消息。节点较多时，可以使用搜索、聚焦、折叠、缩略图和
   缩放控件来浏览。

想继续主任务时，随时切回 **Chat**。

### 折叠较大的会话树

首次打开一棵树，看到的就是最小形态：主对话加上每条有分支的回答旁的一枚点位。
点击 **⊕** 后出现每条分支各自的胶囊，再点击某枚胶囊才恢复该分支的消息卡片。打开
点位永远从胶囊列表开始，每一层都由你亲自选择展开哪条；其余胶囊保持折叠。想把卡片
组收回胶囊，把鼠标移到虚线框内侧底边，点击浮现的上箭头阴影区；点击 **⊖** 可把整组
收回点位。

按住 Alt 点击 **⊕** 或胶囊，可以一次深度展开其全部后代；工具栏的 **一键全收** 会把
所有一级锚点组收成点位。重启后按当前会话恢复布局。折叠状态下，蓝色脉冲表示后代仍在
生成，红色表示其中有失败；搜索命中折叠内容时会自动逐级展开祖先链并定位消息。

### 两个动作，两种含义

这两个动作的区别不只体现在外观上，也体现在数据结构中：

| 动作 | 生长方向 | 效果 |
| --- | --- | --- |
| **Ask follow-up** | 向右 | 新建一条分支，继承到所选回答为止的对话内容 |
| **Continue this branch** | 向下 | 在当前分支中追加下一轮对话 |

**Ask follow-up** 绝不会向已有分支追加内容，**Continue this branch** 也绝不会新建
分支。后者只出现在某条分支最新的已完成回答上，主对话中不会出现。

### 删除分支

删除一条分支时，它下面的所有分支也会一并删除。确认对话框会说明将要删除多少条分支
和多少条消息。主对话和同级的其他分支不受影响。

## 工作原理

### 分支隔离

每条分支都是一个真实的 DeepSeek Harness 会话，从其上级对话的一个完整回合处创建。
以从回答 A2 创建的分支为例，它继承从对话开头到 A2 为止的全部内容；主对话在此之后
产生的内容不会进入这条分支，分支里的内容也不会进入主对话。从同一条回答创建的多条
分支之间同样互不可见。

分支会被标记为子代理来源的会话。这样既能让它们不出现在会话列表中，又完整保留了各自
的历史记录——分支从属于它的主对话，而不是变成一个需要你单独管理的条目。

### 只读执行

分支以只读方式运行：可以查看工作区，但不能改变其中的任何内容。

这一限制是在工具真正执行时生效的，而不是通过对模型隐藏工具来实现。允许使用的工具
如下：

`read`、`read_image`、`glob`、`grep`、`lsp`、`session_*` 系列查询工具、`job_list`、
`job_output`、`terminal_list`、`terminal_read`、`list_agents` 和 `get_goal`。

其余一律拒绝，包括插件不认识的任何工具，因此新增的工具不会因为遗漏而被放行。代码
模式的 `run_code` 仍然可用，因为程序调用的每个工具都会被逐个检查，其中的写入操作会
被逐个拒绝。

放行读取是有意为之：追问常常就是"这个文件是做什么的"。而写入不能放行，因为分支与
主对话使用同一个工作目录，否则分支可能在任务进行过程中修改文件。

### 复用模型服务的缓存

插件不会改写分支发出的请求。分支会沿用其上级会话所使用的配置组合，并且完全不改动
工具定义、提示词段落和呈现格式。因此，分支请求的开头部分与主对话在分叉点处的请求
逐字节一致。

这直接关系到响应速度。模型服务会缓存请求的前缀，而工具定义正好位于请求的最前面。
哪怕只移除一条工具定义，开头的字节就变了，整段缓存随之失效，模型服务必须重新读完
继承来的全部对话，才能给出回答的第一个字。限制可见工具列表实现起来更简单，但会白白
放弃这份收益，而且并不会更安全——隐藏一个工具和拒绝执行它，拦下的是同一次调用。

## 与 DeepSeek Harness 0.1.x 的兼容性

当前版本已在未经修改的 `@deepseek-ai/dsh` `0.1.1-rc.2` 上验证，并继续以
`0.1.0-rc.7` 作为兼容下限。以下两项限制在已经验证的各版本中仍然存在，原因是
DeepSeek Harness 对子代理来源会话的处理方式。

**分支无法在原生对话界面中续聊。** DeepSeek Harness 用子代理来源标记来判断一个会话
归哪个组件所有，并会拒绝从原生对话界面向这类会话发送消息。原生界面可以显示分支的
历史，但无法向其中追加内容。因此当前版本没有提供"在对话中打开分支"的功能，阅读和
续聊都在树视图中完成。

**分支可能出现在子代理菜单中。** 由于插件不会安装子代理描述符，内置子代理列表可能
会把分支显示为不可用的诊断条目。在 `0.1.1-rc.2` 中，该列表改由会话顶栏的谱系控件
打开。这些条目无法选中，键盘浏览时会被跳过，也不计入活跃子代理的数量；控件和对话
本身都能正常使用。分支仍然不会出现在侧边栏的会话列表里。

插件中预留了对一项尚未发布的 DeepSeek Harness 能力的检测，该能力将允许在原生对话
界面中续聊分支。检测要求对应版本同时提供该能力并明确声明支持投递用户消息，以免某个
只实现了一半的版本悄悄启用可写界面。在此之前，该功能保持关闭。

## 开发

```sh
pnpm install
pnpm run check
```

`pnpm run check` 会依次执行代码检查、宿主端与浏览器端的类型检查、测试、生产构建和
发布包校验。

| 命令 | 用途 |
| --- | --- |
| `pnpm run lint` | 静态检查 |
| `pnpm run typecheck` | 类型检查 |
| `pnpm test` | 单元测试与集成测试 |
| `pnpm run build` | 生产构建 |
| `pnpm run check` | 以上全部，外加发布包校验 |

## 参与贡献

欢迎提交问题反馈和合并请求。提交合并请求前，请先运行 `pnpm run check`。

## 项目状态

当前实现已在未经修改的 DeepSeek Harness `0.1.0-rc.7`、`0.1.0-rc.8` 和
`0.1.1-rc.2` 上验证通过。DeepSeek Harness 仍处于开发者预览阶段，即使本软件包声明
的兼容范围更宽，后续版本仍可能需要相应更新。

## 许可证

[MIT](LICENSE)
