dsh-temp-workspace
Verified@yezack/dsh-temp-workspace · v0.2.4 · MIT · Web UI
DSH web client plugin: a temporary workspace (临时工作区) — every conversation it contains is deleted at the next Harness restart (immediately or after a delay), optionally behind a confirm dialog. Temp workspaces are named after their creation moment and grou
Install
dsh plugin add @yezack/dsh-temp-workspace Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
@yezack/dsh-temp-workspace
一个 DSH web 客户端插件,提供 临时工作区(临时工作区) 功能。
点击空白新会话页「选择工作区」旁的 新建临时工作区 chip,即可创建一个一次性工作区(以创建时刻命名,如 2026-10-16 09:45)并打开一个全新的对话。它会出现在「工作区」列表里一个可收起的 临时工作区 分组中,因此不会和你的正式项目混在一起。下次 Harness 重启时,这个插件会删除该工作区下的全部对话以及工作区本身——临时工作区不留任何痕迹。
它是什么
- 宿主半边(
lib/index.js)—— 一个 Cordis 插件,注入webServer/workspaceRegistry/sessionPersistence,注册受浏览器信任围墙保护的/temp-workspace/api路由,并在启动时驱动清理逻辑。dsh 0.1.x 与 0.2+ 都能跑:信任判定在 0.2+ 取自connection.allowsRemoteAuthorities,旧版回落到只有 0.1.x 才有的webRuntime.trustedHosts——webRuntime刻意不列为注入依赖,因为 dsh 0.2 移除了它, 而注入不满足会让整个插件不激活。 - 浏览器半边(
src/client.js→lib/client.js)—— 在原生「工作区」列表内渲染可收起的 临时工作区 分组(数据来自本插件自己的路由,以及workspaces/uiWorkspace/sessions三个客户端服务);在空白新会话页挂一个新建临时工作区chip; 注册一张 设置 → 插件 卡片;并在设置开启时于启动阶段弹确认对话框。装了dsh-better-workspace时不注入侧栏分组,改用它的/名称分组(见下文)。
「临时工作区」分组
工作区
▤ 临时工作区 (2) ← 一个可收起的分组行,固定排在列表最前
📁 2026-10-16 09:45 ← 一个临时工作区
会话 A
会话 B
📁 2026-10-16 10:12
会话 C
📁 某个项目
📁 未分组
- 分组固定是列表的第一行,并且做了负缩进(
padding-left: 0),因此看起来是下面那些工作区行的父级,而不是又一个工作区;它的子行仍用树的正常缩进(24px / 40px)。 - 它前面不是工作区行那种文件夹图标,而是一个状态图标:闭合的收纳盒=已收拢,打开的收纳盒=已展开。一个图标同时承担"这是临时工作区分组"和"当前展开/收拢状态",不再另设旋转小三角。
- 点击整行即可收起/展开分组;该状态会跨页面刷新记住(浏览器本地
localStorage)。 - 悬停某个临时工作区行,会出现 永久保留、新建会话、删除 三个操作;分组行自己的 + 用于再建一个临时工作区。
- 点击会话即可像原生行一样打开它。
与 dsh-better-workspace 共存
dsh-better-workspace 接管侧边栏的工作区树,并用工作区标题里的 / 生成虚拟分组(web/前端 归到 web 组)。检测到它(客户端启动清单里有 dsh-better-workspace,另以 .bw-tree 作兜底探测)时,本插件改用它的分组机制、不再往侧边栏注入自己的 UI:
- 临时工作区标题改为
临时工作区/yyyyMMddHHmmss(如临时工作区/20261021175513),better-workspace 会把它们自动收进同一个临时工作区虚拟分组;秒级时间戳本身就能区分同一天内的多次创建,同一秒内仍撞名时才会补-2、-3…… 后缀。 - 侧栏注入全部关闭:不再绘制本插件的「临时工作区」分组、不再隐藏原生临时行、也不再每 4 秒轮询列表。分组/重命名/删除/排序都用 better-workspace 自己的界面;删除仍会走本插件的清理钩子(会话 + 目录 + 标记一并清掉),重命名走 registry 的
rename(与 better-workspace 自己的改名同一通道)。 - 空白新会话页的
新建临时工作区chip、启动确认弹窗、设置卡片都不受影响。 - 前提:better-workspace 的 视图选项 → 工作区分组 需设为 磁盘 + 名称 或 仅名称(
/才会被当作分组);它默认的 磁盘目录 不解析/,此时只会平铺显示临时工作区/20261021175513。
新建会话
- 点「新建会话」进入空白新会话页,不再自动绑定工作区。 原生行为是:侧边栏的「新建会话」按钮(以及
session.new快捷键)调用不带参数的uiWorkspace.startSession(),于是按"当前会话所在工作区 → 最近活跃工作区"自动绑定。本插件把这不带参数的调用改成清空主面板(即空白新会话页),让你在那里自己挑工作区。带明确workspaceId的调用仍保持原生行为(本插件自己的临时工作区行就依赖它)。该拦截以 属性访问器 的形式安装,而不是一次性赋值:其它插件稍后包装同一个startSession时(例如@linxin666/dsh-client-ui-git-graph的 auto-isolation 会先自行解析出工作区、再以显式 id 转发),setter 会捕获这次替换并把策略重新包在最外层,因此"不带参数"的语义不会再被旁路。同时第二个参数原样透传:startSession(void 0, { agentPreset })这类带草稿选项的调用保持原生含义。 - 该页面控制行里多了一个
新建临时工作区chip,就在原生选择工作区与 Agent 预设 chip 旁边(度量完全一致:高 28px、内边距 8px、圆角 8px、字号 13px/500)。点它即创建一个临时工作区并在其中打开对话 —— 与「临时工作区」分组行的 + 同一动作。 - 一旦为这次新会话选中了工作区,该 chip 就消失(选择已经做完了)。状态直接取自原生 chip:它的
aria-label恒为"选择工作区"这个行动号召,而可见文案在选中后变成工作区名 —— 所以不比较任何语言字符串。
安装方式
从 npm 安装(预构建,推荐)
该包已发布到 npm,lib/ 为预构建产物,安装无需构建、可跳过 pnpm 的 allowBuilds 授权——一条命令搞定:
dsh plugin --profile web add @yezack/dsh-temp-workspace
dsh plugin … add 会把该 spec 转发给 profile 目录里的 pnpm,并代为合并 dsh.profile.bundles。因为清单声明了 dsh.bundle.patch,包会自动加入 Loader 层栈,下次启动 dsh web 时宿主把它组合为 Loader 条目、注册 /temp-workspace/api 路由、设置卡片进入 __DSH_BOOT__。
从 GitHub 安装
直接从 GitHub 安装,使用 dsh plugin 转发器(在 profile 目录里 pnpm clone + add,并自动合并 bundle 层):
# 启动某个 profile,例如 web 应用
dsh plugin --profile web add github:yezack/dsh-temp-workspace
因为清单声明了 dsh.bundle.patch,add 会把该包自动追加到 dsh.profile.bundles,下次启动 dsh web 时宿主就把它组合为 Loader 条目、注册 /temp-workspace/api 路由、设置卡片进入 __DSH_BOOT__。
若想自己驱动 pnpm,可在 profile 目录里执行同一 spec:
cd "$DSH_HOME/profiles/web"
pnpm add github:yezack/dsh-temp-workspace
要固定到某个分支/标签/提交,按 pnpm 的 git ref 语法追加:
dsh plugin --profile web add github:yezack/dsh-temp-workspace#<ref>
构建后的客户端 bundle(
lib/client.js)已提交在仓库里,所以 git 安装不需要构建步骤。要改它,克隆仓库后用node build.mjs --watch(见开发循环),或改为从本地 checkout 安装(见接入 profile)。
设置 → 插件
插件在 设置 → 插件 注册一张卡片(以设置命名空间 dsh-temp-workspace 为键,
持久化在 ~/.dsh/settings.yaml):
| 设置 | 取值 | 默认 | 含义 |
|---|---|---|---|
deleteMode |
immediate / delayed |
immediate |
下次启动时何时删除临时工作区 |
deleteDelay |
秒 | 3600 |
delayed 模式下删除前的宽限期 |
confirmBeforeDelete |
开 / 关 | 开 |
删除前是否先询问用户 |
confirmBeforeDelete开:宿主启动时绝不自动删除,而是挂起这些工作区, 浏览器弹出确认对话框,提供两个选项:- 删除 —— 清空被挂起的工作区(会话 + 注册 + 目录)。
- 临时保留 —— 本次不动它们,但仍然是临时:它们还在标记里,会在之后的 启动时再次(按设置)被确认/清理。
- 未作答时工作区原样保留到下次启动。
永久保留不在启动确认弹窗里——它有自己的按钮。侧边栏里每个临时工作区行
(标题「临时工作区」)在其「+」(New Session) 旁边有一个图钉按钮。点击后选取一个
目标文件夹(通过 ctx.uiWorkspace.pickDirectory() 的系统目录选择器)。临时工作区的文件会
直接移进该文件夹(不会额外创建「临时工作区」子文件夹),该文件夹本身成为永久工作区,
并用其文件夹名作为工作区标题。同时迁移对话:把每个会话日志头行的 cwd 改写为该
文件夹路径,并把日志目录移到新 cwd 的 projectKey 槽位。成功后弹出一个不可关闭的
对话框,带 「立即重启」 按钮——它触发真正的宿主级重启(一个脱离的辅助进程按原
DSH 启动命令重新拉起,机制与 dsh-market 更新插件时一致),让注册表重新索引、迁移的对话
出现在新工作区下。
confirmBeforeDelete关:自动删除——启动时立即执行,或在deleteMode = 'delayed'时等待deleteDelay秒后执行。
清理是如何工作的
DSH 没有公开的「删除会话日志」API——持久化层是 append-only。所以临时工作区的删除分为四步:
- 先拆在线会话 —— 如果宿主进程里还活着该工作区的会话(确认弹窗、延迟自动删除、或只刷新了页面而宿主并未真正重启时,删除都会在仍持有这些会话的宿主里运行),会尽力拆除它们:
先取消 agent 并等它回到空闲(
whenIdle,镜像 agent loop 自身的销毁顺序),再先把待写内容落盘 (sessions.flush),最后从会话存储中摘除该条目(发出session/disposed,客户端立即关闭该对话)。 先 flush 再摘除对多会话工作区至关重要:摘除会触发session/disposed,持久化协调器随之做一次 "退休刷新"(retire → flush → initFor),若会话还有未落盘的写入,这次刷新会在目录被删除后把日志 重新物化出来——那些会话就会变成不可读取的 Ungrouped 残留。先 flush 后,退休刷新无内容可写,日志 只会被删除一次。 0b. 归档冷会话 —— 本轮宿主里从未被打开(未 attach)的会话只有磁盘日志,删日志后浏览器收不到session-removed事件,会在侧边栏残留成不可读取的 Ungrouped 条目。archiveColdSessions在删日志 之前把它们逐一归档(workspaceRegistry.archiveSession):归档集变化会推送host/archived-sessions-changed,侧边栏(分组、扁平列表、搜索)都会过滤归档 id,残留条目立即消失。 归档留下的 archived-id 记录由启动清扫回收。 - 再删会话 —— 工作区所属的每个会话(其头行
cwd等于工作区路径)通过ctx.sessionPersistence.locate定位,并用fs.rm删除其磁盘目录。 - 删注册 ——
ctx.workspaceRegistry.delete(id)删除工作区记录(本身不动日志/目录)。 - 删目录 —— 删除临时目录(位于
<dsh-home>/temp-workspaces/<uuid>)。
插件用 <dsh-home>/temp-workspaces/state.json 记录临时工作区(持久化标记
{ workspaceId, path, createdAt })。删除由该标记里的 path 驱动,所以重启前已通过
UI 删除注册记录的临时工作区仍会清理残留目录(修复了「残留文件夹」bug)——目录以
标记路径(而非注册记录)为准。
零残留:删除时还会清理会话的投影缓存行(session_projcache 域,位于
~/.dsh/storages/session_projcache.json)与旧布局遗留的 per-session 缓存文件。
启动时还有一次孤儿残留清扫(pruneOrphanTempResidue):凡 cwd 位于临时根目录下、
且不再被标记引用的投影缓存行,连同其 archived-id 记录与遗留缓存文件一并删除。这些行
永远不会把幽灵对话显示出来(身份校验后读不到),但"不留痕迹"就是不留痕迹;用户对真实
会话的主动归档不会被触碰。
目录结构
temp-workspace-plugin/
package.json name, dsh.bundle.patch + dsh.client, exports
cordis.patch.yml 把该插件插入为宿主 Loader 条目
lib/index.js 节点半边 —— /temp-workspace/api 路由 + 设置 + 启动清理
src/client.js 浏览器半边源码(临时工作区分组/chip + 设置卡片 + 确认弹窗)
lib/client.js 构建后的浏览器半边(生成文件,勿改)
build.mjs 把 src/client.js 包装 -> lib/client.js(HMR watch)
开发循环
node build.mjs --watch
lib/client.js 是 client-modules 提供、dsh-client-hmr 轮询的唯一文件;编辑
src/client.js 会热更新界面(无需刷新页面)。宿主半边改动(lib/index.js)需要重启
dsh web。
接入 profile
# 本地 checkout / clone(任意绝对或相对路径)
dsh plugin --profile web add /path/to/dsh-temp-workspace
这会把它加入 dsh.profile.bundles。下次启动 dsh web 时宿主组合该行、
注册 /temp-workspace/api 路由、设置卡片进入 __DSH_BOOT__,并运行启动清理/确认流程。
API(/temp-workspace/api/<method>)
读取用 GET,变更用 POST。
| 方法 | 返回 | 说明 |
|---|---|---|
POST create |
{ workspace, created: true } |
mkdir + workspaceRegistry.create + 标记 |
GET list |
{ entries } |
当前标记为临时的 [{ workspaceId, path, createdAt }] |
POST delete |
{ ok: true } |
立即删除一个临时工作区(会话 + 注册 + 目录) |
GET/POST config |
{ ok, value } |
读取设置,或 POST {…} 写入补丁 |
POST configReset |
{ ok, value } |
恢复所有设置为默认 |
GET pending |
{ ok, value } |
被挂起的工作区 + confirmBeforeDelete + deleteAt |
POST confirm |
{ ok, deleted } |
用户确认——删除被挂起的集合 |
POST keep |
{ ok, kept } |
临时保留——不动作(仍为临时) |
POST permanentKeep |
{ ok, moved } |
{ target, workspaceId? } —— 把 target(所选文件夹)直接作为新工作区并去临时化;带 workspaceId 时只移动该工作区(省略则移动全部被挂起的) |
POST restart |
{ restarting, pid, helperPid } |
真正的宿主重启(仅限同源 loopback) |
说明 / 局限
- 侧边栏工作区浏览器没有给合成工作区分组的 slot,空白新会话页的控制行又是三个
single slot 的组合,所以 临时工作区 分组与
新建临时工作区chip 都是用MutationObserver注入 DOM 的(与dsh-workspace-auto-approval同款写法)。占用其中任何一个 slot 都会把系统自带 UI 顶掉。临时工作区的原生行用display:none隐藏 ——从不移动,本插件自己的子树也只会被追加为行尾子节点(React 不会引用它), 因此原生树的协调过程完全不受影响。若上游侧边栏或首屏布局改变,这些位置可能需要相应调整。 - 原生工作区搜索仍可能搜到临时工作区的对话;被重组的只是列表行。
- 清理时直接删除会话日志目录。在线会话会在删除时尽力拆除(取消 agent + 归档 + 从会话存储 摘除),因此即使删除发生在仍持有这些会话的宿主进程里(确认弹窗、延迟删除、页面刷新),对话也会 立即从界面消失;启动时没有在线会话,正是清理运行的时候。
- 临时工作区的目录位于 DSH home(
~/.dsh/temp-workspaces),不在你的常规项目文件夹里。 - 浏览器插件没有通用的「询问用户」API,因此确认弹窗由插件自绘(轻量内联样式遮罩),
通过自己的
react-dom/clientroot 渲染(不依赖 primitives 的 CSS-module 作用域,所以必定显示)。 - 永久保留需要
native目录选择器能力(ctx.uiWorkspace.pickDirectory()); 在远程/非 native 部署下该选择器不可用,按钮会被禁用。 - 对话迁移只改写每个会话日志的头帧(
.jsonl.zstd是一串独立压缩的 Zstandard 帧 连接而成——帧 0 是带cwd的头行,其余是事件批次)。头帧的cwd被改写,每个事件帧 逐字节保留,因此不会丢失任何对话内容。 - DSH 会话头中的
cwd不可变,故采用改写方式。DSH 工作区注册表的「会话→工作区分组」 (bootstrap)只在第一次启动时运行一次;之后的启动只会重建 id→cwd 索引,不会把 迁移后的会话加进新工作区的sessionIds。为避免迁移的对话最终变成未分组,插件在 移动时记录一条持久化的 pending-attach 列表,并在下次启动时把这些迁移会话重新挂载到 新工作区——这也是永久保留后客户端提示重启的原因。
不准备修复的Bug
- 受到Harness的限制,永久保留后对话有概率变成
Ungrouped,已经尽力处理了,但是仍然有概率出问题 - 创建临时工作区后永久保留相关按钮不一定会立即出现,但是肯定会在一段时间后(2-3s后)出现的,不准备修复
- 受到Harness的限制,迁移过程完成后对话的名称有概率变成工作区的名称,点击后又变回原来的名称,不影响使用,不计划修复,自己改一下名应该就可以处理这个问题
界面语言
所有界面文案(chip、行内“永久保留”按钮、设置卡片、确认/重启弹窗)都提供中文与英文
两套字典,并跟随 DSH 的激活语言自动切换(ctx.locale + locale.subscribe),语言切换时
已注入的按钮文案也会刷新。