@l1vide/dsh-session-delete 0.0.0-stage → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 L1vide (Dubai1210)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
1
+ # DSH Session Delete
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/Dubai1210/dsh-session-delete)
4
+
5
+ 在会话「⋯」菜单的「归档会话」下面多一项 **删除会话**:点一下、确认一次,这个会话的对话记录消失、它在工作区留下的过程残留被清掉,**它做出来的成果保留**。
6
+
7
+ DSH 自带的「归档」只是把会话从列表里挪走,文件还在、可以取消归档恢复。这个插件补上真正不可逆的那一步。
8
+
9
+ ## 功能
10
+
11
+ - 菜单项插在 `sidebar.workspaces.session.menu.item` 插槽 **order 410**,即「归档会话」之后、「会话 ID」之前
12
+ - 垃圾桶图标,点击先关闭菜单,再弹出二次确认框
13
+ - 确认框打开时先做一次只读扫描,列出「会删掉哪些过程文件」「会保留多少成果文件」,可取消勾选只删对话
14
+ - 确认后 Host 侧一次性完成清理,所有已打开的窗口立刻把该行移除
15
+
16
+ ## 删除范围
17
+
18
+ | 对象 | 处理 |
19
+ | --- | --- |
20
+ | 对话记录 | `~/.dsh/sessions/<project>/<session-id>/` 整目录删除(`session.v4.jsonl.zstd` 等世代日志) |
21
+ | 投影缓存 | `storages/session_projcache` 里该会话的行(含投影后的消息正文) |
22
+ | 工作区里的过程残留 | 该会话新建的脚本、下载的工具、临时文件和脚手架目录,逐个 `rm`,随后清掉因此变空的目录(不越过会话 `cwd`) |
23
+ | 正在运行的 agent | `cancel({kind:"user"})` + `agent.scope.dispose()`,释放日志文件句柄 |
24
+ | 工作区归属 | 从每个工作区记录的 `sessionIds` 中 `detachSession` |
25
+ | 归档 / 置顶集合 | 全局 `archivedSessionIds`、`pinnedSessionIds` 中移除 |
26
+ | 客户端列表 | `ctx.emit("api-session/removed", id)` → 所有窗口实时移除该行 |
27
+
28
+ 成果文件、你原来就有并被它改过的文件,都不在删除范围内。详见下一节。
29
+
30
+ 删除**不进回收站**,对话记录不可恢复。
31
+
32
+ ## 哪些文件算残留,哪些算成果
33
+
34
+ 判定在 `lib/classify.js`,先保后删:
35
+
36
+ **一律保留**
37
+
38
+ | 情况 | 理由 |
39
+ | --- | --- |
40
+ | 会话**修改过**的文件(`kind === "modified"`) | 改的是你的东西,删了无法还原 |
41
+ | 通过 `present` 工具交付的文件 | 会话明确交给你的成果 |
42
+ | 最后一次回复正文里点名的文件 | 同上 |
43
+ | 位于 git 仓库内的文件 | 是项目代码,能靠 git 回滚 |
44
+ | 文档/数据/媒体扩展名:`.docx .xlsx .pptx .pdf .md .txt .csv .png .jpg .mp4 .json .yaml .html` 等 | 成果形态 |
45
+ | 判不出来的扩展名 | 宁可留着 |
46
+
47
+ **会被清理(仅当文件是该会话新建的)**
48
+
49
+ | 类型 | 例子 |
50
+ | --- | --- |
51
+ | 脚手架目录内的文件 | `node_modules/` `dist/` `build/` `tmp/` `temp/` `.cache/` `__pycache__/` `.venv/` `target/` `coverage/` |
52
+ | 脚本/源码 | `.js .mjs .cjs .ts .py .ps1 .sh .bat .rb .pl .php .lua .go .rs .java` … |
53
+ | 下载或构建产物 | `.exe .msi .zip .7z .tar .gz .apk .jar .whl .dll .so` … |
54
+ | 临时文件 | `.log .bak .tmp .out .err .orig .rej .swp` |
55
+ | 隐藏文件 | 以 `.` 开头的 |
56
+ | 因此变空的目录 | 顺手删掉,不越过会话 `cwd` |
57
+
58
+ 「新建」的判定:该会话对它的第一次操作是写、且此前没读过它 —— DSH 要求覆盖已有文件前先读,所以「写而无读」即表示文件原本不存在。
59
+
60
+ 做不到的部分(明确写在这里,避免误解):
61
+
62
+ - **通过 shell 命令写出的文件不在归属范围内**。`bash` / `ops_bash` 的命令行只有字符串,没有可解析的目标路径,插件不会去猜。所以 `pip install`、`curl -o`、`python make.py` 产生的东西会留在磁盘上
63
+ - 子会话(subagent)写出的文件不计入父会话;要清就删对应子会话
64
+ - 「修改」类文件只保留、不还原 —— 原内容已被覆盖,本插件不做备份;需要旧版本请靠自己的 git
65
+
66
+ ## 安装
67
+
68
+ 源码安装(GitHub):
69
+
70
+ ```bash
71
+ dsh plugin --profile web add github:Dubai1210/dsh-session-delete
72
+ ```
73
+
74
+ 本地目录安装(开发调试):
75
+
76
+ ```bash
77
+ dsh plugin --profile desktop add link:/path/to/dsh-session-delete
78
+ ```
79
+
80
+ 安装后**刷新一次 DSH 窗口**(Ctrl+R)让 client bundle 加载;如果菜单里没有出现新项,重启一次 DSH。
81
+
82
+ ## 使用
83
+
84
+ 1. 在左侧工作区列表里找到目标会话行,点行尾的「⋯」
85
+ 2. 选择「归档会话」下面的「**删除会话**」
86
+ 3. 弹窗会先扫一次工作区,列出要清的过程文件;不想清就取消勾选
87
+ 4. 点「删除」
88
+ 5. 该会话行消失,对话记录和过程残留同时被清掉,成果文件还在
89
+
90
+ ## 权限与风险
91
+
92
+ - Host 半只注册同源 HTTP 路由:`POST /dsh-session-delete/inspect`(只读预检)、`POST /dsh-session-delete/delete`。同源校验照抄 DSH 官方插件的做法:拒绝 `sec-fetch-site: cross-site`、`origin: null`、以及 `origin.host` 与 `host` 头不一致的请求
93
+ - 只操作本机 `~/.dsh` 下的会话数据、工作区注册表,以及该会话自己留下的过程文件,**不发起任何外部网络请求**,不依赖任何第三方服务
94
+ - 没有任何遥测、没有后台常驻任务
95
+ - 风险点:删除是永久性的。被判定为「残留」的文件如果没有版本控制,删掉就没了;判定规则和预览清单就是为此存在的
96
+
97
+ ## 兼容与运行要求
98
+
99
+ - DSH:0.2.x(desktop profile 与 web profile 均可)
100
+ - 平台:Windows / macOS / Linux
101
+ - 运行时:Node.js >= 18(Host 半只用 `node:fs/promises`、`node:path`、`node:zlib`)
102
+ - 依赖的服务:`webServer`、`sessions`、`agents`、`sessionPersistence`、`workspaceRegistry`、`storageDomain`,全部来自 DSH 基础 bundle,无额外依赖
103
+
104
+ ## 能力分类
105
+
106
+ 界面与体验(同时涉及会话与消息的数据清理)
107
+
108
+ ## 实现说明
109
+
110
+ bundle 插件,两半:
111
+
112
+ **Host** — `lib/index.js`,导出 `apply(ctx)`,注册两条 `webServer` exact 路由。删除流水线:
113
+
114
+ 1. `lib/transcript.js` 读转录日志(多帧 zstd)拿出该会话碰过的工作区文件
115
+ 2. `lib/classify.js` 把它分成「保留 / 残留」两堆(有 `present`、末尾回复、git 仓库、扩展名等信号)
116
+ 3. 只对「残留」逐个 `rm`,再清空目录
117
+ 4. 停活 agent 并释放文件句柄
118
+ 5. `sessionPersistence.locate({cwd, id})` 定位日志文件,`rm -r` 其所在会话目录
119
+ 6. `storageDomain.get("session_projcache").table("sessions").delete(id)` 清投影缓存行
120
+ 7. 遍历 `workspaceRegistry.list()`,逐个 `entity.detachSession(id)`
121
+ 8. `unarchiveSession(id)` + `unpinSession(id)` 清全局集合
122
+ 9. `ctx.emit("api-session/removed", id)` 通知所有客户端
123
+
124
+ 每一步独立 try/catch,单步失败不影响其余步骤;返回值 `{ok, live, removedFiles, cacheRows, headerFound, workspace:{removed,kept,failed,cwd}}` 便于排查。
125
+
126
+ 文件归属解析在 `lib/transcript.js`:会话日志是**多帧 zstd**(每个追加批次一帧),一次性 `zstdDecompressSync` 只能解出第一帧,所以这里先按 frame header / block header 走一遍结构定位所有完整帧再逐帧解压;然后扫描 `tool/call` 事件,按工具名取路径参数(`path` / `file_path` / `target_file`,`apply_patch` 走 `*** Update File:` 行),用会话 `cwd` 规整并拒绝越界路径。
127
+
128
+ **Client** — `client.js`,`window.__ModuleLoader__.load` 注册懒工厂,往两个插槽注册:
129
+
130
+ - `sidebar.workspaces.session.menu.item` → 删除菜单项(order 410)
131
+ - `shell.overlay` → 确认弹窗(`Modal` + 清单 + 清理开关 + 取消/删除按钮)
132
+
133
+ 打开弹窗先 `fetch("/dsh-session-delete/inspect")` 取清单,确认时 `fetch("/dsh-session-delete/delete", {body:{sessionId, clean}})`,同源、`credentials: same-origin`。
134
+
135
+ ## 目录结构
136
+
137
+ ```
138
+ dsh-session-delete/
139
+ ├── package.json # dsh.bundle.patch + dsh.client
140
+ ├── cordis.patch.yml # - insert: 宿主插件条目
141
+ ├── lib/index.js # Host 半:两条路由 + 删除流水线
142
+ ├── lib/transcript.js # Host 半:多帧 zstd 解码 + 会话写文件归属解析
143
+ ├── lib/classify.js # Host 半:残留 / 成果判定
144
+ ├── client.js # Client 半:菜单项 + 确认框
145
+ ├── locale/en.json # meta.title / meta.description
146
+ ├── locale/zh.json
147
+ └── icon.svg
148
+ ```
149
+
150
+ ## 开发
151
+
152
+ 改完文件后重新装一次即可热生效(Host 半需要重新加载):
153
+
154
+ ```bash
155
+ dsh plugin --profile desktop add link:/path/to/dsh-session-delete
156
+ ```
157
+
158
+ ## 许可证
159
+
160
+ MIT,见 [LICENSE](LICENSE)。
package/client.js ADDED
@@ -0,0 +1,277 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-session-delete",
3
+ factory: (require) => {
4
+ const module = { exports: {} };
5
+ const exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+
8
+ const react = require("react");
9
+ const jsxRuntime = require("react/jsx-runtime");
10
+ const primitives = require("@deepseek-ai/dsh-client-ui-primitives");
11
+ const store = require("@deepseek-ai/dsh-client-store");
12
+
13
+ const jsx = jsxRuntime.jsx;
14
+ const jsxs = jsxRuntime.jsxs;
15
+ const Fragment = jsxRuntime.Fragment;
16
+ const createSnapshotStore = store.createSnapshotStore ?? store.default?.createSnapshotStore;
17
+
18
+ const name = "dsh-session-delete";
19
+ const NS = "dsh-session-delete";
20
+ const inject = ["slots", "locale", "sessions"];
21
+
22
+ const DELETE_URL = "/dsh-session-delete/delete";
23
+ const INSPECT_URL = "/dsh-session-delete/inspect";
24
+
25
+ const zh = {
26
+ "menu.deleteSession": "删除会话",
27
+ "delete.confirm.title": "删除此会话?",
28
+ "delete.confirm.desc":
29
+ "对话记录会永久删除,它在工作区里留下的过程文件(脚本、下载的工具、临时文件)一并清理;它做出来的成果文件会保留。",
30
+ "delete.confirm.action": "删除",
31
+ "delete.confirm.pending": "正在删除…",
32
+ "delete.scanning": "正在检查工作区…",
33
+ "delete.scanFailed": "工作区检查失败,仍可删除会话。",
34
+ "delete.cleanup.label": "清理 {count} 个过程文件",
35
+ "delete.cleanup.none": "没有需要清理的过程文件",
36
+ "delete.keep.label": "保留 {count} 个成果文件(含被修改的文件)",
37
+ "cancel": "取消",
38
+ "close": "关闭"
39
+ };
40
+
41
+ const en = {
42
+ "menu.deleteSession": "Delete session",
43
+ "delete.confirm.title": "Delete this session?",
44
+ "delete.confirm.desc":
45
+ "The conversation is deleted for good, together with the scaffolding it left in the workspace (scripts, downloaded tools, temp files). Files it produced are kept.",
46
+ "delete.confirm.action": "Delete",
47
+ "delete.confirm.pending": "Deleting…",
48
+ "delete.scanning": "Checking the workspace…",
49
+ "delete.scanFailed": "Workspace check failed; the session can still be deleted.",
50
+ "delete.cleanup.label": "Clean up {count} scaffolding files",
51
+ "delete.cleanup.none": "Nothing to clean up",
52
+ "delete.keep.label": "Keep {count} result files (modified files included)",
53
+ "cancel": "Cancel",
54
+ "close": "Close"
55
+ };
56
+
57
+ const listStyle = {
58
+ margin: "8px 0 0",
59
+ padding: "6px 8px",
60
+ maxHeight: 144,
61
+ overflow: "auto",
62
+ listStyle: "none",
63
+ fontFamily: "var(--font-mono, ui-monospace, monospace)",
64
+ fontSize: 12,
65
+ lineHeight: 1.6,
66
+ background: "var(--surface-2, rgba(127,127,127,0.08))",
67
+ borderRadius: 6
68
+ };
69
+
70
+ function DeleteSessionMenuItem(props) {
71
+ const { sessionId, displayTitle, useMenuOpenState, requestDelete, t } = props;
72
+ const [, setMenuOpen] = useMenuOpenState();
73
+ return jsx(primitives.MenuItemButton, {
74
+ icon: jsx(primitives.IconTrashOutlineRegular, { size: 14 }),
75
+ onSelect: () => {
76
+ setMenuOpen(false);
77
+ requestDelete(sessionId, displayTitle);
78
+ },
79
+ children: t("menu.deleteSession")
80
+ });
81
+ }
82
+
83
+ function DeleteSessionConfirmDialog(props) {
84
+ const { useDeleteRequest, settleSessionDelete, confirmSessionDelete, t } = props;
85
+ const request = useDeleteRequest((pending) => pending);
86
+ if (request === null) return null;
87
+ return jsx(DeleteConfirmForm, {
88
+ request,
89
+ confirmSessionDelete,
90
+ onSettle: settleSessionDelete,
91
+ t
92
+ });
93
+ }
94
+
95
+ function DeleteConfirmForm({ request, confirmSessionDelete, onSettle, t }) {
96
+ const [plan, setPlan] = react.useState(null);
97
+ const [scanFailed, setScanFailed] = react.useState(false);
98
+ const [clean, setClean] = react.useState(true);
99
+ const [deleting, setDeleting] = react.useState(false);
100
+ const [error, setError] = react.useState(null);
101
+
102
+ react.useEffect(() => {
103
+ let alive = true;
104
+ postJson(INSPECT_URL, { sessionId: request.sessionId })
105
+ .then((payload) => {
106
+ if (alive) setPlan(payload);
107
+ })
108
+ .catch(() => {
109
+ if (alive) setScanFailed(true);
110
+ });
111
+ return () => {
112
+ alive = false;
113
+ };
114
+ }, [request.sessionId]);
115
+
116
+ const close = () => {
117
+ if (deleting) return;
118
+ onSettle();
119
+ };
120
+
121
+ const confirm = () => {
122
+ setDeleting(true);
123
+ setError(null);
124
+ confirmSessionDelete(request.sessionId, clean)
125
+ .then(() => {
126
+ setDeleting(false);
127
+ onSettle();
128
+ })
129
+ .catch((reason) => {
130
+ setDeleting(false);
131
+ setError(reason instanceof Error ? reason.message : String(reason));
132
+ });
133
+ };
134
+
135
+ const residue = plan?.residue ?? [];
136
+ const kept = plan?.keep?.length ?? 0;
137
+ const scanning = plan === null && !scanFailed;
138
+
139
+ return jsxs(primitives.Modal, {
140
+ open: true,
141
+ onClose: close,
142
+ closeLabel: t("close"),
143
+ title: t("delete.confirm.title"),
144
+ description: t("delete.confirm.desc"),
145
+ footer: jsxs(Fragment, {
146
+ children: [
147
+ jsx(primitives.Button, {
148
+ variant: "outline",
149
+ disabled: deleting,
150
+ onClick: close,
151
+ children: t("cancel")
152
+ }),
153
+ jsx(primitives.Button, {
154
+ variant: "primary",
155
+ disabled: deleting,
156
+ onClick: confirm,
157
+ children: deleting ? t("delete.confirm.pending") : t("delete.confirm.action")
158
+ })
159
+ ]
160
+ }),
161
+ children: [
162
+ scanning ? jsx("div", { role: "status", children: t("delete.scanning") }) : null,
163
+ scanFailed ? jsx("div", { children: t("delete.scanFailed") }) : null,
164
+ plan !== null && residue.length > 0
165
+ ? jsxs(Fragment, {
166
+ children: [
167
+ jsx("label", {
168
+ style: { display: "flex", gap: 8, alignItems: "center", marginTop: 4 },
169
+ children: [
170
+ jsx("input", {
171
+ type: "checkbox",
172
+ checked: clean,
173
+ disabled: deleting,
174
+ onChange: (event) => setClean(event.target.checked)
175
+ }),
176
+ jsx("span", { children: t("delete.cleanup.label", { count: residue.length }) })
177
+ ]
178
+ }),
179
+ jsx("ul", {
180
+ style: listStyle,
181
+ children: residue.map((entry) =>
182
+ jsx("li", { children: entry.path }, entry.path)
183
+ )
184
+ })
185
+ ]
186
+ })
187
+ : null,
188
+ plan !== null && residue.length === 0
189
+ ? jsx("div", { style: { marginTop: 4 }, children: t("delete.cleanup.none") })
190
+ : null,
191
+ plan !== null && kept > 0
192
+ ? jsx("div", {
193
+ style: { marginTop: 6, opacity: 0.75 },
194
+ children: t("delete.keep.label", { count: kept })
195
+ })
196
+ : null,
197
+ error !== null ? jsx("div", { role: "alert", children: error }) : null
198
+ ]
199
+ });
200
+ }
201
+
202
+ async function postJson(path, body) {
203
+ const response = await fetch(path, {
204
+ method: "POST",
205
+ headers: { "content-type": "application/json" },
206
+ credentials: "same-origin",
207
+ cache: "no-store",
208
+ body: JSON.stringify(body)
209
+ });
210
+ let payload = null;
211
+ try {
212
+ payload = await response.json();
213
+ } catch {}
214
+ if (!response.ok || payload?.ok === false) {
215
+ throw new Error(payload?.error || `HTTP ${response.status}`);
216
+ }
217
+ return payload;
218
+ }
219
+
220
+ function apply(ctx) {
221
+ const sessions = ctx.get("sessions", false);
222
+ const deleteRequest = createSnapshotStore(null);
223
+
224
+ const deleteMenuItemInjected = () => ({
225
+ requestDelete: (sessionId, displayTitle) => {
226
+ deleteRequest.set({ sessionId, displayTitle });
227
+ }
228
+ });
229
+
230
+ const deleteDialogInjected = () => ({
231
+ hooks: { deleteRequest },
232
+ settleSessionDelete: () => {
233
+ deleteRequest.set(null);
234
+ },
235
+ confirmSessionDelete: async (sessionId, clean) => {
236
+ const payload = await postJson(DELETE_URL, { sessionId, clean });
237
+ try {
238
+ sessions?.handleSessionRemoved?.(sessionId);
239
+ } catch {}
240
+ return payload;
241
+ }
242
+ });
243
+
244
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-session-delete: dictionaries");
245
+
246
+ ctx.slots.inject("sidebar.workspaces.session.menu.item", function* () {
247
+ yield ctx.slots.register(
248
+ {
249
+ name: "sidebar.workspaces.session.menu.item",
250
+ id: "session-delete",
251
+ order: 410,
252
+ locale: NS,
253
+ inject: deleteMenuItemInjected
254
+ },
255
+ DeleteSessionMenuItem
256
+ );
257
+ });
258
+
259
+ ctx.slots.inject("shell.overlay", function* () {
260
+ yield ctx.slots.register(
261
+ {
262
+ name: "shell.overlay",
263
+ id: "session-delete",
264
+ locale: NS,
265
+ inject: deleteDialogInjected
266
+ },
267
+ DeleteSessionConfirmDialog
268
+ );
269
+ });
270
+ }
271
+
272
+ exports.name = name;
273
+ exports.inject = inject;
274
+ exports.apply = apply;
275
+ return module.exports;
276
+ }
277
+ });
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: dsh-session-delete
3
+ name: '@l1vide/dsh-session-delete'
package/icon.svg ADDED
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M4 7h16" />
3
+ <path d="M10 4h4a1 1 0 0 1 1 1v2H9V5a1 1 0 0 1 1-1z" />
4
+ <path d="M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12" />
5
+ <path d="M10 11v6M14 11v6" />
6
+ </svg>