npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@yezack/dsh-temp-workspace

v0.2.6

Published

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

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)。

设置 → 临时工作区

插件在设置导航里注册了自己的独立页面(settings.section 槽位,id 即设置命名空间 dsh-temp-workspace,order 60)。取值存放在 profile patch 中 Loader 条目 dsh-temp-workspace 的 config 里(dsh 0.1.7 起旧的 settings.plugin.item 卡片插槽与 ~/.dsh/settings.yaml 均已移除,注册在旧槽位的卡片永远不会渲染):

| 设置 | 取值 | 默认 | 含义 | | --- | --- | --- | --- | | deleteMode | immediate / delayed | immediate | 下次启动时何时删除临时工作区 | | deleteDelay | 秒 | 3600 | delayed 模式下删除前的宽限期 | | confirmBeforeDelete | 开 / 关 | 开 | 删除前是否先询问用户 |

  • confirmBeforeDelete 开:宿主启动时绝不自动删除,而是挂起这些工作区, 浏览器弹出确认对话框,提供两个选项:
    • 删除 —— 清空被挂起的工作区(会话 + 注册 + 目录)。
    • 临时保留 —— 本次不动它们,但仍然是临时:它们还在标记里,会在之后的 启动时再次(按设置)被确认/清理。
    • 未作答时工作区原样保留到下次启动。

永久保留不在启动确认弹窗里——它有自己的按钮。侧边栏里每个临时工作区行 (标题「临时工作区」)在其「+」(New Session) 旁边有一个图钉按钮。点击后选取一个 目标文件夹:优先桌面端 __DSH_DIRECTORY_PICKER__ 原生桥,其次 uiWorkspace.pickDirectory()(0.2.x),最后 ctx.workspaces.pickDirectory()(0.1.x); 完全没有选择器时会明确告知,而不是留一个点了没反应的按钮。临时工作区的文件会 直接移进该文件夹(不会额外创建「临时工作区」子文件夹),该文件夹本身成为永久工作区, 并用其文件夹名作为工作区标题。同时迁移对话:把每个会话日志头行的 cwd 改写为该 文件夹路径,并把日志目录移到新 cwd 的 projectKey 槽位。迁移进行期间显示 「正在迁移工作区…」 提示(没有进度条;10 秒后会出现隐藏入口,避免提示把人困住), 请求超过 90 秒未回报也会给出可操作的说明。

迁移只有在整个进程重启后才会反映到工作区列表,而浏览器插件无法在桌面端重启宿主 (Electron 外壳持有窗口,杀掉里面的 web 宿主会让应用不可用)。因此成功后的对话框改为 说明如何手动重启:

  • Web 版:在终端里退出 dsh 进程,然后重新打开。
  • 桌面版:彻底退出桌面端,然后重新打开。
    • Windows:右键托盘区的 DSH 图标 → 退出。
    • macOS:直接按 Command+Q,或右键 Dock 栏里的图标 → 退出。

宿主仍保留 POST /restart(真正的宿主级重启,由脱离的辅助进程重新拉起),但 界面已不再调用它。

同一个页面还有一个 「立即清理」 面板:显示当前有多少个临时工作区,并可立即把它们 全部删除(先弹确认对话框列出清单),不必等到下次启动。它复用启动清理的同一套拆除流程, 所以在线会话会先被摘除、冷会话归档,日志、一次性目录、注册记录与标记条目都会被清掉。

  • confirmBeforeDelete 关:自动删除——启动时立即执行,或在 deleteMode = 'delayed' 时等待 deleteDelay 秒后执行。

清理是如何工作的

DSH 没有公开的「删除会话日志」API——持久化层是 append-only。所以临时工作区的删除分为四步:

  1. 先拆在线会话 —— 如果宿主进程里还活着该工作区的会话(确认弹窗、延迟自动删除、或只刷新了页面而宿主并未真正重启时,删除都会在仍持有这些会话的宿主里运行),会尽力拆除它们: 先取消 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 记录由启动清扫回收。
  2. 再删会话 —— 工作区所属的每个会话(其头行 cwd 等于工作区路径)通过 ctx.sessionPersistence.locate 定位,并用 fs.rm 删除其磁盘目录。
  3. 删注册 —— ctx.workspaceRegistry.delete(id) 删除工作区记录(本身不动日志/目录)。
  4. 删目录 —— 删除临时目录(位于 <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 } | 立即删除一个临时工作区(会话 + 注册 + 目录) | | POST cleanup | { deleted, requested, remaining } | 立即删除全部临时工作区——与启动清理同一套拆除流程;设置页的「清理全部临时工作区」按钮调用它 | | 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/client root 渲染(不依赖 primitives 的 CSS-module 作用域,所以必定显示)。
  • 永久保留需要目录选择器,解析顺序:桌面端 __DSH_DIRECTORY_PICKER__ 原生桥 → uiWorkspace.pickDirectory()(0.2.x)→ ctx.workspaces.pickDirectory()(0.1.x)。 这里刻意不用 ctx.remote.directoryPicker:命名空间化的 remote 需要单独声明 inject,否则 cordis 会抛错。三者都不可用时会弹窗明确告知,而不是留一个点了没反应的按钮。
  • 对话迁移只改写每个会话日志的头帧(.jsonl.zstd 是一串独立压缩的 Zstandard 帧 连接而成——帧 0 是带 cwd 的头行,其余是事件批次)。头帧的 cwd 被改写,每个事件帧 逐字节保留,因此不会丢失任何对话内容。
  • dsh 0.2.x 的日志文件名带格式代次(session.v4.jsonl.zstd),同一会话目录里还留着 更早的代次(session.v3.jsonl.zstd、无代次的 session.jsonl[.zstd])。因此迁移按 文件族匹配(/^session(?:\.v[1-9]\d*)?\.jsonl(?:\.zstd)?$/),把每一个代次都 带过去,帧结构无法解析的代次原样拷贝而不是丢弃。
  • DSH 会话头中的 cwd 不可变,故采用改写方式。DSH 工作区注册表的「会话→工作区分组」 (bootstrap)只在第一次启动时运行一次;之后的启动只会重建 id→cwd 索引,不会把 迁移后的会话加进新工作区的 sessionIds。为避免迁移的对话最终变成未分组,插件在 移动时记录一条持久化的 pending-attach 列表,并在下次启动时把这些迁移会话重新挂载到 新工作区——这也是永久保留后客户端提示重启的原因。

不准备修复的Bug

  • 受到Harness的限制,永久保留后对话有概率变成Ungrouped,已经尽力处理了,但是仍然有概率出问题
  • 创建临时工作区后永久保留相关按钮不一定会立即出现,但是肯定会在一段时间后(2-3s后)出现的,不准备修复
  • 受到Harness的限制,迁移过程完成后对话的名称有概率变成工作区的名称,点击后又变回原来的名称,不影响使用,不计划修复,自己改一下名应该就可以处理这个问题

界面语言

所有界面文案(chip、行内“永久保留”按钮、设置卡片、确认/重启弹窗)都提供中文与英文 两套字典,并跟随 DSH 的激活语言自动切换(ctx.locale + locale.subscribe),语言切换时 已注入的按钮文案也会刷新。