- Home
- Plugins
- Tools & Capabilities
- dsh-temp-workspace
dsh-temp-workspace
yezack/dsh-temp-workspace
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 --profile web add github:yezack/dsh-temp-workspaceREADME
@yezack/dsh-temp-workspace
A DSH web client plugin that adds a temporary workspace.
Click the 新建临时工作区 chip beside the blank New Session page's Workspace
picker to create a throwaway workspace — named after the moment you created it
(2026-10-16 09:45) — and open a fresh conversation in it. It shows up inside a
collapsible 临时工作区 group in the ordinary Workspace list, so temporary
work never mixes with your real projects. On the next Harness restart this
plugin deletes the conversations under that workspace and the workspace
itself — the temp workspace leaves no trace.
What it is
- Host half (
lib/index.js) — a Cordis plugin that injectswebServer/workspaceRegistry/sessionPersistence, registers a browser-trust-fenced/temp-workspace/apiroute, and drives the cleanup at boot. Works on dsh 0.1.x and 0.2+: the trust verdict comes fromconnection.allowsRemoteAuthoritieson 0.2+, and from the 0.1.x-onlywebRuntime.trustedHostsotherwise —webRuntimeis deliberately not an injected dependency, because dsh 0.2 removed it and an unsatisfied injection would keep the whole plugin from activating. - Browser half (
src/client.js→lib/client.js) — renders the collapsible 临时工作区 group inside the native Workspace list (fed by this plugin's own route plus theworkspaces/uiWorkspace/sessionsclient services), places a新建临时工作区chip on the blank New Session page, registers a Settings → Plugins card, and shows a confirm dialog at boot when the setting is enabled. Withdsh-better-workspaceinstalled it leaves the sidebar group to that plugin's/-name grouping instead (see below).
The 临时工作区 group
工作区
▤ 临时工作区 (2) ← one collapsible group row, first in the list
📁 2026-10-16 09:45 ← a temp workspace
会话 A
会话 B
📁 2026-10-16 10:12
会话 C
📁 some-project
📁 未分组
- The group is the list's first row and is out-dented (
padding-left: 0), so it reads as the parent of the workspace rows below it rather than as one more workspace. Its children keep the tree's normal indents (24px / 40px). - Its single leading icon is not the folder glyph the Workspace rows use: a closed tray means collapsed, an opened tray means expanded — one icon carrying both the group's identity and its state, with no separate rotating caret.
- Clicking the row collapses or expands the whole group; the choice is
remembered across page reloads (browser-local
localStorage). - Hovering a temp workspace row offers keep permanently, new session and delete; the group row's own + creates another temp workspace.
- Clicking a conversation opens it exactly like a native row.
Coexisting with dsh-better-workspace
dsh-better-workspace takes over the sidebar Workspace tree and derives virtual
folders from every / in a Workspace title (web/前端 nests under web). When
it is detected — dsh-better-workspace in the client boot manifest, with a
.bw-tree DOM probe as fallback — this plugin rides that mechanism and stops
injecting its own sidebar UI:
- Temp workspaces are titled
临时工作区/yyyyMMddHHmmss(e.g.临时工作区/20261021175513), so better-workspace nests all of them under one临时工作区virtual folder. The second-precision stamp already tells same-day runs apart; a-2,-3, … suffix remains only as a same-second guard. - The sidebar injection is off entirely: no plugin-rendered group, no native-row
hiding, no 4 s list poll. Grouping, renaming, deleting and reordering are
better-workspace's own UI; a delete still runs this plugin's cleanup hook
(sessions + directory + marker), and rename goes through the registry
renamecall better-workspace itself uses. - The blank New Session page's
新建临时工作区chip, the boot confirm dialog and the settings card are unchanged. - Requires better-workspace's 视图选项 → 工作区分组 to be 磁盘 + 名称 or
仅名称; its default 磁盘目录 does not derive
/groups and shows the raw临时工作区/20261021175513title flat.
Starting a new session
新建会话opens the blank New Session page — it does not auto-bind a Workspace. By default the sidebar's New Session button (and thesession.newshortcut) pick the current Session's Workspace, or the most recently active one; this plugin makes that no-argument call clear the main panel instead, so the Workspace is chosen deliberately. An explicit workspace id still behaves natively. The interception is installed as a property accessor, not a one-off method assignment: when another plugin wrapsuiWorkspace.startSessionlater (e.g.@linxin666/dsh-client-ui-git-graph's auto-isolation resolves a Workspace itself and forwards it as an explicit id), the setter captures that replacement and re-wraps the policy around it, so the bare call keeps its meaning wherever the patch sits in the chain. Draft options are forwarded untouched:startSession(void 0, { agentPreset })keeps its native behaviour.- That page's control row gains a
新建临时工作区chip beside the native选择工作区picker and the agent-preset chip (same metrics: 28px tall, 8px padding, 8px radius, 13px/500). It creates a temp workspace and opens a conversation in it — the same action as the group row's +. - The chip disappears once a Workspace is chosen for that New Session, since
the choice has then been made. The state comes from the native chip itself
(its
aria-labelstays the "choose workspace" call to action while its visible label becomes the chosen Workspace's title), so no locale string is compared.
Installation
From npm (prebuilt — recommended)
The package is published to npm with lib/ prebuilt, so installing it
needs no build and skips pnpm's allowBuilds approval — a single command:
dsh plugin --profile web add @yezack/dsh-temp-workspace
dsh plugin … add forwards the spec to pnpm inside the profile dir, then
reconciles dsh.profile.bundles for you. Because the manifest declares
dsh.bundle.patch, the package auto-joins the Loader layer stack, so on the
next dsh web start the host composes the plugin as a Loader entry, registers
the /temp-workspace/api route, and the settings card enters __DSH_BOOT__.
To republish after a change (requires an npm account that owns the
@dsh-dev scope):
npm login # one-time
npm publish # prepublishOnly rebuilds lib/client.js from src
From GitHub
Install it straight from GitHub with the dsh plugin forwarder (pnpm clone +
add inside the profile dir, then the bundle layer is reconciled for you):
# boot a profile, e.g. the web app
dsh plugin --profile web add github:yezack/dsh-temp-workspace
Because the manifest declares dsh.bundle.patch, the add step auto-appends
the package to dsh.profile.bundles, so on the next dsh web start the host
composes the plugin as a Loader entry, registers the /temp-workspace/api
route, and the settings card enters __DSH_BOOT__.
If you prefer to drive pnpm yourself, run the same spec from inside the profile directory:
cd "$DSH_HOME/profiles/web"
pnpm add github:yezack/dsh-temp-workspace
To pin a branch, tag or commit, append the usual pnpm git ref:
dsh plugin --profile web add github:yezack/dsh-temp-workspace#<ref>
The built client bundle (
lib/client.js) is committed in the repo, so a git install runs no build step. To hack on it, clone the repo and usenode build.mjs --watch(see Dev loop) — or install from a local checkout instead (see Wiring into the profile).
Settings → Plugins
The plugin registers a card in Settings → Plugins (keyed by the settings
namespace dsh-temp-workspace, persisted in ~/.dsh/settings.yaml):
| setting | values | default | meaning |
|---|---|---|---|
deleteMode | immediate / delayed | immediate | when a temp workspace is removed on the next boot |
deleteDelay | seconds | 3600 | grace period before removal when delayed |
confirmBeforeDelete | on / off | on | ask the user before removing held workspaces |
confirmBeforeDeleteon: the host never auto-deletes at boot. It holds the workspaces and the browser shows a confirm dialog with two choices:- Delete — purge the held workspaces (sessions + registration + dir).
- Keep temporarily — leave the workspaces alone this boot but keep them temporary: they stay in the marker and are subject to cleanup (and re-confirmation) again on a later boot.
- An unanswered dialog leaves the workspaces intact for the next boot.
Permanent keep is not in the boot dialog — it has its own button.
Each temp workspace row in the sidebar (titled "临时工作区", i.e. "Temporary Workspace") shows a small pin
button next to its "+" (New Session). Clicking it picks a destination folder (the
OS folder picker via ctx.workspaces.pickDirectory()). The temp workspace's files
are moved directly into that folder (no extra "临时工作区" subfolder is
created), the folder itself becomes the permanent workspace, and its folder name
is used as the workspace title. The workspace's conversations are migrated
too: each session log's header cwd is rewritten to the folder path and its log
directory is moved into the new cwd's projectKey slot. After it succeeds, a
non-dismissible dialog appears with a "Restart now" button that
triggers a real host-level restart (a detached helper respawns the exact DSH
invocation — the same mechanism dsh-market uses for plugin updates) so the
registry re-indexes and the migrated conversations appear under the new workspace.
confirmBeforeDeleteoff: deletion is automatic — immediately on boot, or afterdeleteDelayseconds whendeleteModeisdelayed.
How cleanup works
There is no public "delete session log" API in DSH — the persistence seam is append-only. So a temporary workspace is removed in four steps:
- Live sessions first — sessions the running host still holds in memory
(a confirm dialog, the delayed auto-delete, or a page reload can all trigger
the deletion in a host that never truly restarted) are torn down best-effort:
their agent is cancelled and awaited idle, the session's pending writes are
flushed to disk, and the live store entry is detached so
session.listdrops it and the client closes the conversation immediately. Flushing before detaching keeps multi-session workspaces clean: detaching emitssession/disposed, whose retirement flush would otherwise re-materialize a deleted log for sessions that still had pending writes, leaving unreadable Ungrouped leftovers. 0b. Archive cold sessions — sessions that were never attached in this host exist only as disk logs; deleting those logs sends nosession-removedevent, so an already-open browser would keep them as unreadable Ungrouped leftovers.archiveColdSessionsarchives each of them (the supported "hidden from every grouping surface" mechanism) BEFORE the logs are deleted: the archive-set change pusheshost/archived-sessions-changedand the sidebar drops them at once. The durable archived-id entries are reclaimed by the boot-time orphan sweep. - Sessions on disk — every session owned by the workspace (its header
cwdequals the workspace path) is located throughctx.sessionPersistence.locateand its on-disk directory is removed withfs.rm. - Registration —
ctx.workspaceRegistry.delete(id)removes the workspace record (which never touches logs or the directory by itself). - Directory — the throwaway directory (under
<dsh-home>/temp-workspaces/<uuid>) is removed.
The plugin tracks temporary workspaces in <dsh-home>/temp-workspaces/state.json
(a durable marker of { workspaceId, path, createdAt }). Deletion is driven by
that marker path, so a temp workspace whose registry record was already removed
via the UI before a restart still reaps its leftover directory (the fix for the
"leftover folder" bug) — the marker path, not the registry record, is the source of
truth for the directory.
No trace: deletion also prunes each detached live session's
projection-cache row (the session_projcache domain under
~/.dsh/storages/session_projcache.json) and the legacy per-session cache file
from an older storage layout. A boot-time orphan sweep
(pruneOrphanTempResidue) additionally reclaims every projection-cache row
whose cwd lives under the temp root and is no longer referenced by the
marker — together with its archived-id record (the ones archiveColdSessions
and the detach fallback leave behind) and legacy cache file. Those rows never
surface ghost conversations by themselves (identity-checked reads miss them),
but "no trace" means no trace; the user's intentional archives of real
conversations are never touched.
Layout
temp-workspace-plugin/
package.json name, dsh.bundle.patch + dsh.client, exports
cordis.patch.yml inserts this plugin as a host Loader entry
lib/index.js node half — /temp-workspace/api route + settings + boot cleanup
src/client.js browser half SOURCE (group + hero chip + settings card + dialog)
lib/client.js BUILT browser half (generated; do not edit)
build.mjs wrap src/client.js -> lib/client.js (HMR watch)
Dev loop
node build.mjs --watch
lib/client.js is the single file client-modules serves and dsh-client-hmr
polls; editing src/client.js hot-reloads the UI without a page refresh.
Host-half changes (lib/index.js) need a dsh web restart.
Wiring into the profile
# local checkout / clone (any absolute or relative path)
dsh plugin --profile web add /path/to/dsh-temp-workspace
This adds it to dsh.profile.bundles. On the next dsh web start the host
composes the row, the /temp-workspace/api route is registered, the settings
card enters __DSH_BOOT__, and the boot cleanup / confirm flow runs.
API (/temp-workspace/api/<method>)
Reads accept GET, mutations POST.
| method | returns | notes |
|---|---|---|
POST create | { workspace, created: true } | mkdir + workspaceRegistry.create (title = yyyy-MM-dd HH:mm) + marker |
GET list | { entries } | [{ workspaceId, path, title, sessionIds, createdAt }] currently marked temporary |
POST delete | { ok: true } | remove one temp workspace now (sessions + registration + dir) |
GET/POST config | { ok, value } | read the settings, or POST {…} to write a patch |
POST configReset | { ok, value } | restore every setting to its default |
GET pending | { ok, value } | held workspaces + confirmBeforeDelete + deleteAt |
POST confirm | { ok, deleted } | user confirmed — delete the held set |
POST keep | { ok, kept } | temporary keep — leave the held set alone (still temp) |
POST permanentKeep | { ok, moved } | { target, workspaceId? } — use target (picked folder) directly and de-temp that workspace (or all held when workspaceId omitted) |
POST restart | { restarting, pid, helperPid } | real host restart (same-origin loopback only) |
Notes / limits
- The sidebar workspace browser has no slot for a synthetic workspace group
and the blank New Session page's control row is a composition of three single
slots, so both the 临时工作区 group and the
新建临时工作区chip are injected into the live DOM with aMutationObserver(same idiom asdsh-workspace-auto-approval) — filling one of those slots would shadow the shipped UI. Temp workspaces' native rows are hidden withdisplay:none— never moved, and the plugin's own subtrees are only ever appended as a trailing child, which React never references — so the native trees keep reconciling untouched. If the sidebar or the hero layout changes upstream these placements may need a matching update. - The native Workspace search can still surface a temp conversation; only the list rows are regrouped.
- Cleaning removes session log directories directly. Live sessions owned by a temp workspace are torn down best-effort on delete (agent cancel + archive + live-store detach), so the conversations disappear even when the deletion runs in a host that still holds them in memory; at boot there are no live sessions, which is exactly when cleanup runs.
- A temp workspace's directory lives under the DSH home (
~/.dsh/temp-workspaces), not in your normal project folders. - There is no generic "ask the human" API for a browser plugin, so the confirm
dialog is rendered by the plugin itself as a lightweight inline-styled overlay
through its own
react-dom/clientroot (no dependency on the primitives' CSS-module scope, so it always shows). - Permanent Keep requires the
nativedirectory-picker capability (ctx.uiWorkspace.pickDirectory()); on a remote/non-native deployment that picker is unavailable and the button is disabled. - Conversation migration rewrites only the header frame of each session log
(a
.jsonl.zstdfile is a concatenation of independently-compressed Zstandard frames — frame 0 is the header line carryingcwd, the rest are the event batches). The header frame'scwdis rewritten and every event frame is preserved byte-for-byte, so no conversation content is lost. - A
cwdis immutable in the DSH session header, hence the rewrite approach. The DSH workspace registry's session→workspace grouping (bootstrap) only runs once on the first boot; later boots only rebuild the id→cwd index and never add a migrated session to the new workspace'ssessionIds. To avoid the migrated conversations ending up ungrouped, the plugin records a durable pending-attach list during the move and, on the next boot, re-attaches the migrated sessions to the new workspace — which is why the client prompts a restart after a permanent keep.
Known bugs that we will not fix
- Due to Harness limits, after a permanent keep there is a chance the conversation
ends up
Ungrouped. We have done what we can to handle it, but it can still occur. - Due to Harness limits, right after a migration the conversation name may take the workspace's name; clicking it changes it back. It does not affect use; not planning to fix — rename it yourself might help.
Related plugins
archify (deepseek-harness)
tt-a1i/archify
WeKnora (dsh-weknora)
tencent/weknora
weknora
tencent/weknora
BrowserSkill (dsh-plugin-browserskill)
tencent/browserskill