# dsh-plugin-branch-origin · 分支联系(DSH 插件) [English](README.md) | [简体中文](README.zh-CN.md) > 给**分叉出来的会话**标上来源,让「我是从哪个对话分出来的」这件事在侧栏上直接可见。 **两条通道,互为补充**: | 通道 | 形态 | 改名后 | |---|---|---| | 宿主半边(标题) | 子会话标题变成 `⤷ 来源:<源会话标题>` | **丢失** —— 标签是标题字符串的一部分 | | 客户端半边(侧栏徽标) | 行首一个 `⤷`(悬停显示源标题) | **仍在** —— 它从会话的父级字段读,不碰标题 | 零依赖、无构建链。宿主半边是 [lib/index.js](lib/index.js) + [cordis.patch.yml](cordis.patch.yml) 一行;客户端半边是手写的 [lib/client.js](lib/client.js)(只用官方基座的 `react` / `react/jsx-runtime`)。 ## 为什么需要它 DSH 的 fork **已经**建立了真实父子链,也**已经**有一点可见性,但没有任何一处写出「来自哪个对话」: | 官方已有的 | 缺的那一格 | |---|---| | `SessionHeader.parentSession`(真实父子链) | 侧栏不显示它指向谁 | | 官方 fork 把子标题改成 `<源标题> (1)`(`increasedForkTitle`) | 只是个编号,看不出这是别人的分叉 | | 侧栏按 `SessionListEntry.depth` 做谱系缩进 | 缩进不说明来源对话叫什么 | 本插件补的就是右上那一格,并且补**两遍**:把子会话标题改写成 `⤷ 来源:<源标题>`(来源在标题里自述),再在侧栏行上加一个不依赖标题的来源徽标(改名也抹不掉)。 ## 行为 规则只有一条,其余都是它的推论。 | 情形 | 结果 | |---|---| | 任何入口产生的新 fork(侧栏「分叉会话」、消息菜单、别的插件的 fork、`ctx.sessions.fork`) | 标题变成 `⤷ 来源:<源标题>` | | 官方随后把它改成 `<源标题> (1)` | 收敛成 `⤷ 来源:<源标题> (1)`,序号保留,同源多个分叉仍可分 | | 子会话标题已被用户或别的谱系插件改过(如 `⑂1 …`、`我的实验`) | **不动**(只标注,不接管) | | 子标题已经是 `⤷ 来源:…` | 幂等,不重复写 | | 没有父级的普通新会话 | 不动 | | 子智能体会话(`header.origin === 'subagent'`) | 不动(默认;可用 `labelSubagents` 打开) | | 源会话当时没有标题 | 不动 | 判定细节:只有当子标题**抹掉官方尾编号后与源标题同名**时才算「仍是继承形态」,这时才贴标记。见 [docs/DESIGN.md](docs/DESIGN.md)。 ### 侧栏徽标(客户端半边) 只读,不接管官方行: | 情形 | 结果 | |---|---| | 行的父级字段非空、且不是子智能体会话 | 行首出现 `⤷`,鼠标悬停该行时有「来源」段显示源对话标题 | | 你把这条会话**改名**了 | 徽标**照旧显示**(它不依赖标题),而标题里的 `⤷ 来源:…` 会随改名消失 | | 源对话被改名 | 徽标跟着显示**新**名字(实时读列表);标题里的那份是 fork 时的快照,不会变 | | 源对话不在当前列表快照里 | 显示 `(未命名对话)`,不崩 | | 普通会话 / 子智能体会话 / 子代理分支 | 不显示徽标 | ## 安装与启用 **已装进 `desktop` profile**(2026-10-07),形态与 `trial` 上已验证的 `dsh-plugin-branch` 一致: | 落点 | 内容 | |---|---| | `/package.json` → `dependencies` | `"dsh-plugin-branch-origin": "link:<本仓库>"`(插件页的「已安装」列表读这里) | | 同上 → `dsh.profile.bundles` | 追加包名(页面上的启用开关;组合时据此应用本 bundle 的 patch) | | `/node_modules/dsh-plugin-branch-origin` | 指向本仓库的目录链接 | 装、查、卸是同一个脚本: ```powershell node .verify/install-desktop.mjs --status # 只看现状,不写任何东西 node .verify/install-desktop.mjs # 安装(幂等,可重复跑来修复) node .verify/install-desktop.mjs --uninstall # 精准卸载(不从备份整体还原) ``` 装完必须**完全退出并重开** DSH Desktop —— 实测 HMR 从不热装载插件行,客户端半边的装载判定也会被缓存到重启。重启后: 1. 侧栏对一个对话点「分叉会话」→ 新会话标题变成 `⤷ 来源:<源会话标题>`,行首出现 `⤷` 徽标; 2. 把这条分叉会话**改名** → 标题里的来源没了,但行首 `⤷` **还在**,悬停仍显示源对话名。 重启前想确认组合对不对:`node --import ./test/register.mjs .verify/diagnose-desktop-compose.mjs`(只读,复现宿主编排)与 `node .verify/check-client-manifest.mjs`(只读,按 `dsh-client-modules` 的规则预检客户端半边声明)。 > ⚠️ 本插件**零 import**,所以不像有依赖的插件那样受"junction 装载解析不到宿主包"的限制。 > ⚠️ 2026-10-07 出现过"应用按自身状态重写 profile 清单,把本地 `link:` 依赖与 bundle 项一起丢掉";真发生的话重跑上面的安装命令即可(脚本是幂等的)。 ## 验证 | 用途 | 命令 | |---|---| | 离线:宿主行为断言(13 项,桩 ctx)+ 客户端半边冒烟 | `npm test` | | 离线:只跑客户端半边冒烟(协议 / 两个席位 / 来源判据 / 降级 / 异常保护) | `npm run client-smoke` | | 真机端到端(14 项,真 Loader / 真 SessionStore / 真 session-title / 真 session-query) | `npm run rm-test` | | 预检:按 `dsh-client-modules` 的规则核对 `dsh.client` 声明 | `node .verify/check-client-manifest.mjs` | | 预检:复现 desktop profile 的宿主编排(先设 `NPM_GLOBAL_ROOT` = `npm root -g`,或用 `DSH_ANCHOR_GLOBAL` 直接指向 dsh 包) | `node --import ./test/register.mjs .verify/diagnose-desktop-compose.mjs` | | **GUI 真机:无头 Chrome + CDP 断言侧栏行/徽标/悬停段/字色(9 项)** | 见 [.verify/REPORT.md](.verify/REPORT.md) §7.3 的运行步骤 | | 提交前隐私检查(暂存内容 / 完整历史) | `.privacy-tools/README.md` | | 文档结构 lint | `node .verify/md-lint.mjs` | 真机装置在仓库内的临时 `DSH_HOME` 里启动真 DSH 运行时,**不碰用户 `~/.dsh`**、不开端口、不调模型。证据与结论见 [.verify/REPORT.md](.verify/REPORT.md)。 ## 已知限制 - **标题里的来源挡不住改名**:这是"标签写在标题里"的固有代价。**侧栏徽标挡得住**(D009);想要两者都有,现在是默认。 - **标题里的是快照**:它记的是 fork 那一刻源对话的标题;改**源**对话的名字不会让旧标题跟着变。徽标显示的是**实时**名字。 - **只标注新 fork**:插件装载**之前**就存在的旧分叉不会补标(不做启动扫描)。 - **标注窗口**:fork 之后约 4.6 秒内(`settleMs 600` + `graceMs 4000`)会持续收敛标题;此窗口内用户手动改名**不会**被覆盖(改名后标题不再是继承形态),但若有人把标题改成恰好等于 `<源标题> (N)` 则可能被贴上标记。 - **标题会被钉住**:走的是 `sessionTitle.rename()`,即 `source: { kind: 'user' }`。对 fork 子会话无副作用 —— first-prompt 标题提供方本来就跳过带父级的会话;但请勿把本插件用于给**非 fork** 会话命名。 - **`maxTitleBytes` 会截断**:DSH 默认 80 字节,源标题很长时官方 `rename` 会截断尾部;截断保留头部,所以 `⤷ 来源:` 前缀仍在。 - **客户端半边已在真实浏览器里机检**(无头 Chrome + CDP,9 项全通过:侧栏行、徽标、tooltip、悬停段、字色、控制台零错误)——见 [.verify/REPORT.md](.verify/REPORT.md) §7.3。 - **悬停段的字色是跟着官方悬停卡写死的**:官方悬停卡是深色菜单材质,它自己的 CSS 里把文字颜色写成 `#fff` / `#cfd3d6` / `#adb2b8`,**不走主题 token**。所以本插件的悬停段也用这几个固定色(v0.1.1 曾用 `--dsw-alias-label-primary`,在卡片里会解析成浅色主题的近黑值、与卡片背景同色 ⇒ 字看不见;v0.1.2 修正)。若 DSH 之后改了卡片配色,这里要跟着改(GUI 真机的 A7/A8 断言会报警)。 - **只在 DSH 0.2.0-rc.2 / Windows 上实测过**:宿主契约与客户端席位按该版本核对;换版本需重跑验证表里的检查。 - **不做**:不创建分叉、不搬子 agent、不画树、不改 parent pointer、不写 sidecar、不注册任何模型可见工具、不占 `single` 占位、不覆盖官方行。 ## 与同类插件的关系 「把来源写进会话标题」是已被验证的做法([dsh-autofork](https://github.com/vlln/dsh-autofork) 的 `⑂n 家族根名`、[dsh-session-tree](https://github.com/Nirvana-Jie/dsh-session-tree) 的 `Task (1) (1)`),但两家**只标注自己创建的分叉**。本插件的差异点是**入口覆盖**:挂原生 `session/created`,官方入口造出来的 fork 同样带标签。核查过程与出处见 [docs/PRIOR-ART.md](docs/PRIOR-ART.md)。 本插件**只标注、不接管**,所以可以和上述插件并存:它们先命名过的标题会被原样放过。 ## 获取、反馈与许可 | 项 | 现状 | |---|---| | 获取方式 | 公开仓库:[WindFromKadath/dsh-plugin-branch-origin](https://github.com/WindFromKadath/dsh-plugin-branch-origin);克隆后按「安装与启用」用 `link:` 目录链接装进 DSH profile | | 反馈渠道 | 可复现的问题与使用疑问走 [Issue](https://github.com/WindFromKadath/dsh-plugin-branch-origin/issues);改动建议走 Pull Request —— 检查方式见 [MAINTAINER.md](MAINTAINER.md) | | 许可 | MIT,见 [LICENSE](LICENSE)(Copyright (c) 2026 WindFromKadath) | | 第三方材料 | 无捆绑的第三方代码或素材;`README` 与 `docs/PRIOR-ART.md` 只以链接方式引用他人项目 | | 维护可用性 | 个人项目,按需维护;不承诺响应时限与版本支持周期 | ## 项目入口 - [AGENTS.md](AGENTS.md):接手与维护规则(含本机环境事实、危险边界)。 - [MAINTAINER.md](MAINTAINER.md):作者侧维护入口(本轮范围、理解、取舍、验收与公开前检查)。 - [docs/DESIGN.md](docs/DESIGN.md):源码级契约与判定规则(每条带 `包/文件:行号`)。 - [docs/PRIOR-ART.md](docs/PRIOR-ART.md):2026-10-07 先行者核查。 - [tasks.csv](tasks.csv):任务与验收。 - [.verify/REPORT.md](.verify/REPORT.md):真机验证报告。 - [.privacy-tools/README.md](.privacy-tools/README.md):提交前隐私检查怎么跑、查什么。