dsh-delete-session 0.1.7 → 0.1.9
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/README.md +16 -0
- package/package.json +10 -2
- package/src/client.js +95 -16
package/README.md
CHANGED
|
@@ -72,6 +72,14 @@ dsh plugin --profile web add github:DDDMUC/dsh-delete-session
|
|
|
72
72
|
- `src/client.js`(浏览器):经典客户端 bundle——监听 portal 到 body 的会话菜单(`body > [role="menu"]`),沿 React fiber 链读取会话节点(`props.node.id`),克隆原生菜单行保持样式,弹出确认对话框
|
|
73
73
|
- 删除链路:停 agent → flush / detach 活跃会话 → 写所有权探测(被占用 → 409)→ 删除目录(两种 ID 写法 + 复核)→ 清理工作区记账
|
|
74
74
|
|
|
75
|
+
### 测试
|
|
76
|
+
|
|
77
|
+
- `test/checkbox.dom.test.js` —— 勾选框回归测试(`node --test`,零依赖):把真实的 `src/client.js` 跑在一个可读的 DOM 实现上,只经由用户真正会碰的入口(注入的会话菜单项、插件自己生成的 DOM)驱动,断言 **「存在 `.dsdel-check` ⟺ 多选模式开启」**
|
|
78
|
+
- 覆盖的回归:单条删除(`deleteNow` / 对话框确认)会调用 `scheduleDecorate()`,而 v0.1.8 之前 `decorateRows()` **无条件**给每一行插入勾选框,于是删掉一个会话后所有行都长出了圆勾选框、底部操作条却仍然隐藏——一个「半开」的多选状态
|
|
79
|
+
- 这个测试是有意义的,不是空跑:把 `decorateRows()` 改回旧写法时其中 5 项会失败;改成「永不插入勾选框」时另有 4 项会失败
|
|
80
|
+
- 运行:`npm test`
|
|
81
|
+
- **尚未验证的一环(诚实说明)**:与真实 DSH 界面的耦合——真实 DOM 结构、真实 CSS 布局、以及 React 调和器在重渲染会话行时是否会丢弃注入的勾选框——只能由真实浏览器验证。上述测试覆盖的是插件的点击与装饰状态机本身
|
|
82
|
+
|
|
75
83
|
### 已知限制
|
|
76
84
|
|
|
77
85
|
- 被 DSH 当前打开占用的会话:删除会自动等待写所有权释放(通常几秒)后完成;极少数仍被占用的情况会提示稍后重试或重启 DSH
|
|
@@ -158,6 +166,14 @@ Restart `dsh web` to apply.
|
|
|
158
166
|
- `src/client.js` (browser): a classic client bundle — watches for the portalled session menu (`body > [role="menu"]`), walks the React fiber chain to the session node (`props.node.id`), clones a native menu row for styling, and opens the confirmation dialog
|
|
159
167
|
- Pipeline: stop agent → flush / detach live session → write-lease probe (busy → 409) → remove directories (both id spellings + verification) → clean workspace accounting
|
|
160
168
|
|
|
169
|
+
### Tests
|
|
170
|
+
|
|
171
|
+
- `test/checkbox.dom.test.js` — the multi-select checkbox regression test (`node --test`, zero dependencies): it runs the real `src/client.js` against a readable DOM implementation and drives it only through surfaces a user touches (the injected session-menu items and the DOM the plugin builds), asserting that **a `.dsdel-check` exists if and only if multi-select is on**
|
|
172
|
+
- The regression it covers: a plain single delete (`deleteNow` / the dialog's confirm handler) calls `scheduleDecorate()`, and before v0.1.8 `decorateRows()` planted a checkbox on **every** row unconditionally — so after deleting one session every row grew a round checkbox while the batch bar stayed hidden, a "half-on" multi-select state
|
|
173
|
+
- The test earns its keep: reverting `decorateRows()` to the old shape fails 5 of its cases, and a fix that never plants checkboxes fails 4
|
|
174
|
+
- Run it with `npm test`
|
|
175
|
+
- **Not verified (honest note):** the coupling to the real DSH UI — real DOM structure, real CSS layout, and whether React's reconciler drops injected checkboxes when it re-renders a session row — can only be checked in a real browser. What these tests cover is the plugin's own click and decoration state machine
|
|
176
|
+
|
|
161
177
|
### Known limitations
|
|
162
178
|
|
|
163
179
|
- A session DSH currently holds open: the delete waits for the write lease to be released (usually a few seconds) and then completes; in the rare case it stays busy, the dialog asks you to retry later or restart DSH
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-delete-session",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
"delete"
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test \"test/*.test.js\""
|
|
31
|
+
},
|
|
26
32
|
"repository": {
|
|
27
33
|
"type": "git",
|
|
28
34
|
"url": "git+https://github.com/DDDMUC/dsh-delete-session.git"
|
|
@@ -33,7 +39,9 @@
|
|
|
33
39
|
},
|
|
34
40
|
"client": {
|
|
35
41
|
"inject": [
|
|
36
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
42
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
43
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-primitives"
|
|
37
45
|
],
|
|
38
46
|
"platform": "web"
|
|
39
47
|
}
|
package/src/client.js
CHANGED
|
@@ -12,12 +12,15 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The module is a classic client bundle (client-modules protocol): it
|
|
14
14
|
// registers a factory with window.__ModuleLoader__ and returns apply().
|
|
15
|
-
//
|
|
16
|
-
// UI revisions that keep the menu contract
|
|
15
|
+
// The menu injection and dialog are DOM-only (theme tokens, no SDK) so they
|
|
16
|
+
// survive core UI revisions that keep the menu contract; React is pulled in
|
|
17
|
+
// only for the plugin-page settings card (slot `plugins.row.config`).
|
|
17
18
|
window.__ModuleLoader__.load({
|
|
18
19
|
id: 'dsh-delete-session',
|
|
19
|
-
factory: () => {
|
|
20
|
+
factory: (require) => {
|
|
20
21
|
const NS = 'dsh-delete-session'
|
|
22
|
+
const react = require('react')
|
|
23
|
+
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
21
24
|
|
|
22
25
|
const zh = {
|
|
23
26
|
menu: '删除会话',
|
|
@@ -41,6 +44,11 @@ window.__ModuleLoader__.load({
|
|
|
41
44
|
batchDone: (n) => `已删除 ${n} 个会话`,
|
|
42
45
|
batchPartial: (ok, fail) => `已删除 ${ok} 个,${fail} 个失败`,
|
|
43
46
|
batchFailed: '批量删除失败:',
|
|
47
|
+
pageAsk: '删除时询问',
|
|
48
|
+
pageHintAsk: '每次删除会话前都会弹出确认框。',
|
|
49
|
+
pageHintAuto: '删除会话会直接执行,不再询问。',
|
|
50
|
+
pageSummaryAsk: '删除前询问',
|
|
51
|
+
pageSummaryAuto: '直接删除',
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
const en = {
|
|
@@ -65,22 +73,42 @@ window.__ModuleLoader__.load({
|
|
|
65
73
|
batchDone: (n) => `Deleted ${n} session(s)`,
|
|
66
74
|
batchPartial: (ok, fail) => `Deleted ${ok}, ${fail} failed`,
|
|
67
75
|
batchFailed: 'Batch delete failed: ',
|
|
76
|
+
pageAsk: 'Ask before deleting',
|
|
77
|
+
pageHintAsk: 'Show a confirmation dialog before every session delete.',
|
|
78
|
+
pageHintAuto: 'Delete the session right away without asking.',
|
|
79
|
+
pageSummaryAsk: 'Ask before deleting',
|
|
80
|
+
pageSummaryAuto: 'Delete without asking',
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
let localeSvc = null
|
|
71
84
|
let sessionsSvc = null
|
|
72
85
|
|
|
73
|
-
// Once the user
|
|
74
|
-
//
|
|
75
|
-
// the
|
|
86
|
+
// Once the user opts out ("删除时询问" unticked - either in the plugin page
|
|
87
|
+
// or via the dialog's "don't ask again") the confirmation dialog is skipped
|
|
88
|
+
// and the menu item deletes directly. Shift+click always opens the dialog
|
|
89
|
+
// again. The plugin page's checkbox and the dialog read the same key, and a
|
|
90
|
+
// subscription keeps the card in step when either one flips it.
|
|
76
91
|
const SKIP_KEY = 'dsh-delete-session:skip-confirm'
|
|
92
|
+
const skipListeners = new Set()
|
|
77
93
|
|
|
78
94
|
function skipConfirm() {
|
|
79
95
|
try { return localStorage.getItem(SKIP_KEY) === '1' } catch { return false }
|
|
80
96
|
}
|
|
81
97
|
|
|
82
|
-
function setSkipConfirm() {
|
|
83
|
-
|
|
98
|
+
function setSkipConfirm(value) {
|
|
99
|
+
const skip = value === true
|
|
100
|
+
try {
|
|
101
|
+
if (skip) localStorage.setItem(SKIP_KEY, '1')
|
|
102
|
+
else localStorage.removeItem(SKIP_KEY)
|
|
103
|
+
} catch { /* private mode: ignore */ }
|
|
104
|
+
for (const listener of skipListeners) {
|
|
105
|
+
try { listener() } catch { /* a subscriber must not break the toggle */ }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function subscribeSkip(listener) {
|
|
110
|
+
skipListeners.add(listener)
|
|
111
|
+
return () => { skipListeners.delete(listener) }
|
|
84
112
|
}
|
|
85
113
|
|
|
86
114
|
function postDelete(sessionId) {
|
|
@@ -230,6 +258,8 @@ window.__ModuleLoader__.load({
|
|
|
230
258
|
'.dsdel-bar-btn{appearance:none;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));background:var(--dsw-alias-fill-elevated,rgba(128,128,128,.08));color:var(--dsw-alias-label-primary,inherit);border-radius:8px;font-size:13px;cursor:pointer}',
|
|
231
259
|
'.dsdel-bar-btn:disabled{opacity:.5;cursor:default}',
|
|
232
260
|
'.dsdel-bar-danger{border-color:var(--dsw-alias-state-error-primary,#e5484d);background:var(--dsw-alias-state-error-primary,#e5484d);color:#fff}',
|
|
261
|
+
'.dsdel-page{display:flex;flex-direction:column;gap:8px}',
|
|
262
|
+
'.dsdel-page-hint{margin:0;color:var(--dsw-alias-label-tertiary,#888);font-size:13px;line-height:20px}',
|
|
233
263
|
].join('')
|
|
234
264
|
|
|
235
265
|
function ensureStyle() {
|
|
@@ -359,7 +389,7 @@ window.__ModuleLoader__.load({
|
|
|
359
389
|
cancel.disabled = true
|
|
360
390
|
confirm.textContent = t('deleting')
|
|
361
391
|
error.style.display = 'none'
|
|
362
|
-
if (skipBox.checked) setSkipConfirm()
|
|
392
|
+
if (skipBox.checked) setSkipConfirm(true)
|
|
363
393
|
pendingRemoval.add(info.id)
|
|
364
394
|
scheduleDecorate()
|
|
365
395
|
postDelete(info.id)
|
|
@@ -419,16 +449,26 @@ window.__ModuleLoader__.load({
|
|
|
419
449
|
rows.forEach((row) => {
|
|
420
450
|
const id = sessionIdFromRow(row)
|
|
421
451
|
if (!id) return
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
452
|
+
// Checkboxes belong to multi-select only: a single delete (or any other
|
|
453
|
+
// decorate pass outside the mode) must not plant them on every row.
|
|
454
|
+
if (!selecting) {
|
|
455
|
+
const stray = row.querySelector(':scope > .dsdel-check')
|
|
456
|
+
if (stray) stray.remove()
|
|
457
|
+
} else {
|
|
458
|
+
let check = row.querySelector(':scope > .dsdel-check')
|
|
459
|
+
if (!check) {
|
|
460
|
+
check = document.createElement('span')
|
|
461
|
+
check.className = 'dsdel-check'
|
|
462
|
+
check.setAttribute('data-dsh-delete-session', 'check')
|
|
463
|
+
row.insertBefore(check, row.firstChild)
|
|
464
|
+
}
|
|
465
|
+
check.classList.toggle('dsdel-check-on', selected.has(id))
|
|
428
466
|
}
|
|
429
|
-
check.classList.toggle('dsdel-check-on', selected.has(id))
|
|
430
467
|
row.classList.toggle('dsdel-row-pending', pendingRemoval.has(id))
|
|
431
468
|
})
|
|
469
|
+
if (!selecting) {
|
|
470
|
+
document.querySelectorAll('.dsdel-check').forEach((el) => el.remove())
|
|
471
|
+
}
|
|
432
472
|
}
|
|
433
473
|
|
|
434
474
|
function scheduleDecorate() {
|
|
@@ -664,6 +704,40 @@ window.__ModuleLoader__.load({
|
|
|
664
704
|
window.dispatchEvent(new Event('resize'))
|
|
665
705
|
}
|
|
666
706
|
|
|
707
|
+
// --- plugin page (slot plugins.row.config) ---------------------------------
|
|
708
|
+
|
|
709
|
+
// The manager's detail page for this bundle gives the component row a
|
|
710
|
+
// configure arrow once the bundle registers its own row.config entry (key
|
|
711
|
+
// `<bundle package>#<row id>`). The card carries the one setting the dialog
|
|
712
|
+
// also exposes: whether a session delete asks first.
|
|
713
|
+
function AskBeforeDeleteOption({ view }) {
|
|
714
|
+
const [ask, setAsk] = react.useState(!skipConfirm())
|
|
715
|
+
react.useEffect(() => subscribeSkip(() => setAsk(!skipConfirm())), [])
|
|
716
|
+
if (view === 'summary') return t(ask ? 'pageSummaryAsk' : 'pageSummaryAuto')
|
|
717
|
+
return react.createElement(
|
|
718
|
+
'div',
|
|
719
|
+
{ className: 'dsdel-page' },
|
|
720
|
+
react.createElement(primitives.Checkbox, {
|
|
721
|
+
checked: ask,
|
|
722
|
+
label: t('pageAsk'),
|
|
723
|
+
onChange: (checked) => {
|
|
724
|
+
setAsk(checked === true)
|
|
725
|
+
setSkipConfirm(checked !== true)
|
|
726
|
+
},
|
|
727
|
+
}),
|
|
728
|
+
react.createElement('p', { className: 'dsdel-page-hint' }, t(ask ? 'pageHintAsk' : 'pageHintAuto')),
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function installSettings(slots) {
|
|
733
|
+
slots.inject('plugins.row.config', () =>
|
|
734
|
+
slots.register(
|
|
735
|
+
{ name: 'plugins.row.config', key: 'dsh-delete-session#dsh-delete-session' },
|
|
736
|
+
AskBeforeDeleteOption,
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
|
|
667
741
|
function install() {
|
|
668
742
|
if (typeof document === 'undefined' || !document.body) return null
|
|
669
743
|
ensureStyle()
|
|
@@ -694,6 +768,11 @@ window.__ModuleLoader__.load({
|
|
|
694
768
|
if (!localeSvc) {
|
|
695
769
|
try { ctx.inject(['locale'], (sub) => { localeSvc = sub.locale }) } catch { /* optional */ }
|
|
696
770
|
}
|
|
771
|
+
const slots = ctx.get('slots')
|
|
772
|
+
if (slots) installSettings(slots)
|
|
773
|
+
else {
|
|
774
|
+
try { ctx.inject(['slots'], (sub) => installSettings(sub.slots)) } catch { /* optional */ }
|
|
775
|
+
}
|
|
697
776
|
const dispose = install()
|
|
698
777
|
if (dispose) ctx.effect(() => dispose)
|
|
699
778
|
}
|