Skip to content

dsh-session-map

Verified

dsh-session-map · v0.1.3 · MIT · Web UI

Adds a draggable, zoomable radial map of your session tree to the DSH conversation view, so you can see which conversation branched off which, and switch by clicking a node.

Install

dsh plugin add dsh-session-map

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Tags

Readme

dsh-session-map

English | 中文

给 DSH 的会话视图加第三个标签页:一张能拖能缩的会话树地图。

它回答的是原生会话列表答不了的那个问题:

我这段对话,是从哪一句、哪个词上分出来的?它下面又接了几条?

原生列表是一条线,父子关系靠缩进,一棵树上多个分支的子节点看起来长得一样、找不着。这个插件把「一棵树」画成一张图:点哪个节点,就切到哪个会话。

它长什么样

        [根 · 线性变换]
               │
          [线性映射]  ← 父,永远在正上方
               │
   ┌───────────┴───────────┐
 [像与核]              [同构判定]      ← 子,全摊在下方半圆
      \                  /
       [单射与满射]  [核与像] …        ← 子达 3 个起自动换缩小档

中心是当前会话(焦点),父在正上方,子全部摊在下方半圆里 —— 上半圆整块留给父。

这个「不占上半圆」的规矩不只是审美:节点卡片很宽(中心 276px),子节点一旦落到正左右,那 276px 的半宽会把连线整段吃掉,画面上看着就是「没有连线」。(v0.1.0 就是这个毛病,v0.1.1 修掉了 —— 见「已知限制」里的回归防线。)

  • 点节点 = 切到那个会话。(拖动之后那一下不算点击,不会误切。)
  • 全树 开关把整棵树一次铺开(径向 tidy-tree + 分层引导圈)。
  • 拖动 = 平移地图;滚轮 = 以指针为锚缩放(跟游戏地图一个手感);另有 − / + 与「回到全部可见」两个按钮。
  • 顶部面包屑显示当前焦点到根的路径,右侧显示 第 N 层 · M 个分支。
  • 节点右上角的状态点:运行中(青)/ 等你确认(橙)/ 已完成(暗)。

安装

dsh plugin --profile web add dsh-session-map

重启 DSH 之后,会话视图顶部会多出第三个标签:对话 | 轨迹 | 树地图,排在出厂那两个之后。

看不到的话,说明「装了但没挂上」——检查包名是否在 dsh.profile.bundles 里。dsh plugin add 只往 dependencies 里写,不会自动挂载。

卸载

dsh plugin --profile web remove dsh-session-map

本插件不改任何出厂代码,纯加法;卸载后环境回到原样。

目录结构

dsh-session-map/
├── package.json          # 清单:dsh.bundle.patch + dsh.client
├── cordis.patch.yml      # bundle 层:把自己 insert 进配置树
├── dsh/
│   ├── index.js          # host 半边(Node)—— 刻意空心,见下
│   └── client.js         # client 半边(浏览器)—— 全部实现在这里
└── README.md

为什么 host 半边是空心的

dsh/index.js 只导出 name / inject = [] / apply(_ctx) {},什么都不干。

原因只有一个:cordis 的配置树需要一个可解析的模块入口,bundle 才能挂上。这个插件所有能力都在 UI 侧,Host 侧没有任何要做的事。所以它不注册路由、不注册服务、不读配置 —— 改了什么 = 零。

client 半边为什么长这样

外部插件的 client bundle 走 DSH 外壳的 lazy-CJS 协议:

window.__ModuleLoader__.load({
  id: 'dsh-session-map',
  factory: function (require) {
    var React = require('react')   // 用外壳自带的那一份,不自己打包
    // ...
    return { apply: apply, inject: ['slots', 'sessions'], __internals: { ... } }
  }
})

React 必须从 require('react') 拿外壳那一份 —— 自己带一份 React 会导致 hook 读不到外壳的运行时上下文。

挂载点

往 conversation.view 座位注册(list 型座位,出厂界面是「对话」「轨迹」两个 tab):

scope.slots.inject('conversation.view', function () {
  return scope.slots.register({
    name: 'conversation.view',
    id: 'session-tree',
    order: 20,                 // 排在轨迹之后
    label: '树地图',
    inject: function (sessionId) {
      return {
        sessionId: sessionId,
        open: function (id) { scope.sessions.open(id) }
      }
    }
  }, TreeMap)
})

已知限制

按严重度排:

  • 🔴 n≥5 时会挤。 中心会话若有 5 个以上直接分支,下半圆塞不下那么宽的卡片,相邻节点会重叠。根因是几何硬限制:卡片 208px 宽、容器只有 ~720px,5 张就要 1040px。但真实数据里几乎碰不到 —— 扫过 69 个真实会话,用户自己的对话分支最多 2 个;子节点多的(如 23 个)全是 subagent。
  • 🟠 subagent 会话会混进树里。 上一条的数据侧原因:DSH 的 useSessions.ids 含全部会话,其中约一半是子智能体,插件目前一视同仁。
  • 🟡 全树展开受窗口大小限制。 树一大就得靠缩放看,字会变小 —— 画布的固有代价,不是 bug。看细节建议用焦点模式。
  • 🟡 拖动没有惯性和边界。 手感是「直接跟手」,不是「甩出去滑一段」。故意不做。
  • 🟡 只认 parentId。 父会话还没落盘的空会话会显示成「上层」但点不过去;在它里面说句话就落盘了。
  • ⚪ label 没做 i18n,硬编码中文「树地图」。
  • ⚪ 没接 HMR:改完 client.js 刷新页面即生效(bundle 路由按内容 hash 出 rev),但「不用重启 DSH」这点是实测出来的。

一条回归防线

v0.1.0 有个真缺陷:n=2 时子节点正好落在正左右(±90°),Rx=223 而「中心半宽 138 + 子半宽 104 = 242」> 223,整条连线藏在卡片底下 —— 图上 3 个节点、0 条线,看着像功能没做。

现在有一条专门盯它的断言:要求每条「中心↔子」连线的可见段 > 22px,n=1..4 全扫。以后谁再动 layoutFocus 的角度,这条会先响。

其他限制,如实写

  • 虚拟根是展示用的分组节点,不是 DSH 里真实存在的会话。它会让「根」这一层多出一个节点(真实根数 = 虚拟根标题里写的 N)。
  • 从没有已完成轮次的源会话 fork,host 会拒绝(session/fork-unavailable);插件会如实报错并退回当前主会话重试一次,把这次退回记账而不是静默吞掉。

许可

MIT