--- description: "划词分支助手:框选对话文字后从该处开新分支,保留原文标记与可跳转的分支树面板。" kind: package-reference --- # dsh-branch-companion [English](README.md) | 中文 ## 概述 `dsh-branch-companion` 给 Web 客户端加一条“从这段文字长出一个分支”的路径:选中回答或提问中的一段文字,浮出的**添加到新的分支**会以当前 Session 为源调用 Host 的 Session fork,随后打开子 Session、记录引用、在原文旁留一个可点击的 ⑂ 标记,并在右上角维护一棵可跳转的分支树。 节点由浏览器持有(`localStorage`),Host 不新增任何服务或事件;去掉这个插件只会带走面板与标记,不会动 Session、不会动历史。 ## 功能 - **划词建分支**:框选回答或提问里的任意一段文字,选区下方浮出「添加到新的分支」,点击即以当前 Session 为源 fork 子会话。 - **上下文保留**:子会话保留分叉点之前的上下文,输入框自动预填 `关于这段内容:「…」`。 - **原文标记**:被引用文字右侧出现 ⑂ 标记,随滚动、重排、流式输出重新贴合;点击跳回分支。 - **分支树面板**:右上角「⑂ 分支」胶囊展开面板,按来源会话分组、父子层级展示;可逐级展开/折叠,也可一键折叠全部;当前分支带高亮圆点。 - **可跳转、可清理**:点击节点跳转;悬停 ✕ 移除该节点及其子分支,不删除会话本身。 - **纯客户端**:节点存在浏览器 `localStorage`,Host 不新增服务或事件;卸载插件只带走面板与标记。 ## 使用示例 以一段多小节的技术回答(例如「解释 OFDM 原理」)为例: 1. **从回答中创建分支**:框选「循环前缀(CP)」,点击选区下方浮出的「添加到新的分支」。 2. **在分支里继续追问**:子会话自动打开,输入框已预填 `关于这段内容:「循环前缀(CP)」`,直接接着问即可。 3. **回到原文**:原会话里这段文字右侧出现 ⑂ 标记,点击跳回刚建的分支;滚动后标记会重新贴合。 4. **查看整棵树**:点右上角「⑂ 分支」,面板按层级列出所有节点;点击节点跳转,悬停 ✕ 移除节点(会话保留)。 > 截图演示见 ChatGPT / Codex 桌面版 [chatgpt-desktop-branch-companion](https://github.com/Tetro02/chatgpt-desktop-branch-companion)。两版核心交互一致(划词 → 建分支 → 原文标记 → 树面板);DSH 版的宿主是 DSH Web 会话,节点存在浏览器 `localStorage`。 ## 目录 - [功能](#features) - [使用示例](#usage-examples) - [使用本包](#use-this-package) - [理解实现](#understand-the-implementation) - [进一步探索](#further-exploration) - [模型体验](#model-experience) - [已知限制与延期工作](#known-limitations-and-deferred-work) - [开发备注](#dev-note) ----- ## 使用本包 ### 交互 | 操作 | 结果 | |---|---| | 在对话正文里框选文字 | 选区下方浮出**添加到新的分支** | | 点击该按钮 | 以当前 Session 为源 fork 子 Session,打开它,并把 `关于这段内容:「…」` 预填进输入框 | | 原文选区右侧的 ⑂ 标记 | 点击跳到对应分支;标记随滚动、重排、流式输出重新贴合 | | 右上角**⑂ 分支**胶囊 | 展开分支树;点击节点跳转,悬停 ✕ 移除该节点及其子分支 | | 分叉后继续分叉 | 子分支挂在父节点下,按层级缩进 | ### 安装 这个包本身就是一个 DSH bundle:声明了 `dsh.bundle`、自带 `cordis.patch.yml`,不需要手工改 Loader。 用插件管理器(`plugin_manager`,`action: install_bundle`)安装,target 传下面任一 spec: - 仓库地址:`https://github.com/Tetro02/dsh-branch-companion` - 本地目录:`file:C:\path\to\dsh-branch-companion` 管理器会写入 profile 依赖、选中该 bundle,并热应用它的 patch 层;`remove_bundle` 可整体回退。需要 DSH 0.2.x 与 Web UI——Host 半边只为让客户端半边能被组合出来。 ## 理解实现
实现细节——点击展开 ### 设计思路 插件只做三件事,且都在浏览器里:把一次选区变成一次 fork、把 fork 关系记录下来、把记录画回原文旁边。它不注册 slot、不渲染 React 节点,因此不改动 transcript 的宿主结构;代价是依赖 DOM 契约(`[data-conversation-content]`、`[data-conversation-session]`)。 标记不包裹文字。它把 transcript 的文本节点拼成一个字符串,用去掉空白差异的匹配找到引用的片段,生成一个 Range 量出 `getBoundingClientRect()`,再把绝对定位的徽标放到右侧——React 重渲染后旧节点消失、新节点就位,下一次重绘自动贴合。 ### 源码地图 | 文件 | 职责 | |---|---| | `src/index.ts` | Host 半侧:只为让 Loader 组合客户端 bundle 而存在,无服务、无状态 | | `src/client/index.ts` | 浏览器入口:注册词典、挂载 overlay、把清理交给插件 fiber | | `src/client/overlay.ts` | 选区动作、来源标记、分支树面板、预填重试 | | `src/client/anchor.ts` | 把存储的引用重新锚定到当前 transcript,返回 Range | | `src/client/quote.ts` | 引用规范化与空白容忍匹配(纯函数,可单测) | | `src/client/tree.ts` | 节点文档的解析、父子关系、子树移除、深度(纯函数,可单测) | | `src/client/types.ts` | 用到的客户端服务结构类型(不引入 DSH 类型依赖) | | `scripts/build.mjs` | esbuild 产出 Host ESM 与包装后的浏览器 bundle,tsc 产出声明 | ### 主流程 选区 → `sessions.fork({ sessionId, increaseTitle: true })` → 子 Session id → 追加节点并落盘 → `uiWorkspace.openSession(childId)` → 轮询 `sessions.binding(childId)` 与 `conversation.input.requestDraftInitialization` 完成预填(最多 15 次、每次 200 ms,失败静默)。
----- ## 进一步探索 - Session fork 语义见 `@deepseek-ai/dsh-api-session-controller` 的客户端 `sessions.fork`;标题递增与 `atSeq` 边界都由它决定。 - 导航入口见 `@deepseek-ai/dsh-client-ui-workspace` 的 `uiWorkspace.openSession`。 - 文档预填见 `@deepseek-ai/dsh-client-ui-conversation` 的 `conversation.input.requestDraftInitialization`。 ## 模型体验 无直接内容。插件不注册工具、不注入提示词、不发送消息;分支会话之间的可见差异只来自用户自己的输入。 #### Token 影响 零。没有系统提示词段落,没有运行时上下文行。 #### KV Cache 影响 无。插件不组装也不发送提供方请求。 ## 已知限制与延期工作 - **分叉点由 Host 决定**:`sessions.fork` 默认切在最后一个已完成轮次;选中的文字只作为上下文与节点引用,不是“精确到这条消息”的切面。 - **流式回答中不能分叉**:Host 拒绝在未完成轮次上 fork,插件只给出一次失败提示。 - **标记依赖文本还在**:消息被折叠(工作过程详情)或被改写时,引用匹配不到就不画标记,节点仍在面板里。 - **面板是浮层**:覆盖右栏内容,需要手动收起;没有接入侧边栏 slot。 - **文档按 Origin 存**:换 host/端口会得到新的 `localStorage`,旧的节点文档不会跟过去。 - **没有 Host 配置**:阈值(引用上限 600 字、标记上限 40、重试次数)是编译期常量,不是 Schemastery 配置项。 - **相对 ChatGPT 桌面版**:树节点暂不支持键盘导航与删除撤销;节点存在 `localStorage`,不写 `branch-state.json`。 ### 开发备注
维护者工作上下文——点击展开 ```bash pnpm install # esbuild + typescript pnpm run build # lib/index.js、lib/client.js、lib/internal/*、lib/types/** pnpm test # node --test,纯函数单测 pnpm run typecheck # tsc --noEmit ``` 浏览器 bundle 必须是 `window.__ModuleLoader__.load({ id, factory })` 形式,且模块副作用留在 factory 内;`scripts/build.mjs` 把 esbuild 的 IIFE 包进 factory,就是为了不提前执行。