@feecc/dsh-session-delete
v1.0.0
Published
Permanently delete conversations from the sidebar, in bulk, or together with a workspace.
Maintainers
Readme
Delete Conversations (@feecc/dsh-session-delete)
A DeepSeek Harness bundle that adds the one destructive capability the shipped packages deliberately omit: permanent deletion of conversations — one at a time, in bulk, or together with a workspace.
Harness itself only ever archives sessions and only ever removes a workspace
registration: session-persistence exposes no removal API, the Workspace
registry never touches logs, and @deepseek-ai/dsh-client-ui-workspace lists
"no Session deletion" as a known limitation. This bundle adds that capability
behind one Host route so every surface shares one implementation.
Surfaces
| # | Surface | Where |
|---|---|---|
| 1 | 删除会话 / Delete conversation | A destructive row in each conversation's … menu (sidebar.workspaces.session.menu.item, order 500, after Archive and Copy Session ID) and a trash button in the conversation header. Both open one confirmation dialog. |
| 2 | 批量选择对话 | A checklist toggle in the Workspace browser header, immediately right of the search button (a square-with-check glyph). It turns the conversation list into a selection surface; a floating bar offers 全选 / 退出 / 删除所选 — 全选 becomes 清空 once everything visible is selected — and deleting asks for confirmation once. A batch that contains a running conversation still deletes every idle one and reports the rest as 无法删除运行中的会话. |
| 3 | 同时删除该工作区下的全部对话 | A checkbox added to the shipped "删除工作区" dialog, aligned with the dialog's own description text. Confirming with it checked replaces the shipped delete with one Host operation that refuses the whole request while any conversation in the workspace is still running (workspace-active), so a running conversation can never be orphaned under Ungrouped. On success the workspace and its conversations are gone; on refusal the dialog stays open with the reason. |
Notices appear at the top of the window, not at the bottom.
All destructive paths ask for confirmation first, and the confirmation names what will disappear and states that it cannot be undone.
What "permanently delete" does (Host)
index.js serves one authenticated POST /api/session-delete route with four
actions: delete-sessions, delete-workspace, workspace-sessions, and
diagnose. Deleting one conversation:
- refuses while that conversation still has work in progress (
session-active) — the activity check is the sameworkspace/session-activitywaterfall thatarchiveSessionuses — because a running turn keeps writing to the log; - removes the session-owned directory under the persistence root
(
<root>/--<cwd>--/<session-id>/or<root>/_no-cwd/<session-id>/), including every historical format generation; - removes the write-behind projection-cache record;
- drops the Workspace accounting slot, the archive mark, and the pin;
- announces
api-session/removed, so every connected Client drops the row.
A conversation that is still loaded in this process cannot be disposed from outside its own Agent (Harness exposes no disposal API), and the Host list merges loaded Sessions back in from memory. Such a conversation is therefore made unreachable instead: after its log is gone, its row is withdrawn and an archive mark keeps the default sidebar filter from listing it again. The plugin also remembers the id in-process, withdraws any later re-announcement of it, and at activation sweeps every loaded conversation whose log is already gone (plus any archive mark that refers to nothing), so leftover rows clean themselves up. Once the process restarts, the loaded Session is gone and nothing remains.
Direct subagent descendants of a deleted conversation are deleted with it
(sessionQuery.traceSession), so a conversation tree leaves nothing behind.
Requests are idempotent: deleting an id whose log is already gone still withdraws
the row, purges its stored references, and reports success.
Running work and counting
One request classifies the whole batch before deleting anything, so a batch's
outcome does not depend on where the sweep happens to reach a conversation whose
turn ends mid-request: idle conversations are deleted, running ones come back as
session-active refusals, and the Client reports them as 无法删除运行中的会话 while
still confirming the deletions that succeeded. Deleting a workspace with its
conversations is all-or-nothing instead: while any conversation in it runs, the
whole request is refused and nothing is removed.
The count shown in the workspace dialog is the number of real conversations, not
the raw accounting: a Workspace's sessionIds also collects blank placeholders
(an unused "New Session" whose log carries only setup events) and ids whose log is
gone. Classification uses the cached sessionListMetadata.blank projection — the
same signal the sidebar uses to hide such rows — with the log size as a fallback.
Extension points used
Slots: sidebar.workspaces.session.menu.item,
conversation.session.header.actions, shell.overlay (confirmation, selection
bar, toast), and sidebar.footer.action (fallback for the multi-select toggle).
The two placements the shipped packages expose no slot for are small React roots appended to host markup this bundle owns:
- the multi-select toggle, inserted right after the Workspace browser's search slot (before the view-options / add-workspace actions), hidden while the search field is expanded;
- the extra option, appended below the description inside the
aria-label-identified delete-workspace dialog.
Both are removed on dispose, are driven by one requestAnimationFrame-throttled
MutationObserver, never read another plugin's source, and style through
--dsw-alias-* theme tokens only.
Installing
The bundle is an ordinary profile bundle. It has no build step and no runtime
dependencies (the Host half imports Node built-ins only; the browser half uses
the module table's react and @deepseek-ai/dsh-client-ui-primitives seeds), so
the source directory is the complete package.
From a registry, a git URL, or a packed tarball, install it as a profile bundle:
plugin_manager install_bundle target: "@feecc/dsh-session-delete" # or github:owner/repo, or D:\x.tgz
dsh plugin --profile <name> add @feecc/dsh-session-delete # CLI profileFrom a directory on disk, either point install_bundle at its absolute path, or
install it by hand:
place the directory in
<profile>/node_modules/@feecc/dsh-session-delete(alink:/junction to the checkout also works, and makes edits live);declare it in the profile
package.json:{ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "@feecc/dsh-session-delete"] } }, "dependencies": { "@feecc/dsh-session-delete": "link:D:\\plugins\\dsh-session-delete" } }
The profile's own patch layer stays untouched; cordis.patch.yml here inserts the
single Host row. A configuration refresh (any edit to the profile's
cordis.patch.yml, or a plugin-manager operation) mounts it live; no application
restart is needed.
peerDependencies pin the Host packages this bundle is built against, so a DSH
version mismatch is refused by the plugin manager's compatibility check instead
of loading a plugin whose assumptions no longer hold.
Uninstalling
Remove @feecc/dsh-session-delete from dsh.profile.bundles (and from
dependencies), delete the package directory, and refresh the profile
configuration.
Sharing and publishing
Three routes, in increasing formality:
- Send the directory or a tarball.
npm packproducesfeecc-dsh-session-delete-1.0.0.tgz(about 24 kB) that contains only the files listed infiles;install_bundleaccepts the.tgzpath directly. A zip of the directory works the same way for a manual install. - Push a git repository and have the colleague install
github:owner/repo; the repository root is the package root. - Publish to npm.
npm publishusespublishConfig.access: public, so the scoped package is public. Keep the name identical inpackage.json,cordis.patch.yml(name:) andclient.js(window.__ModuleLoader__.load({ id: ... })): the Client module system keys the browser bundle by package name, and a mismatch reportsbundle loaded without registering "<id>".
After installing, check the three surfaces: 删除会话 in a conversation's … menu,
the square-with-check toggle right of the sidebar search, and
同时删除该工作区下的全部对话 in the delete-workspace dialog.
Verification
node test/host.test.mjsdrives the real route handler against a fake Context and a disposable session root: running-work refusal, batch classification, subagent tree deletion,_no-cwdlookup, projection-cache removal, Workspace/archive/pin purge,api-session/removed, re-announcement withdrawal, workspace all-or-nothing deletion, blank-session classification, malformed input, and idempotence. All assertions pass.node test/client.test.mjsevaluates the realclient.jsin avmsandbox with a fake DOM and stubbed React: bundle registration,injectlist, all six slot registrations, the menu row, the header button, the selection bar and its 全选/清空 toggle, the confirmation dialog and its Host call, deterministic partial/refused outcomes, the main-view recovery, the toast, header-seat placement between the search slot and the actions, the search-expansion hiding, the workspace-dialog option (inset and conversation count), the row-click selection interception, workspace-wide deletion, and disposal. All assertions pass.- Against the live profile: the composed configuration resolves the bundle
(
dsh --profile <copy> --dump-config), the Host row mounts on a configuration refresh, and in-process calls confirmed the real persistence root, the Workspace registry, the archive-set purge, and the conversation count (Downloads 2, TypeScript 1 for the observed data). - Verified interactively in the running Web UI: all three surfaces render, the destructive flows delete what they promise, a running conversation is refused, and a workspace that contains one is left untouched.
Renderer placement is verified by use of the live page; the seat containers are
still plain DOM insertion into host markup this plugin does not own, so a future
harness layout change can require updating findWorkspaceHeader (search field)
or matchWorkspaceDeleteDialog (dialog label).
