dsh-delete-session 0.1.6 → 0.1.7
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 +21 -0
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/client.js +43 -0
- package/src/index.js +45 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DDDMUC
|
|
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
|
@@ -28,7 +28,8 @@ DSH 侧边栏的会话菜单只有**重命名 / 分叉 / 归档**——归档只
|
|
|
28
28
|
- **风险确认** —— 首次删除弹出确认对话框:显示会话标题与 ID,勾选「我已了解,永久删除」后确认按钮才可用
|
|
29
29
|
- **确认一次后可跳过** —— 对话框里勾选「不再询问,以后直接删除」,之后点菜单项直接删除(左下角 toast 提示);**按住 Shift 点击**可临时恢复弹窗;清除 `localStorage['dsh-delete-session:skip-confirm']` 可永久恢复
|
|
30
30
|
- **多选批量删除** —— 会话菜单里的「多选」进入选择模式:每行左侧出现勾选圈,点整行即勾选(不会打开会话),底部操作条显示「已选 N 个 / 取消 / 删除」;点删除**立即批量执行**(无确认弹窗、无全选、所有行都可选):被选中的行**乐观隐藏**、操作条实时显示「正在删除 x/N…」、客户端以 6 路并发请求宿主(不同会话并行、工作区记账串行),完成后 toast 汇总并刷新列表;失败的行立即恢复显示并保持选中,方便直接重试;Esc 或「取消」退出
|
|
31
|
-
- **真正的删除** —— 停止运行中的 agent(cancel + 等待空闲)→ flush / detach 活跃会话 → 删除磁盘会话目录(原始 ID 与 `session-` 前缀两种写法)→ 持久化服务复核 →
|
|
31
|
+
- **真正的删除** —— 停止运行中的 agent(cancel + 等待空闲)→ flush / detach 活跃会话 → 删除磁盘会话目录(原始 ID 与 `session-` 前缀两种写法)→ 持久化服务复核 → 清理工作区记账,侧栏不会残留「未分组」孤儿,并连带清理项目缓存行(`$DSH_HOME/storages/*/sessions/<id>.json`),不留无法再显示的幽灵记录
|
|
32
|
+
- **删除后回到新会话** —— 若删除的正是当前打开的会话,自动切到一个全新会话(不再落到已不可用的邻居页——那种页面 composer 会被禁用、侧栏也列不出来,看起来像「没删掉」)
|
|
32
33
|
- **默认安全** —— 宿主路由仅限本机回环(回环 socket + 回环 Host 头 + 同源校验);被 DSH 打开占用的会话会被明确拒绝(409),绝不与活跃写者竞争
|
|
33
34
|
- **中英双语** —— 菜单项与对话框跟随 DSH 界面语言(zh / en)
|
|
34
35
|
- **零依赖** —— 不引入任何 DSH SDK,所有服务(`sessions` / `sessionPersistence` / `agents` / `workspaceRegistry` / `webServer`)都在调用时通过 cordis 上下文获取
|
|
@@ -80,7 +81,7 @@ dsh plugin --profile web add github:DDDMUC/dsh-delete-session
|
|
|
80
81
|
|
|
81
82
|
### 兼容性
|
|
82
83
|
|
|
83
|
-
- DSH `0.1.5-rc.1`
|
|
84
|
+
- DSH `0.1.5-rc.1` / `0.1.7-alpha.2` 实测通过(0.1.7 起不再导出 `SettingsProvider`;本插件不依赖 DSH SDK,无需改动)
|
|
84
85
|
- 宿主链路对缺少服务的旧版本会退化为纯文件系统清理
|
|
85
86
|
|
|
86
87
|
如果这个插件帮到了你,欢迎给仓库点个 ⭐([GitHub](https://github.com/DDDMUC/dsh-delete-session))——星标是开发者继续维护的最大动力,感谢支持!
|
|
@@ -113,7 +114,8 @@ This plugin puts Delete back where you already click: the session row's `...` me
|
|
|
113
114
|
- **Risk consent** — the first delete opens a confirmation dialog with the session title and id; the confirm button unlocks only after ticking "I understand"
|
|
114
115
|
- **Confirm once, then skip** — tick "Don't ask again" and later deletes run directly from the menu (a toast reports the result); **hold Shift while clicking** to force the dialog back, or clear `localStorage['dsh-delete-session:skip-confirm']` to restore it permanently
|
|
115
116
|
- **Multi-select batch delete** — "Select multiple" in the session menu enters selection mode: every row gets a round checkbox, clicking a row toggles it (it never opens the conversation), and a bottom bar shows "N selected / Cancel / Delete". Delete runs **immediately** (no confirm dialog, no select-all, every row selectable): selected rows are hidden optimistically, the bar shows live progress ("Deleting x/N..."), and the client runs 6 concurrent requests (independent sessions in parallel, workspace accounting serialized), then reports a toast summary; failed rows reappear immediately and stay selected for a quick retry; Esc or Cancel exits
|
|
116
|
-
- **Real deletion** — stops a running agent (cancel + quiescence), flushes and detaches the live session, removes the on-disk session directories (both the raw and `session-` prefixed id), verifies through the persistence service, then cleans workspace accounting so no orphan shows up under "Ungrouped"
|
|
117
|
+
- **Real deletion** — stops a running agent (cancel + quiescence), flushes and detaches the live session, removes the on-disk session directories (both the raw and `session-` prefixed id), verifies through the persistence service, then cleans workspace accounting so no orphan shows up under "Ungrouped", and removes the session's projection-cache rows (`$DSH_HOME/storages/*/sessions/<id>.json`) so no ghost the sidebar can no longer show is left behind
|
|
118
|
+
- **Leaves a fresh session behind** — when the deleted session is the one the main view is showing, the app switches to a brand-new session instead of a stale neighbour (an unavailable row with a disabled composer that the sidebar cannot even list, which reads as "the delete did not work")
|
|
117
119
|
- **Safe by default** — the host route is loopback-only (loopback socket, loopback Host header, same-origin check); a session still held open by DSH is refused with 409 instead of racing the active writer
|
|
118
120
|
- **Bilingual** — the menu item and dialog follow the DSH interface language (zh / en)
|
|
119
121
|
- **Zero dependencies** — no DSH SDK imports; every service (`sessions`, `sessionPersistence`, `agents`, `workspaceRegistry`, `webServer`) is resolved through the cordis context at call time
|
|
@@ -165,7 +167,7 @@ Restart `dsh web` to apply.
|
|
|
165
167
|
|
|
166
168
|
### Compatibility
|
|
167
169
|
|
|
168
|
-
- Tested on DSH `0.1.5-rc.1`
|
|
170
|
+
- Tested on DSH `0.1.5-rc.1` and `0.1.7-alpha.2`
|
|
169
171
|
- On older versions without the services, the host pipeline degrades to filesystem cleanup
|
|
170
172
|
|
|
171
173
|
If this plugin has been helpful, a ⭐ on [GitHub](https://github.com/DDDMUC/dsh-delete-session) would mean a lot — it is the biggest motivation for the developer to keep maintaining it. Thank you!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-delete-session",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Delete sessions from the DSH Web sidebar: adds a Delete action to the session row menu with a risk-consent dialog, then removes the session log, workspace accounting and live session state.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/client.js
CHANGED
|
@@ -104,6 +104,44 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
})
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// --- current-session handoff ------------------------------------------------
|
|
108
|
+
|
|
109
|
+
// Deleting the session the main view is showing leaves the app to restore a
|
|
110
|
+
// stale neighbour (on 0.1.7 it lands on an unavailable Agent Team row with a
|
|
111
|
+
// disabled composer), which reads as "the delete did not work". When the
|
|
112
|
+
// deleted session is the one the app has open, open a fresh session instead.
|
|
113
|
+
function deletedVariants(id) {
|
|
114
|
+
return id.startsWith('session-') ? [id, id.slice('session-'.length)] : [id, `session-${id}`]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function leaveIfCurrent(id) {
|
|
118
|
+
let current = null
|
|
119
|
+
try {
|
|
120
|
+
const raw = localStorage.getItem('dsh.sessions.current')
|
|
121
|
+
current = raw ? (JSON.parse(raw) || {}).sessionId || null : null
|
|
122
|
+
} catch {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (!current || !deletedVariants(id).includes(current)) return
|
|
126
|
+
const btn = document.querySelector(
|
|
127
|
+
'button[class*="newSession"], button[aria-label*="New session" i], button[aria-label*="新会话"]',
|
|
128
|
+
)
|
|
129
|
+
if (btn) {
|
|
130
|
+
try {
|
|
131
|
+
btn.click()
|
|
132
|
+
return
|
|
133
|
+
} catch {
|
|
134
|
+
// fall through to the state reset
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
localStorage.removeItem('dsh.sessions.current')
|
|
139
|
+
window.location.reload()
|
|
140
|
+
} catch {
|
|
141
|
+
// ignore
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
107
145
|
function deleteNow(info) {
|
|
108
146
|
pendingRemoval.add(info.id)
|
|
109
147
|
scheduleDecorate()
|
|
@@ -112,6 +150,7 @@ window.__ModuleLoader__.load({
|
|
|
112
150
|
toast(t('done'))
|
|
113
151
|
refreshList()
|
|
114
152
|
settlePendingSoon()
|
|
153
|
+
leaveIfCurrent(info.id)
|
|
115
154
|
})
|
|
116
155
|
.catch((reason) => {
|
|
117
156
|
pendingRemoval.delete(info.id)
|
|
@@ -329,6 +368,7 @@ window.__ModuleLoader__.load({
|
|
|
329
368
|
toast(t('done'))
|
|
330
369
|
refreshList()
|
|
331
370
|
settlePendingSoon()
|
|
371
|
+
leaveIfCurrent(info.id)
|
|
332
372
|
})
|
|
333
373
|
.catch((reason) => {
|
|
334
374
|
busy = false
|
|
@@ -559,6 +599,9 @@ window.__ModuleLoader__.load({
|
|
|
559
599
|
updateBar()
|
|
560
600
|
}
|
|
561
601
|
refreshList()
|
|
602
|
+
for (const id of ids) {
|
|
603
|
+
if (!failedIds.includes(id)) leaveIfCurrent(id)
|
|
604
|
+
}
|
|
562
605
|
})
|
|
563
606
|
}
|
|
564
607
|
|
package/src/index.js
CHANGED
|
@@ -92,6 +92,44 @@ function removeSessionDirs(sessionId) {
|
|
|
92
92
|
return dirs
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Deleting the on-disk directory is not enough: projection caches keep a
|
|
96
|
+
// per-session JSON row (`$DSH_HOME/storages/<store>/sessions/<id>.json`) that
|
|
97
|
+
// would linger as a ghost — the sidebar list is directory-backed, so the row
|
|
98
|
+
// can never be shown or deleted again (issue: "can't delete anymore"). Remove
|
|
99
|
+
// the session's cache units together with its directories.
|
|
100
|
+
function removeSessionCaches(sessionId) {
|
|
101
|
+
const storesRoot = path.join(dshHome(), 'storages')
|
|
102
|
+
const removed = []
|
|
103
|
+
let stores
|
|
104
|
+
try {
|
|
105
|
+
stores = fs.readdirSync(storesRoot, { withFileTypes: true })
|
|
106
|
+
} catch {
|
|
107
|
+
return removed
|
|
108
|
+
}
|
|
109
|
+
const names = idVariants(sessionId).map((variant) => `${variant}.json`)
|
|
110
|
+
for (const store of stores) {
|
|
111
|
+
if (!store.isDirectory()) continue
|
|
112
|
+
const dir = path.join(storesRoot, store.name, 'sessions')
|
|
113
|
+
let entries
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(dir)
|
|
116
|
+
} catch {
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!names.includes(entry)) continue
|
|
121
|
+
const file = path.join(dir, entry)
|
|
122
|
+
try {
|
|
123
|
+
fs.rmSync(file, { force: true })
|
|
124
|
+
removed.push(file)
|
|
125
|
+
} catch {
|
|
126
|
+
// best-effort: a locked cache row must not fail the deletion
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return removed
|
|
131
|
+
}
|
|
132
|
+
|
|
95
133
|
// --- live state --------------------------------------------------------------
|
|
96
134
|
|
|
97
135
|
// Stop a running agent before its session disappears: cancel the active turn
|
|
@@ -303,11 +341,16 @@ async function deleteSession(ctx, sessionId) {
|
|
|
303
341
|
throw new HttpError(500, 'verify-failed', 'session directories could not be fully removed')
|
|
304
342
|
}
|
|
305
343
|
|
|
344
|
+
// Cache rows are projection state, not the source of truth: clean them even
|
|
345
|
+
// when the directories were already gone, so a deleted session cannot leave
|
|
346
|
+
// a ghost behind.
|
|
347
|
+
const cachesRemoved = removeSessionCaches(sessionId)
|
|
348
|
+
|
|
306
349
|
const workspaceDetached = await withWorkspaceLock(() => detachFromWorkspace(ctx, sessionId))
|
|
307
|
-
if (removed.length === 0 && !workspaceDetached && !flushed && !detached) {
|
|
350
|
+
if (removed.length === 0 && !workspaceDetached && !flushed && !detached && cachesRemoved.length === 0) {
|
|
308
351
|
throw new HttpError(404, 'not-found', 'session not found')
|
|
309
352
|
}
|
|
310
|
-
return { removed: removed.length, stopped, flushed, detached, workspaceDetached }
|
|
353
|
+
return { removed: removed.length, caches: cachesRemoved.length, stopped, flushed, detached, workspaceDetached }
|
|
311
354
|
}
|
|
312
355
|
|
|
313
356
|
// --- http --------------------------------------------------------------------
|