前言¶
DeepSeek Harness(DSH)的对话视图通常以列表或画布形式呈现。当对话轮次较多、分支较多时,列表难以一眼看清整体结构,画布又没有明确的分支语义。dsh-client-ui-branch-map 提供一张对话分支图:把当前工作区的对话画成一张图,每条消息是一个节点,每个 Session 是节点上的分支引用,可以像 git 分支一样从任意节点切出去。
这是什么¶
dsh-client-ui-branch-map 是一个 DSH 客户端视图插件,由维护者 AnichHaveatry 维护。它的核心是把 DSH 工作区里的对话渲染成一张分支图:根节点代表 Workspace,消息节点代表一轮对话,Session 是指向某个节点的引用。归档的对话不会进入这张图,子代理运行也不会进入。
核心功能¶
分支图视图¶
视图包含四个基本元素:
- 作用域:恰好一个 Workspace
- 根节点:一个,代表 Workspace 本身
- 消息节点:用户发出的一条消息(一轮对话)
- 边:节点 → 在该会话里紧随其后的下一条消息
- Session:不是节点,而是指向某个节点的引用,和 git 分支指向 commit 一样
一个 Session 的引用停在它自己拥有的最后一条消息上;还没有自己消息的 Session 指向它被切出来的那个节点。两个 Session 停在同一个节点,读起来就像两个 git 分支指向同一个 commit。
行布局与根节点¶
行按轮号上升排列。轮号小的一定在前,所以这始终是一个合法的拓扑序;兄弟分支落在同一行带,分叉就读成两条并排的 lane。
根节点在树中水平居中:根级会话向左右两侧展开,新增会话不会把所有根线堆在左边。
不画什么¶
- 子代理运行:子代理是代理自己派出去的工作,不是用户与它的交互;子代理 Session、以及从子代理再分出来的 Session,都不进入图,也不占节点。过滤按血缘做:
isOwnConversation一路走到根,只要祖先是origin: "subagent"就整条排除。 - 归档的对话:归档集由宿主单独给出:
useWorkspaces快照的archivedSessionIds。归档的 Session 仍然是它工作区的成员,所以不能靠成员表过滤,必须显式减掉。减掉的是它的引用,以及只由它引入的节点。 - 当前打开的那条会话是例外:它被归档时照样画。工具栏的
已隐藏 N 条归档数的就是被减掉的其它分支。 - 存储细节:没有事件序号、没有 Session id、没有加载/刷新控件、没有子代理计数。
操作控件¶
| 控件 | 效果 |
|---|---|
| +分支 on a message | ctx.sessions.fork({ sessionId, atSeq }) — 打开一个新 Session,带上从根到这条消息的整条路径,然后切过去。始终显示,不藏在 hover 里 |
| 改写 on a message | 在消息之前分叉,然后把改写后的文本作为新分支的第一条 prompt 发出去;原分支不受影响。轮次起点在日志第一条事件时没有”之前”可切,这个按钮不出现(从根开新分支即可) |
| a ref chip on a node | 点它打开那条会话现在所在的位置(它的 tip)。这和”从这个节点开新分支”是两个不同的问题,标签也分开写 |
| 从根开新分支 on the root | 一个从工作区根开始的空 Session |
两件容易混淆的事,界面刻意分开:
- 打开分支去它的 tip,因为 ref 记录的是这条线走到了哪里;不会把你送到中间的某个节点。
- 从节点开分支才是”站在这个节点上”:新 Session 的上下文是从根到它的路径,下一句话从这里继续。
atSeq 是包含式的事件切点。一轮自己的 turn/start seq 是包含的,所以下一轮 turn/start 之前的那条事件就是这一轮的末尾——因此切点不会落在轮中间,新 Session 继承节点的所有祖先、不会多带一点。
数据与行为边界¶
这个插件不读私有数据、不改对话、不联网、不落盘。
- 读:只用
conversation.view的标准客户端 props:useSessions、useWorkspaces(含它的archivedSessionIds归档集)、useProjection("turnOutline");以及ctx.sessions.refreshProjections(id)(宿主语义:只读投影,不启动迁移)。 - 写:视图自身不写任何东西。
+分支/改写是用户点击后才调用的ctx.sessions.fork(...),和 DSH 自带的分支按钮同一语义;不点就什么也不发生。 - 会话历史:从不
retain其它 Session、从不打开别人的历史;不读useChat,不读子代理目录。 - 存储:无。没有 localStorage / sessionStorage / IndexedDB / cookie / 插件配置文件。
- 文件:插件自身不读写任何路径。宿主可能按自己的投影缓存策略缓存派生值,但不会改会话日志。
- 网络:无。没有 fetch、WebSocket、遥测或外部服务。
- 宿主半边:
lib/index.js的apply是空的。不注册服务、投影、路由或工具。 - 停用 / 卸载:Tab 消失;样式表通过 effect 清理,locale 注册随 fiber 卸载;没有需要清理的插件数据。
- 迁移:
refreshProjections不启动日志迁移;视图也不打开别的会话,所以不会触发格式迁移写盘。
唯一”碰数据”的地方是用户主动点分支产生的 Session,以及 openSession 的界面导航——这两件事和 DSH 自带的同名操作完全一样。
安装与启用¶
这个包本身就是一个 DSH bundle:声明了 dsh.bundle.patch,自带的 cordis.patch.yml 按包名插入一行,不写任何绝对路径。
# cordis.patch.yml,随包发布
- insert:
- id: ui-branch-map
name: dsh-client-ui-branch-map
从 GitHub 安装(仓库自带 lib/,安装时不需要构建):
dsh plugin --profile desktop add github:AnichHaveatry/dsh-client-ui-branch-map
从 tarball 安装:
dsh plugin --profile desktop add .\dsh-client-ui-branch-map-0.3.0.tgz
从目录安装(开发时用,link: 是活链接):
dsh plugin --profile desktop add C:\path\to\dsh-client-ui-branch-map
装好之后重启 DSH并刷新页面。两个缓存要到重启才失效:
dsh-client-modules会按 Loader specifier 缓存每个包的 client bundle 路径;- bundle 字节在激活时快照,改
lib/client.js不会热更新。
兼容性:peerDependencies 声明 @deepseek-ai/dsh: ^0.2.0-rc.2。不兼容的 DSH 会跳过这个 bundle 并点名,而不是挂上一个坏掉的视图。pnpm 安装时出现 peer 依赖警告是预期的:peer 由 DSH 安装提供,不在 profile 里。
这个包和 dsh-client-ui-canvas 是两个独立包、两个独立 Tab(canvas 与 branch-map),可以并存;想只留一个就卸掉另一个。
适用场景与注意¶
适合需要清晰查看对话分支结构的场景:多轮对话、多次分支、需要快速定位某个分支的当前位置。
插件以当前 dsh 进程权限运行,安装前应检查源码与许可证。插件本身不落盘、不联网、不读私有数据,只通过标准客户端 props 和 ctx.sessions.fork 与宿主交互。
短结尾¶
dsh-client-ui-branch-map 提供了一张清晰的对话分支图,让 DSH 工作区的分支结构一目了然。它像 git 一样,用 Session 作为分支引用,让你可以方便地从任意消息节点切出新分支。
- 目录页:https://www.skillhub.cn/plugins/AnichHaveatry/dsh-client-ui-branch-map
- GitHub:https://github.com/AnichHaveatry/dsh-client-ui-branch-map