返回目录
其他 插件

dsh-plugin-workspace-sorted

karottc/dsh-plugin-workspace-sorted

让 DeepSeek Harness 的 **工作区(Workspace)** 按「最近使用」排序的宿主(Host)插件。 侧边栏自带的「排序方式:手动排序 / 最近更新」只作用于**会话**;工作区分组的顺序永远是 Host 注册表的持久顺序。本插件让最近有活动的那个工作区排到最前面。

Stars
1
Forks
0
Issues
0
更新
3 天前

PROJECT TOPICS

项目标签

INSTALL REFERENCE

安装参考

未验证
dsh plugin --profile web add github:karottc/dsh-plugin-workspace-sorted

该命令指向仓库当前默认分支;尚无绑定当前 commit 的完整验证结果。

PROJECT README

README

dsh-plugin-workspace-sorted

让 DeepSeek Harness 的 工作区(Workspace) 按「最近使用」排序的宿主(Host)插件。

侧边栏自带的「排序方式:手动排序 / 最近更新」只作用于会话;工作区分组的顺序永远是 Host 注册表的持久顺序。本插件让最近有活动的那个工作区排到最前面。

为什么需要它

三条来自 DSH 0.1.5-rc.2 源码的事实决定了实现方式:

  1. @deepseek-ai/dsh-client-ui-workspacederiveGroups() 接收 real workspaces in stable Host order——工作区分组顺序就是 ctx.workspaceRegistry.list() 的顺序,与会话排序选项无关。
  2. Host 侧只有 workspaceRegistry.insertBefore(id, beforeId?) 能改变这个顺序 (ctx.workspaceController.insertBefore 是它的 Remote 封装),而新建工作区只是 prepend,会话活动从不重排工作区。
  3. 浏览器端没有可用的排序扩展点:ui-workspace/client 只导出 apply/inject 与类型,WorkspaceBrowser 组件是包内私有的,workspaces 这个 root hook 也已被它占用。要加 UI 选项只能整体替换 sidebar.workspaces 席位(约 2800 行 UI)。

因此唯一受支持的接缝是 Host 注册表本身,本插件就驱动它。

排序语义

与客户端为「新会话」挑选最近活跃工作区的逻辑(ui-workspace 的 recentWorkspace()完全一致

  • 工作区的活跃时间 = 其成员会话中最大的活跃时间;
  • 没有任何成员会话时,回退到工作区自己的 createdAt
  • 排序为活跃时间降序,同分保持当前注册表位置(不会来回抖动)。

Host 侧一条会话的活跃时间是 max(header.createdAt, sessionListMetadata.lastPromptAt)——也就是侧边栏会话行上 显示的那个相对时间。所以「最近使用」看的是用户真正发过 prompt 的时间, 而不是任何一次后台事件。

数据来源:

来源 作用
ctx.sessionController.list()(可选服务,经 ctx.inject 等待就绪) 启动时播种,保证刚装好的首次启动就是正确顺序
session/created 在该工作区新建会话 = 使用了它
session/eventtype === 'user/message'source.kind === 'user' 用户真正发 prompt

事件只记录会话级时间戳,归属到哪个工作区是在 debounce 之后、 reconcile 时通过 workspace.sessionIds 惰性解析的——因为 attachSession() 是在会话发布之后才执行的。

安装与激活

插件包是两层结构,两者缺一不可,且激活行只能声明在其中一个层里

1) 安装

本包以源码仓库形式分发(未发布到 npm registry)。克隆到本机后,在本包所在 目录执行:

git clone <repo-url> dsh-plugin-workspace-sorted
cd dsh-plugin-workspace-sorted
dsh plugin --profile web add link:.

dsh plugin 在转发给 pnpm 之前会把 link:. 解析成该目录的绝对路径,因此 README 与包本身都不需要、也不应该写死任何本机路径——这份文档可以直接给别人用。

也支持让 dsh 直接从 git 安装(本包没有 prepare 构建步骤,所以不会被 pnpm 的构建白名单挡住):

dsh plugin --profile web add github:<owner>/<repo>

2) 激活

在 profile 自己的 ~/.dsh/profiles/web/cordis.patch.yml 里声明这一行:

- insert:
    - id: workspace-sorted
      name: dsh-plugin-workspace-sorted
      config:
        enabled: true
        debounceMs: 1500
        countSessionCreation: true
        includeSubagents: false
        verbose: false

为什么激活行不放在包的 patch 里

loader 会拒绝一份 id 重复的组合结果:

TypeError: duplicate loader entry id: workspace-sorted
  (cordis-plugin-loader, EntryTree.update)

这个 reject 发生在 Include 的初始化里,会直接让整个 profile 启动失败applyEntryPatches()insert 只是 data.push(...insert),不做去重, 所以同一 id 在两个层各 insert 一次必炸。

于是形成一条硬约束:

激活行放在哪 下次重启 不重启热加载
包的 cordis.patch.yml ❌ bundle 列表是启动时读的
profile 的 cordis.patch.yml patchReload: live 会重放这一层

本插件选择了后者:包的 cordis.patch.yml 故意是 [],激活行由 profile 层声明。 这样改一行就能热加载/热卸载,且下次重启仍然只有一份 id。 (如果更想要「装完即生效、必须重启」的自包含形态,把那一行搬回包的 cordis.patch.yml 即可——但两层不能同时声明。)

配置

patch 会替换整份 config,所以要保留的键必须全部重写:

- id: workspace-sorted
  config:
    enabled: true            # 总开关;false = 完全交回手动拖拽
    debounceMs: 1500         # 活动合并窗口(50–60000)
    countSessionCreation: true  # 新建会话是否算「使用了该工作区」
    includeSubagents: false  # 侧边栏隐藏的 subagent 会话是否参与
    verbose: false           # 每次实际重排写一条 info 日志

日志走 ctx.logger(DSH 官方约定)。默认的 dsh web 组合只把日志写进 Cordis 内存缓冲区,终端不打印;只有在组合里注册了 logger exporter (ctx.logger.exporter(...))时才会看到这些行。

验证

在本包所在目录下执行:

node --test              # 20 个用例
node tools/preview.mjs   # 只读 dry-run:当前顺序 vs 目标顺序 vs 计划移动数
dsh --profile web --dump-config | grep -c '^- id: workspace-sorted$'   # 必须是 1

tools/preview.mjs 读取真实的 ~/.dsh/storages/workspace.jsonsession_projcache/,跑的是插件里同一份纯函数,不写任何东西

已完成的实测记录

  • 单元 / 框架层(20/20 通过):排序算法、插件生命周期,以及两个跑在真实 Cordis 上的集成用例——在真实 root context 上挂载插件、解析 inject、 通过真实事件总线 emit 触发重排,以及 ctx.inject(['sessionController']) 在服务后于插件提供时仍能触发启动播种。

  • 真机端到端:在隔离的 DSH_HOME(建在用户目录下而非系统临时目录—— macOS 的临时目录是一条符号链接,会让 HMR 的路径匹配失效)里启动真实 dsh web,预置一份故意写反的注册表 ['older','newer'],然后 在宿主运行中把激活行追加进 profile patch。观察到的宿主日志:

    [spy info] workspace-sorted: Workspace order will follow most-recent activity
    [spy info] workspace-sorted: startup seed took 0 Workspace timestamp(s) from 0 Session(s)
    [spy info] workspace-sorted: reordered 1 Workspace row(s) -> bbbb… > aaaa…

    随后 workspace.json 落盘顺序变为 ['newer','older']。这同时证明了 热加载激活真实宿主内加载、以及真实的持久重排三条链路。

  • 重启安全dsh --profile web --dump-config^- id: workspace-sorted$ 计数为 1,即两个 patch 层没有重复 id

设计约束

  • 零运行时依赖lib/index.js 不 import 任何包,只与传入的 Cordis context 交互,因此作为 profile bundle 层加载时不存在自身模块解析问题。
  • 尽力而为、绝不伤主流程:两个事件监听器各自吞掉自己的异常—— session/created 是同步派发的,在那里抛异常会否决会话创建
  • 空闲零写入:推导顺序与当前注册表顺序一致时直接返回,不调用 insertBefore。所以「一直待在一个工作区里」不会反复写盘,安装瞬间 也不会产生任何写盘(本机首次 dry-run 就是「计划移动 0 次」)。
  • 拖拽会被覆盖:这是「按最近使用排序」的必然代价。enabled: false 或卸载插件即可恢复纯手动排序。

卸载 / 回滚

  1. 删掉 profile patch 里的那一行(热卸载,立即生效);
  2. 可选:dsh plugin --profile web remove dsh-plugin-workspace-sorted

两种方式都不会删除任何工作区、目录或会话;唯一被改动的持久数据是注册表里的 工作区顺序,而顺序本来就可以手动拖拽。

CLASSIFICATION EVIDENCE

分类依据

项目类型插件
功能分类其他
规则置信度

系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。当前命中: 无有效分类标签。