whybebabo/dsh-better-workspace

dsh-better-workspace

在原生工作区列表之上叠加主题分组:emoji 分组文件夹、筛选与搜索、批量归档,以及带安全边界的永久删除。

未分類cordis-plugindeepseek-harnesssidebar

dsh-better-workspace

English | 简体中文

dsh-better-workspace 是 DeepSeek Harness 的侧边栏主题分组插件。它叠加在原生"工作区"区域之上,不替换官方列表:会话仍由宿主管理,插件只在这块区域上重新组织显示,并补上宿主没有的批量操作与安全删除。

有了它,散落在各个工作区里的会话可以按自己的习惯归类:

  • 主题分组:每个工作区内部是一棵树,用 emoji 标记的"分组"文件夹收纳会话,另有系统默认分组与「已归档」桶;
  • 筛选与搜索:工作区下拉筛选、面板内本地搜索、「只看此分组」;
  • 批量操作:归档 / 取消归档 / 移动到分组 / 永久删除,在选择模式下由标题栏一行原生图标按钮承载;
  • 安全删除:DSH 本身没有删除会话的命令,插件自行实现,规则是"宁可拒删"(见永久删除的安全边界);
  • 与原生一致:状态点、置顶上浮、行内悬浮卡、行尾操作条、定时任务时钟、图标与配色,都对齐宿主自己的实现。

插件不会接管 DSH 的其余部分:侧栏折叠、窄视口或路由不可达时,面板会完全隐藏并交还原生 UI(见失效保护)。

界面预览

工作方式

  • 叠加而非替换:面板渲染在 shell.overlay,用结构选择器定位原生工作区区域,覆盖矩形与该区域自身盒子重合。被覆盖区域加上 inert + aria-hidden,官方弹窗出现时自动释放。
  • 主题数据单独存放:分组与归属关系写在 /dsh-better-workspace/topics.v1.json 这个单文件 sidecar 里,可直接备份、可与宿主升级解耦;卸载插件后数据仍在。
  • 顺序分两种:分组顺序写入 sidecar(跨窗口共享);工作区与会话的拖拽顺序存在本地偏好 localStorage。
  • 全部走官方命令:打开会话、归档、置顶、重命名、分叉、建工作区都调用宿主 uiWorkspace / workspaces 服务,插件不声明、不借用其它包的插槽。

安装

环境要求

  • DeepSeek Harness Desktop 或 Web 0.2.1-rc.x,@deepseek-ai/cordis 4.0.5;
  • Node.js ≥ 22.19;
  • 本机可用 Git(以 github: 方式安装时)。

从 npm 安装(推荐)

dsh plugin --profile web add @whybebabo/dsh-better-workspace

DSH 会把插件装进 Web profile,并把 @whybebabo/dsh-better-workspace 注册到 dsh.profile.bundles,不需要克隆仓库、手动安装依赖或构建客户端。

没有全局 dsh 命令时,用 npx 执行:

npx @deepseek-ai/dsh plugin --profile web add @whybebabo/dsh-better-workspace

安装命令需要本机可以使用 Git 与 pnpm;使用 npx 时还需要 Node.js。

Desktop 会修改 desktop profile,装之前建议先备份 cordis.patch.yml / package.json / pnpm-lock.yaml:

dsh plugin --profile desktop add @whybebabo/dsh-better-workspace

从 GitHub 安装

不经过 npm,直接从仓库安装某个发布版本:

dsh plugin --profile web add github:whybebabo/dsh-better-workspace#v0.1.6

把 #v0.1.6 换成你要的 tag。这条路径同样需要 Git 与 pnpm,也不需要克隆仓库。

从源码安装

需要改代码或跟 main 分支时:

git clone https://github.com/whybebabo/dsh-better-workspace.git
cd dsh-better-workspace
npm install && npm run build

先在不影响现有 profile 的隔离环境里验证一遍:

dsh --profile topic-e2e --from-default-profile web --dump-config
dsh plugin --profile topic-e2e add "link:$(pwd)"
dsh --profile topic-e2e --no-open --port 8791      # 打印带 token 的 URL

确认无误后装到日常使用的 profile:

dsh plugin --profile web add "link:$(pwd)"

以 link: 方式安装时,宿主读取的始终是当前目录的产物:改完代码 npm run build 并重启即可,不必重新 add。

启动或重启 DSH

安装后关闭旧的 DSH Web 进程并重新启动:

dsh web

使用 npx 的启动方式:

npx @deepseek-ai/dsh web

打开侧栏,点「工作区」标题旁出现的 ▾,或把鼠标移到「工作区」上——看到工作区下拉与分组树,说明插件已加载。

升级

发布新版本后重新执行安装命令并替换版本:

dsh plugin --profile web add @whybebabo/dsh-better-workspace

随后重启 DSH。以源码方式安装的则用:

git pull && npm install && npm run build

卸载

dsh plugin --profile web remove @whybebabo/dsh-better-workspace

主题数据保留在 /dsh-better-workspace/,需要清除请手动删除该目录。

验证安装

插件在宿主上注册了只读的 GET /state。浏览器访问(端口换成你实际使用的):

Invoke-WebRequest 'http://127.0.0.1:3080/dsh-better-workspace/api/state' | Select-Object StatusCode

预期状态码 200,返回体里 ok: true、result.workspaces 是你的分组数据。

该路由只接受本机回环 + 同源请求,用别的机器访问会拿到 403,这不是故障。

用法

分组

  • 新建分组:工作区 ⋯ 菜单,或标题栏 + 菜单里的「新建主题并放入…」;也可以把会话拖到目标分组来实现归类。新分组默认排在该工作区分组列表的最上面,创建后不用再滚到底部去找。
  • 新建会话:分组行尾的 + 直接在该分组下新建会话(工作区行尾的 + 则在该工作区新建)。
  • 改名:双击分组名直接内联编辑(重名与非法字符会即时提示并禁用保存)。
  • 排序:拖动分组行,拖到目标行的上/下半区决定插入位置,拖动时显示插入指示线。
  • 只看此分组:分组 ⋯ 菜单里的筛选会同时锁定它所属的工作区——同名分组不会跨工作区串味。
  • 删除分组:只清除归属,组内会话回到系统默认分组,不删会话。

每个工作区都有系统默认分组,不能改名或删除,但可以像普通分组一样拖动排序。

会话

  • 双击重命名(与桌面端一致);
  • 悬停出现原版同款悬浮卡:完整标题、相对时间、空闲/进行中/待回答状态,已归档则显示归档标记;
  • 悬停被截断的标题:与原生一致,标题以约 30 px/s 匀速滚到末尾后停在末尾(不回弹、不来回摆);移开鼠标立刻回到开头。溢出不足 8px 时不滚(否则看起来像抖动),开启「减少动态效果」时直接跳到末尾;
  • 行尾 hover 出现 ⋯ / 归档 / 置顶,悬停时行内时间让位;
  • 拖拽:拖到别的工作区会拒绝(护栏),拖到别的分组则直接归入;
  • 置顶会真正上浮到所属分组顶部(按宿主置顶顺序,最近置顶在前)。

新建会话落在哪里

  • 工作区行的 +、rail 上的「新建会话」:会话不被写入任何分组归属,因此留在系统默认分组里,并按原生顺序排在该桶最前面——与官方列表一致;
  • 标题栏 + →「新会话」:先选工作区与分组(默认分组也在列表里),再建会话并按所选归组;
  • 分组行的 +、+→「新会话」里显式选中的分组:先建会话,再把它归入该分组;
  • 正在「只看此分组」时新建:归入当前筛选的那个分组(此时它就是你正在看的地方);只有新空白确实属于该工作区时才会写入,另一个窗口在别的工作区新建的空白不会被误归;
  • 早先版本会把隐式新建的会话塞进「最近创建的分组」;分组当时按创建时间升序渲染,那个分组通常排在最后,于是新会话直接掉到了工作区最底部。该行为已移除;现在新建分组本身默认置顶,更不会把新会话带到列表末尾。
默认分组本身也能被拖到别的位置(与原生一致:它只是「不能改名、不能删除」,排序不受限)。把它拖到下面时,不归组的会话会跟着它排在那个位置——这是你自己排的序,不是插件又把新会话送到底部;拖回顶部即可恢复。

手动排序与新建会话

拖动会话产生的顺序只对它点名过的 id 生效:这些 id 在各自原有的槽位上互相置换,未出现在手动顺序里的会话(例如拖拽之后才新建的那个)保持宿主给的位置,不会被降级到末尾。这样「先手动排一次,再新建会话」不会把新会话挤到列表底部。

批量操作

点标题栏的「选择」进入选择模式,选择模式接管标题栏:退出、已选数量、归档/取消归档/移动/永久删除排成一行原生图标按钮,窄侧栏不折行。

  • 选择混入已归档条目时,归档与移动置灰;混入未归档条目时取消归档置灰;混入未分组会话时移动置灰;
  • 工具栏下方直接说明置灰原因,悬停也有提示;
  • 暂存中的「新会话」占位行不参与批量选择;
  • 「全选」只包含当前显示出来的行:分组头复选框、分组 ⋯ 菜单、工作区 ⋯ 菜单走同一个派生(selectableIds),超过「显示全部 60 条」的分组会提示「已选中 N 条;另有 M 条未显示」,展开后再选即可——不会悄悄把没渲染过的会话带进批量删除;
  • 另一窗口删掉的会话会从批量目标里剔除,不会再让整批停在「失败:no such session」;
  • 退出选择模式自动清空选择。

/delete/* 之外的操作逐条执行,失败可重试,逐条返回结果。

永久删除

删除会移除磁盘上的会话记录,无法恢复(工作区文件本身不受影响)。流程是:调 /delete/preview 拿逐条判定 → 用户在对话框里确认 → 调 /delete/execute 执行。

  • 会话正在运行 → 先按官方方式 agent.cancel({kind:'user'}) 并等待离开 running(上限 3 秒),停不下来就不删;
  • 有子智能体会话 → 拦下,请删除主会话(会级联清理并在 preview 中计数);
  • 用户分叉(带 parentSession、origin 为空)是顶层会话,可单独删除,不会被当成子智能体;
  • 删除当前正在看的会话后,自动切到一个新的空白会话。

折叠态与快捷键

侧栏折叠成窄轨后,rail 上的「新建会话 / 添加工作区 / 搜索」仍走宿主旧实现,插件在捕获阶段接管:新建会话先展开侧栏再调用与展开态完全相同的 uiWorkspace.startSession();「添加工作区」只展开侧栏、不吞事件,宿主的目录选择照常继续;搜索则展开侧栏并打开面板自己的搜索框。

数据与文件

位置 · 作用 /dsh-better-workspace/topics.v1.json · 分组与归属关系(单文件,可直接备份) /dsh-better-workspace/topics.v1.json.bak · 上一次成功写入的副本 /dsh-better-workspace/topics.v1.json.corrupt- · 主文件无法解析时保留的损坏副本,供人工抢救 /dsh-better-workspace/topics.v1.lock · 跨进程写锁(内容为 - 令牌) localStorage["dsh-better-workspace:prefs:v1"] · 工作区/分组筛选、折叠、搜索、拖拽顺序

写入采用 临时文件 → fsync → rename,并在覆盖前保留 .bak。主文件损坏时自动回退备份并进入只读模式(面板顶部横幅提示);回退期间发生的那次写入不会再拿损坏的主文件覆盖 .bak。主文件与备份同时不可解析时拒绝一切写入(不会用空状态覆盖磁盘),修好文件后只读模式自动解除,无需重启宿主。

写锁释放前校验令牌,只有持有者能删锁;mtime 超过 staleLockMs 且令牌中的 PID 已不存在的锁才会被回收。回收先用硬链接把当前锁固定成一份私有 claim,再核对 claim 的令牌与 inode/dev 仍是最初那把旧锁,确认后才 unlink 原路径;没有硬链接的文件系统退回 rename,并在原路径被后继者占用时归还而不覆盖。因此慢写入不会被判成过期而被顶掉;默认过期窗口(2 秒)短于等待上限(6 秒),崩溃留下的锁不会让每个请求都在可回收前先超时。

主题规则:一个会话最多属于一个分组;分组按工作区隔离;删除分组只清除归属;每个工作区(包括尚未写入 sidecar 的新工作区)都有一个虚拟补齐的系统默认分组;归档/取消归档不改变归属;删除会话时同步清理其归属记录。虚拟「未分组」只承接没有工作区的未归档会话;已归档会话只出现在所属工作区的「已归档」桶里。

工作区归属自愈

会话属于哪个工作区由宿主决定:artifact header 的 cwd 规范化后等于某个工作区路径,就应被该工作区的 sessionIds 记账。宿主只在自己的 session.create 路径上做这件事,以下情形会留下「cwd 对得上、但没人记账」的会话:

  • 工作区被删除后又重建(sessionIds 随旧记录一起清掉);
  • header 写完、attachSession 之前宿主崩溃或窗口被关;
  • 会话经由其它路径恢复(例如沿用了旧的 blank 记录)。

原生列表把这类会话当作「没有工作区承接」,显示在末尾的「未分组」里。这不丢数据,但看起来就像丢了——尤其卸载插件后仍停在未分组。插件在面板首次上屏时调用一次 /sessions/adopt,只用宿主的官方 workspace.attachSession(id) 把这些且仅这些会话归位(该命令会重新校验 cwd、拒绝不匹配、并把 id 前插,所以归位后正好落在工作区最前面,和新建会话一样)。工作区路径会先 realpath 规范化,因此通过 symlink 拼写注册的工作区也能匹配到会话。子智能体、已归档会话、已有归属的会话、cwd 无法解析的会话一律不碰;被宿主拒绝的逐条记入 failed,不强行写。面板会提示「已把 N 个会话归回它们的工作区」。