dsh-delete-session 0.1.8 → 0.1.10
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 +80 -25
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.10",
|
|
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: '删除会话',
|
|
@@ -26,7 +29,6 @@ window.__ModuleLoader__.load({
|
|
|
26
29
|
cancel: '取消',
|
|
27
30
|
confirm: '删除',
|
|
28
31
|
deleting: '删除中…',
|
|
29
|
-
ack: '我已了解,永久删除',
|
|
30
32
|
dontAsk: '不再询问,以后直接删除',
|
|
31
33
|
done: '已删除会话',
|
|
32
34
|
failed: '删除失败:',
|
|
@@ -41,6 +43,11 @@ window.__ModuleLoader__.load({
|
|
|
41
43
|
batchDone: (n) => `已删除 ${n} 个会话`,
|
|
42
44
|
batchPartial: (ok, fail) => `已删除 ${ok} 个,${fail} 个失败`,
|
|
43
45
|
batchFailed: '批量删除失败:',
|
|
46
|
+
pageAsk: '删除时询问',
|
|
47
|
+
pageHintAsk: '每次删除会话前都会弹出确认框。',
|
|
48
|
+
pageHintAuto: '删除会话会直接执行,不再询问。',
|
|
49
|
+
pageSummaryAsk: '删除前询问',
|
|
50
|
+
pageSummaryAuto: '直接删除',
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
const en = {
|
|
@@ -50,7 +57,6 @@ window.__ModuleLoader__.load({
|
|
|
50
57
|
cancel: 'Cancel',
|
|
51
58
|
confirm: 'Delete',
|
|
52
59
|
deleting: 'Deleting...',
|
|
53
|
-
ack: 'I understand - delete permanently',
|
|
54
60
|
dontAsk: "Don't ask again",
|
|
55
61
|
done: 'Session deleted',
|
|
56
62
|
failed: 'Delete failed: ',
|
|
@@ -65,22 +71,42 @@ window.__ModuleLoader__.load({
|
|
|
65
71
|
batchDone: (n) => `Deleted ${n} session(s)`,
|
|
66
72
|
batchPartial: (ok, fail) => `Deleted ${ok}, ${fail} failed`,
|
|
67
73
|
batchFailed: 'Batch delete failed: ',
|
|
74
|
+
pageAsk: 'Ask before deleting',
|
|
75
|
+
pageHintAsk: 'Show a confirmation dialog before every session delete.',
|
|
76
|
+
pageHintAuto: 'Delete the session right away without asking.',
|
|
77
|
+
pageSummaryAsk: 'Ask before deleting',
|
|
78
|
+
pageSummaryAuto: 'Delete without asking',
|
|
68
79
|
}
|
|
69
80
|
|
|
70
81
|
let localeSvc = null
|
|
71
82
|
let sessionsSvc = null
|
|
72
83
|
|
|
73
|
-
// Once the user
|
|
74
|
-
//
|
|
75
|
-
// the
|
|
84
|
+
// Once the user opts out ("删除时询问" unticked - either in the plugin page
|
|
85
|
+
// or via the dialog's "don't ask again") the confirmation dialog is skipped
|
|
86
|
+
// and the menu item deletes directly. Shift+click always opens the dialog
|
|
87
|
+
// again. The plugin page's checkbox and the dialog read the same key, and a
|
|
88
|
+
// subscription keeps the card in step when either one flips it.
|
|
76
89
|
const SKIP_KEY = 'dsh-delete-session:skip-confirm'
|
|
90
|
+
const skipListeners = new Set()
|
|
77
91
|
|
|
78
92
|
function skipConfirm() {
|
|
79
93
|
try { return localStorage.getItem(SKIP_KEY) === '1' } catch { return false }
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
function setSkipConfirm() {
|
|
83
|
-
|
|
96
|
+
function setSkipConfirm(value) {
|
|
97
|
+
const skip = value === true
|
|
98
|
+
try {
|
|
99
|
+
if (skip) localStorage.setItem(SKIP_KEY, '1')
|
|
100
|
+
else localStorage.removeItem(SKIP_KEY)
|
|
101
|
+
} catch { /* private mode: ignore */ }
|
|
102
|
+
for (const listener of skipListeners) {
|
|
103
|
+
try { listener() } catch { /* a subscriber must not break the toggle */ }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function subscribeSkip(listener) {
|
|
108
|
+
skipListeners.add(listener)
|
|
109
|
+
return () => { skipListeners.delete(listener) }
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
function postDelete(sessionId) {
|
|
@@ -230,6 +256,8 @@ window.__ModuleLoader__.load({
|
|
|
230
256
|
'.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
257
|
'.dsdel-bar-btn:disabled{opacity:.5;cursor:default}',
|
|
232
258
|
'.dsdel-bar-danger{border-color:var(--dsw-alias-state-error-primary,#e5484d);background:var(--dsw-alias-state-error-primary,#e5484d);color:#fff}',
|
|
259
|
+
'.dsdel-page{display:flex;flex-direction:column;gap:8px}',
|
|
260
|
+
'.dsdel-page-hint{margin:0;color:var(--dsw-alias-label-tertiary,#888);font-size:13px;line-height:20px}',
|
|
233
261
|
].join('')
|
|
234
262
|
|
|
235
263
|
function ensureStyle() {
|
|
@@ -296,15 +324,6 @@ window.__ModuleLoader__.load({
|
|
|
296
324
|
text.className = 'dsdel-text'
|
|
297
325
|
text.textContent = t('desc')
|
|
298
326
|
|
|
299
|
-
const ack = document.createElement('label')
|
|
300
|
-
ack.className = 'dsdel-ack'
|
|
301
|
-
const checkbox = document.createElement('input')
|
|
302
|
-
checkbox.type = 'checkbox'
|
|
303
|
-
const ackText = document.createElement('span')
|
|
304
|
-
ackText.textContent = t('ack')
|
|
305
|
-
ack.appendChild(checkbox)
|
|
306
|
-
ack.appendChild(ackText)
|
|
307
|
-
|
|
308
327
|
const skip = document.createElement('label')
|
|
309
328
|
skip.className = 'dsdel-ack'
|
|
310
329
|
const skipBox = document.createElement('input')
|
|
@@ -328,14 +347,12 @@ window.__ModuleLoader__.load({
|
|
|
328
347
|
confirm.type = 'button'
|
|
329
348
|
confirm.className = 'dsdel-btn dsdel-danger'
|
|
330
349
|
confirm.textContent = t('confirm')
|
|
331
|
-
confirm.disabled = true
|
|
332
350
|
actions.appendChild(cancel)
|
|
333
351
|
actions.appendChild(confirm)
|
|
334
352
|
|
|
335
353
|
dialog.appendChild(title)
|
|
336
354
|
dialog.appendChild(meta)
|
|
337
355
|
dialog.appendChild(text)
|
|
338
|
-
dialog.appendChild(ack)
|
|
339
356
|
dialog.appendChild(skip)
|
|
340
357
|
dialog.appendChild(error)
|
|
341
358
|
dialog.appendChild(actions)
|
|
@@ -347,19 +364,18 @@ window.__ModuleLoader__.load({
|
|
|
347
364
|
if (busy) return
|
|
348
365
|
backdrop.remove()
|
|
349
366
|
}
|
|
350
|
-
checkbox.addEventListener('change', () => { confirm.disabled = !checkbox.checked })
|
|
351
367
|
cancel.addEventListener('click', close)
|
|
352
368
|
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) close() })
|
|
353
369
|
backdrop.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
|
|
354
370
|
|
|
355
371
|
confirm.addEventListener('click', () => {
|
|
356
|
-
if (busy
|
|
372
|
+
if (busy) return
|
|
357
373
|
busy = true
|
|
358
374
|
confirm.disabled = true
|
|
359
375
|
cancel.disabled = true
|
|
360
376
|
confirm.textContent = t('deleting')
|
|
361
377
|
error.style.display = 'none'
|
|
362
|
-
if (skipBox.checked) setSkipConfirm()
|
|
378
|
+
if (skipBox.checked) setSkipConfirm(true)
|
|
363
379
|
pendingRemoval.add(info.id)
|
|
364
380
|
scheduleDecorate()
|
|
365
381
|
postDelete(info.id)
|
|
@@ -374,7 +390,7 @@ window.__ModuleLoader__.load({
|
|
|
374
390
|
busy = false
|
|
375
391
|
pendingRemoval.delete(info.id)
|
|
376
392
|
scheduleDecorate()
|
|
377
|
-
confirm.disabled =
|
|
393
|
+
confirm.disabled = false
|
|
378
394
|
cancel.disabled = false
|
|
379
395
|
confirm.textContent = t('confirm')
|
|
380
396
|
error.textContent = t('failed') + String((reason && reason.message) || reason)
|
|
@@ -674,6 +690,40 @@ window.__ModuleLoader__.load({
|
|
|
674
690
|
window.dispatchEvent(new Event('resize'))
|
|
675
691
|
}
|
|
676
692
|
|
|
693
|
+
// --- plugin page (slot plugins.row.config) ---------------------------------
|
|
694
|
+
|
|
695
|
+
// The manager's detail page for this bundle gives the component row a
|
|
696
|
+
// configure arrow once the bundle registers its own row.config entry (key
|
|
697
|
+
// `<bundle package>#<row id>`). The card carries the one setting the dialog
|
|
698
|
+
// also exposes: whether a session delete asks first.
|
|
699
|
+
function AskBeforeDeleteOption({ view }) {
|
|
700
|
+
const [ask, setAsk] = react.useState(!skipConfirm())
|
|
701
|
+
react.useEffect(() => subscribeSkip(() => setAsk(!skipConfirm())), [])
|
|
702
|
+
if (view === 'summary') return t(ask ? 'pageSummaryAsk' : 'pageSummaryAuto')
|
|
703
|
+
return react.createElement(
|
|
704
|
+
'div',
|
|
705
|
+
{ className: 'dsdel-page' },
|
|
706
|
+
react.createElement(primitives.Checkbox, {
|
|
707
|
+
checked: ask,
|
|
708
|
+
label: t('pageAsk'),
|
|
709
|
+
onChange: (checked) => {
|
|
710
|
+
setAsk(checked === true)
|
|
711
|
+
setSkipConfirm(checked !== true)
|
|
712
|
+
},
|
|
713
|
+
}),
|
|
714
|
+
react.createElement('p', { className: 'dsdel-page-hint' }, t(ask ? 'pageHintAsk' : 'pageHintAuto')),
|
|
715
|
+
)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function installSettings(slots) {
|
|
719
|
+
slots.inject('plugins.row.config', () =>
|
|
720
|
+
slots.register(
|
|
721
|
+
{ name: 'plugins.row.config', key: 'dsh-delete-session#dsh-delete-session' },
|
|
722
|
+
AskBeforeDeleteOption,
|
|
723
|
+
),
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
|
|
677
727
|
function install() {
|
|
678
728
|
if (typeof document === 'undefined' || !document.body) return null
|
|
679
729
|
ensureStyle()
|
|
@@ -704,6 +754,11 @@ window.__ModuleLoader__.load({
|
|
|
704
754
|
if (!localeSvc) {
|
|
705
755
|
try { ctx.inject(['locale'], (sub) => { localeSvc = sub.locale }) } catch { /* optional */ }
|
|
706
756
|
}
|
|
757
|
+
const slots = ctx.get('slots')
|
|
758
|
+
if (slots) installSettings(slots)
|
|
759
|
+
else {
|
|
760
|
+
try { ctx.inject(['slots'], (sub) => installSettings(sub.slots)) } catch { /* optional */ }
|
|
761
|
+
}
|
|
707
762
|
const dispose = install()
|
|
708
763
|
if (dispose) ctx.effect(() => dispose)
|
|
709
764
|
}
|