WangXuexin24/dsh-session-topics ↗★ 0
dsh-session-topics
会话话题分组:侧边栏按话题把会话归堆(纯客户端,零 host 服务依赖,不删会话、不执行脚本)
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:WangXuexin24/dsh-session-topics说明文档
阅读完整 README ↗dsh-session-topics
给 DSH Web 侧边栏加一层按话题分组会话的能力。
只想先会用? 跳到 用户手册(装 / 开 / 关 / 恢复 / 卸载 / 升级)。 想接手改进代码? 读
docs/HANDOFF.md—— 面向下一个 agent 的交接文档 (代码地图、必须遵守的契约、已知问题与改进路线)。
解决什么问题
DSH 官方的侧边栏只能按工作区分组,而工作区的成员资格是从每个会话记录的
cwd 推导出来的 —— 一个工作区只拥有 cwd 等于它自己路径的会话。所以同一个
目录下开出来的会话全挤在一组里,没法按主题拆开。
本插件加一条纯展示层的轴:话题文件夹。
核心设计:话题归属于工作区
话题是工作区作用域的 —— 一个话题只属于一个工作区,会话绝不会被挪到不属于 它的工作区里。这不是随手定的,有两条理由:
- 它跟 DSH 自己的模型一致。 workspace 记账硬性保证"一个会话只属于一个工作区",
insertSessionBefore的会话排序也是按工作区的。跨工作区的话题会同时破坏这两点。 - 它让工作区保持可读。 如果会话能自由迁移进全局话题,工作区小节就会空掉, 用户再也答不上"这个会话是哪个项目来的"。
因此:跨工作区的拖拽会被直接拒绝(不建话题、不改归属)。这是有意为之。
功能
- 侧边栏渲染成 工作区 → [话题 → 子话题 → 会话] + 未归话题
- 每个工作区行右侧有
+,在那个工作区里新建话题 - 会话行右侧显示相对时间(「刚刚 / 5 分钟前 / 2 小时前 / 3 天前 / N 个月前」),每分钟自动刷新
- 拖会话到另一个会话的上/下半 → 调整顺序(插入指示线在上或在下),顺序由 host 持久化
- 拖到话题标题上(含子话题)→ 归入该话题
- 拖到「未归话题」标题上 → 移出话题回到未归类(归错了能拖回来)
- 话题行悬停出现
+/✎/×:新建子话题 / 重命名 / 删除 - 话题嵌套一层(话题 → 子话题)。数据模型是递归的,只有 UI 限层,见
MAX_TOPIC_DEPTH,改一个常数即可放开 - 会话右键菜单:重命名会话(内联编辑)/ 分叉会话 / 归档会话 / 移出话题
- 工作区右键菜单:新建话题 / 重命名工作区 / 删除工作区(删除前确认)
- 展开/折叠、话题、归属关系全部持久化,刷新和重启都保留
- 点会话行照常打开会话
「拖拽排序」和「拖成文件夹」是两件事
官方的拖拽手势是排序(拖到行的上/下半,host 持久化)。第一版把它挪用成 「拖到会话上就建话题」,等于抢走了用户本来就有的手势,还让排序和分组两个概念 再也分不清。
现在的边界是明确的:
| 你想干什么 | 怎么操作 |
|---|---|
| 调整顺序 | 拖到另一会话的上/下半(显示插入线) |
| 归入某个话题 | 拖到话题标题上 |
| 移出话题 | 拖到**「未归话题」标题**上 |
| 新建话题 | 工作区行右侧 +(或右键工作区) |
| 新建子话题 | 话题行悬停的 + |
接管官方槽位,就必须继承官方职责
本插件用 priority: -1 顶替官方 ui-workspace 注册的浏览器 —— 会话行从此由本插件
渲染。这意味着官方行上原有的每一个操作都属于本插件的责任。
第一版只实现了"打开会话",于是重命名 / 分叉 / 归档 / 工作区重命名删除全部静默消失。 保留会话是因为用户只看得见"少了个功能",看不见"是谁拿走的"。任何接管槽位的插件 都必须先盘点被替换方的全部能力,再逐条实现。
设计取舍(为什么它比同类插件"干净")
| 本插件 | 社区同类插件 | |
|---|---|---|
| host 半边 | 空壳,不 inject 任何服务 | 有的 inject storageDomain/workspaceRegistry |
| 持久化 | 浏览器本地(客户端 store) | 有的落 ~/.dsh 文件 |
| 启动风险 | 结构性为零(不依赖任何 host 服务) | 有挂起启动的先例 |
| 删除会话 | 不做 | 有的会写 .ps1 并 ExecutionPolicy Bypass 执行 |
| 官方数据 | 只读,从不写 | 有的改写/替换官方渲染器 |
| 官方 store key | 从不触碰(自有 dsh.session.topics.v2) | — |
明确不做的事:不删除会话、不调子进程、不生成脚本、不写任何 host 文件、不注册 任何面向模型的工具或提示词(零 token 成本)。
两条来自踩坑的硬约束:
- 垃圾回收器只能删它能证明已死的东西。 清理"死引用"的那一趟,在刷新后的
前几帧会用空的
workspaces/byId去比对已经加载好的话题表 —— 于是每个话题 都被判定"所属工作区已不存在",每次刷新都把用户的分组整个删光。 修法是两层:数据没到齐(phase !== 'ready')就不跑;并且 store 层的retain()在比较集为空时直接返回(空的比较集意味着"还没加载",不是"全都没了")。 - 用户手搭的东西必须有第二份。 分组无法再生成,所以每次非空变更都会把快照写进
dsh.session.topics.v2.bak;当主存储为空而备份有内容时,侧边栏顶部会出现一条 恢复条,一键还原。 - 不依赖 store action 的返回值。 需要原子性的写入都做成单个 action
(如
createTopicWithSessions),因为 store 实现可以不返回值(Redux 风格), 靠读返回值拿新 id 会静默失效。 - 必须接住 shell 的滚动契约。 官方 ui-workspace 的根节点是
flex:1; min-height:0; flex-direction:column,内层列表flex:1; overflow-y:auto。 不这么做,内容会被overflow:hidden的区域裁掉且无法滚动 —— 看起来像 "前端卡死",但 JS 线程其实完全健康。 - 捕获阶段的监听器会抢在一切之前。 右键菜单用捕获阶段的
mousedown做 "点外面关闭",结果连菜单内部的按下也被它先看到 —— 菜单在click触发前就被 卸载,所有菜单项变成摆设。stopPropagation救不了(捕获早于冒泡), 必须显式判断ref.current.contains(event.target)。 parentId是分叉血缘,不是子代理。 用它过滤会把每一个分叉出来的会话 藏起来 —— 用户点完"分叉"就找不到子会话了。唯一正确的过滤条件是origin === 'subagent'。- fixed 定位的菜单必须钳制在视口内。 在贴近屏幕底部右键时,按光标坐标摆放 会把末尾几项推出视口 —— 菜单看着正常,但最后几项物理上点不到。 要在挂载后量尺寸再钳制,而不是渲染前估算。
安装(本地目录)
dsh plugin --profile web add D:/DSH/projects/dsh-session-topics
或按本 profile 已有的 link: 惯例,在 ~/.dsh/profiles/web/package.json 里加:
"dsh-session-topics": "link:D:/DSH/projects/dsh-session-topics"
并把它加进同一文件的 dsh.profile.bundles 数组。
改代码后只需刷新浏览器页面(客户端 bundle 每次加载都重新取),不必重启 dsh web。
验证与回滚
node "/node_modules/@deepseek-ai/dsh/lib/bin.js" --profile web --dump-config
node "/node_modules/@deepseek-ai/dsh/lib/bin.js" plugin --profile web remove dsh-session-topics
本机
dsh.cmd/dsh.ps1在受限 shell 里会被执行策略拦掉(返回空输出,容易被误判成 "命令没生效"),直接node .../lib/bin.js更稳。
离线自测
不需要浏览器,不需要装依赖:
node test/smoke.mjs
测试桩是故意做成迷你 React 的:函数组件会被真正调用、hook 状态跨渲染持久、 effect 尊重依赖数组,并且 store 的 action 故意不返回值。这三点都是踩过的坑 —— 早期版本的桩太松,把真 bug 放过去了。
覆盖:模块加载、slot 注册与 shadow 优先级、会话过滤(隐藏 subagent / 已归档 / 非当前空白会话)、按工作区建话题、空话题可见、真实 input→commit 重命名路径、 同工作区拖拽建话题、跨工作区拖拽被拒绝(含拖到标题上)、嵌套与层数上限、 删除父话题连带子树、点击打开会话、滚动契约 CSS、官方同款 14px 字号。
以及一组**"接管不能丢功能"**的回归:会话右键菜单存在且含重命名/分叉/归档、
重命名走内联编辑并提交到 host RPC、分叉与归档路由正确、工作区菜单含
新建话题/重命名/删除、拖到「未归话题」能把会话移出话题、跨工作区移出被拒绝、
retain() 会清掉已死话题/工作区的展开态 key。
还有一条菜单可点击性回归:菜单内部按下不能把它关掉(否则 click 永远不触发),
菜单项点击必须真的执行动作,菜单外部按下必须关闭。
结构
package.json "." → 空壳 host;"./client" → 浏览器半边
cordis.patch.yml 插入 host 行(id: session-topics)
lib/index.js host 空壳,故意不 inject 任何服务
lib/client.js 全部功能(__ModuleLoader__ 包装的浏览器产物)
test/smoke.mjs 离线冒烟测试
已知限制
- 两级话题,无更深嵌套(改
MAX_TOPIC_DEPTH可放开,但侧边栏宽度会先撑不住)。 - 话题归属存在浏览器本地:换浏览器或清缓存会丢,也不跨设备同步。 这是为了换取"零 host 依赖、零启动风险"而做的刻意取舍。
- 接管了
sidebar.workspaces槽位(priority: -1压过官方的 0)。这是加会话级 分组的前提 —— 会话行本来就是官方那个渲染器画的。官方将来更新该槽位时需人工跟进。 - 未实现官方的「添加工作区…」目录流子 slot(官方
sidebar.workspaces.directoryFlow已由官方条目声明,本插件故意不重复声明)。需要新增工作区时用官方入口或dshCLI。
用户手册(装 / 开 / 关 / 恢复 / 卸载 / 升级)
| 我想… | 怎么做 |
|---|---|
| 装 | 插件市场 → 安装;或 dsh plugin --profile web add D:/DSH/projects/dsh-session-topics |
| 临时关掉 | 插件市场 → 已安装 → 本插件卡片的「启用中」开关关掉 → 按提示重启 DeepSeek Harness |
| 再打开 | 同一开关打开 → 重启 |
| 恢复出厂(重置分组) | 卡片上的「恢复」按钮 |
| 卸载 | 卡片上的「卸载」;或 dsh plugin --profile web remove dsh-session-topics |
| 升级本地代码 | 直接改 D:/DSH/projects/dsh-session-topics 里的文件 → 刷新浏览器页面即可(客户端 bundle 每次加载都重新取)。只有改了 package.json / cordis.patch.yml 才需要重启 dsh web |
| 确认它在跑 | 侧边栏是「话题」分组而不是官方的纯工作区分组 = 它在跑;node test/smoke.mjs 应输出 RESULT: all checks passed |
开关与「恢复 / 卸载」是插件市场(
dshmarket)的能力:它改的是 profile 的 patch 层, 所以开关之后要按提示重启才生效。本插件自身不实现开关。
疑难排错
| 症状 | 原因 | 处理 |
|---|---|---|
| 侧边栏变回官方按工作区分组 | 插件被关掉 / 没加载(priority: -1 没生效) | 看插件市场里是否「启用中」;重启 dsh web |
| 分组整个不见了 | 浏览器本地存储被清(换浏览器、清缓存) | 顶部若有恢复条点还原;否则查 localStorage["dsh.session.topics.v2.bak"] |
| 列表滚不动、像前端卡死 | 滚动契约被破坏(根节点的 flex 与列表 overflow) | 见 docs/HANDOFF.md §3.3 |
| 侧边栏冒出一批空对话 | 不是本插件的问题 | 见下方「已知问题」 |
| 右键菜单点不动 | 捕获阶段的监听器把菜单提前关掉了 | 见 docs/HANDOFF.md §3.7 |
| 冒烟测试不过 | 改了 UI / 过滤逻辑 | node test/smoke.mjs 看具体断言,改完必须全绿 |
已知问题
- 空对话:DSH 的冷会话摘要把
blank默认成false(dsh-api-session-controller的summarizeCold),于是"开起来就死"的空壳会话 在官方侧边栏和本插件里都会显示。根因、识别特征与三条缓解路径见docs/HANDOFF.md§5.1。 - 官方
sidebar.workspaces槽位演进时需要人工跟进(本插件顶替了官方浏览器)。 - 分组数据存在浏览器本地:不跨设备、清缓存即丢(有
.bak+ 恢复条兜底)。
发布状态
- 已公开发布:(PUBLIC,
main)- 他人安装:
dsh plugin --profile web add github:WangXuexin24/dsh-session-topics - 本机仍以
link:安装,出现在插件市场「已安装」列表(带开关) lib/已随仓库提交(dsh 从 git 安装不跑构建)
- 他人安装:
- npm:未发布 —— 可选。不发也能从 GitHub 安装;发了市场才会显示下载量。
- 社区目录收录(出现在市场「发现 / 主题」):可选,尚未提交。
条目已备好在
docs/market-submission.yml,随时可用; 另有「仓库创建满 1 天」的 CI 门槛。不收录不影响安装,只影响被搜到的概率。
License
MIT