dsh-session-mng 0.1.3 → 0.1.4
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/CHANGELOG.md +17 -0
- package/README.md +11 -9
- package/ROADMAP.md +9 -2
- package/client.js +180 -55
- package/docs/02-open-source-and-marketplace.md +8 -0
- package/docs/RELEASE-CHECKLIST-0.1.3.md +7 -5
- package/docs/RELEASE-CHECKLIST-0.1.4.md +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 — 2026-10-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **会话详情页标题行不随页面滚动**:详情页顶部(「← 返回列表」、标题、运行/分支等徽标、「打开 / 归档」按钮)改为吸顶(`position: sticky`),滚动摘要、生成标题、最近消息等内容时始终可见,与列表页的筛选工具栏行为一致。
|
|
8
|
+
- **操作结果提示不再常驻**:提示改为浮动 toast(固定在右下角,不随列表滚动消失),纯成功提示 5 秒后自动隐藏;失败提示与带操作按钮的提示保留到手动关闭,避免错过失败原因。
|
|
9
|
+
- **一键清理空会话超过 50 条时预览报错**:删除范围预览与彻底删除改为按 `maxBatch`(默认 50)分批请求并汇总结果,一次确认可清理任意数量(如「清理空会话(80)」)。
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **「归档空会话(N)」**:有未归档空会话时显示,一键分批(每批 50)归档,归档后即可用「清理空会话」删除。
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **「清理空会话(N)」显示条件**:只要存在已归档空会话就显示(不再要求其中必须有可删除项);数量按全部已归档空会话计算,若其中有在 DSH 中打开、暂时无法删除的,按钮禁用并在悬停提示「重启后才能清理」。
|
|
18
|
+
- **页面头部布局**:「归档空会话 / 清理空会话 / 刷新」三个按钮移到「会话管理」标题同一行的右侧(顶部右对齐),副标题与统计信息在下方独占整行。
|
|
19
|
+
|
|
3
20
|
## 0.1.3 — 2026-10-28
|
|
4
21
|
|
|
5
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ dsh plugin --profile <profile> add link:/绝对路径/dsh-session-mng
|
|
|
59
59
|
|
|
60
60
|
Host 半是进程内模块:装完如果看不到页面,重启一次 dsh/应用再刷新页面;Client 半刷新页面即可。
|
|
61
61
|
|
|
62
|
-
> 市场状态:awesome-dsh-plugin 收录 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672)
|
|
62
|
+
> 市场状态:awesome-dsh-plugin 收录 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 已于 2026-10-27 合并(`check` 与 `Submission gate` 均通过)。dshmarket 读取同一份目录,卡片版本随 npm 自动更新,当前显示 `0.1.3`。
|
|
63
63
|
|
|
64
64
|
## 使用
|
|
65
65
|
|
|
@@ -137,19 +137,20 @@ Host route (lib/api.js) ──► service (lib/service.js)
|
|
|
137
137
|
## 开发
|
|
138
138
|
|
|
139
139
|
```sh
|
|
140
|
-
npm test # node --test,
|
|
140
|
+
npm test # node --test,29 个单元/集成测试
|
|
141
141
|
node --check index.js client.js lib/*.js
|
|
142
142
|
npm --cache /tmp/dsh-npm-cache pack --dry-run
|
|
143
143
|
```
|
|
144
144
|
|
|
145
|
-
实机验收:安装到 `desktop` profile → 确认 `settings.section` 出现 `session-mng-section` → 用**一次性测试会话**验证标题/归档/恢复/移动/删除 → 清理测试数据。0.1.
|
|
145
|
+
实机验收:安装到 `desktop` profile → 确认 `settings.section` 出现 `session-mng-section` → 用**一次性测试会话**验证标题/归档/恢复/移动/删除 → 清理测试数据。0.1.3 的验收与实机记录见 `docs/RELEASE-CHECKLIST-0.1.3.md`。
|
|
146
146
|
|
|
147
147
|
## 当前状态与下一步
|
|
148
148
|
|
|
149
|
-
- `0.1.
|
|
150
|
-
- `0.1.
|
|
151
|
-
- `0.1.
|
|
152
|
-
-
|
|
149
|
+
- `0.1.4` 已发布到 npm / GitHub / Release:详情页标题行吸顶;操作结果改为浮动 toast(成功 5 秒自动消失);头部按钮移到标题右侧;新增「归档空会话(N)」,「清理空会话(N)」只要有已归档空会话就显示,且超过 50 条自动分批预览/删除。
|
|
150
|
+
- `0.1.3` 已发布到 npm / GitHub / Release:三态会话模型(根/分支/子代理),分支默认显示并缩进在父会话下;删除父会话不再级联删掉普通分支;父会话有历史分支时也能移动;兼容声明增加 DSH `0.2.1-rc.3`。
|
|
151
|
+
- `0.1.2` 已发布:空会话默认隐藏、清理空会话、删除范围模态确认。
|
|
152
|
+
- 29 个测试全绿;0.1.4 包 37 个文件 / 583.2 kB(unpacked 834.8 kB)。
|
|
153
|
+
- 市场条目已收录:PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 已于 2026-10-27 合并(`check` 与仓库年龄 gate 均通过);卡片版本随 npm 自动更新,当前 0.1.4。
|
|
153
154
|
- 下一项主任务:**0.2.0 Host 侧 overview + 分页 sessions**,为 1000–10000 条会话设计;详细任务、验收标准、环境坑见 [接手说明](docs/HANDOVER-2026-10-27.md)。
|
|
154
155
|
|
|
155
156
|
## 文档索引
|
|
@@ -159,10 +160,11 @@ npm --cache /tmp/dsh-npm-cache pack --dry-run
|
|
|
159
160
|
- [0.1.0 发布检查清单](docs/RELEASE-CHECKLIST-0.1.0.md)
|
|
160
161
|
- [0.1.1 实施计划](docs/PLAN-2026-10-27.md)
|
|
161
162
|
- [0.1.1 发布检查清单](docs/RELEASE-CHECKLIST-0.1.1.md)
|
|
162
|
-
- [0.1.2
|
|
163
|
+
- [0.1.2 发布检查清单](docs/RELEASE-CHECKLIST-0.1.2.md)
|
|
163
164
|
- [0.1.3 实施计划(分支会话默认可见)](docs/PLAN-2026-10-28-branch-visibility.md)
|
|
164
165
|
- [0.1.3 实施计划(历史分支删除/移动安全)](docs/PLAN-2026-10-28-legacy-fork-safety.md)
|
|
165
|
-
- [0.1.3
|
|
166
|
+
- [0.1.3 发布检查清单](docs/RELEASE-CHECKLIST-0.1.3.md)
|
|
167
|
+
- [0.1.4 发布检查清单](docs/RELEASE-CHECKLIST-0.1.4.md)
|
|
166
168
|
- [设计与实现说明](docs/01-design.md)
|
|
167
169
|
- [开源、发 npm 与市场收录记录](docs/02-open-source-and-marketplace.md)
|
|
168
170
|
- [路线图](ROADMAP.md)
|
package/ROADMAP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
-
> 当前:`0.1.
|
|
3
|
+
> 当前:`0.1.4` 已发布(npm latest / tag `v0.1.4` / Release):详情页吸顶、结果 toast、空会话归档/清理与分批删除。市场条目已收录(PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 于 2026-10-27 合并)。下一项主任务是 0.2.0 的 Host 分页;详细接手任务见 [docs/HANDOVER-2026-10-27.md](docs/HANDOVER-2026-10-27.md)。
|
|
4
4
|
|
|
5
5
|
## 0.1.0 — delivered
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- 「清理空会话(N)」批量清理已归档空会话,复用删除范围预览/确认。
|
|
26
26
|
- 删除范围确认改为模态框,内容独立滚动、底部确认区固定,支持遮罩/Esc 取消。
|
|
27
27
|
|
|
28
|
-
## 0.1.3 — delivered (
|
|
28
|
+
## 0.1.3 — delivered (published)
|
|
29
29
|
|
|
30
30
|
- 三态会话模型:根 / 分支(fork)/ 子代理;分支不再因 `parentSession` 被误判成子会话隐藏。
|
|
31
31
|
- 分支会话默认显示,缩进挂在父行下,父行「N 分支」、分支行「↳ 来自:父标题」;搜索父标题命中分支。
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
- 删除只级联子代理:历史分支保留(预览提示会失去来源标识),不再静默删除;父会话有历史分支时也能移动。
|
|
34
34
|
- 分支的来源父会话被删除后显示「来源父会话已不存在」;移动有分支的会话时提示分支留在原工作区。
|
|
35
35
|
|
|
36
|
+
## 0.1.4 — delivered (published)
|
|
37
|
+
|
|
38
|
+
- 详情页标题/操作行吸顶(返回列表、标题、徽标、打开/归档始终可见)。
|
|
39
|
+
- 操作结果改为浮动 toast:成功提示 5 秒自动消失,失败/带操作提示保留。
|
|
40
|
+
- 头部三个按钮移到会话管理标题右侧;副标题与统计信息独占下方整行。
|
|
41
|
+
- 空会话:「归档空会话(N)」一键分批归档;「清理空会话(N)」只要有已归档空会话就显示;删除范围预览与删除按 `maxBatch` 分批,超过 50 条也能一次确认完成。
|
|
42
|
+
|
|
36
43
|
## 0.2.0 — next (planned)
|
|
37
44
|
|
|
38
45
|
首要目标:真正支撑 1000–10000 条会话,不再首屏拉全量 `state`。实施前先写 `docs/PLAN-2026-10-28.md` 并获批准。
|
package/client.js
CHANGED
|
@@ -48,7 +48,9 @@ window.__ModuleLoader__.load({
|
|
|
48
48
|
showBlank: '显示空会话',
|
|
49
49
|
cleanupBlank: '清理空会话({n})',
|
|
50
50
|
cleanupBlankHint: '批量删除已归档的空会话;未归档的空会话需先归档。',
|
|
51
|
-
|
|
51
|
+
cleanupBlankLockedHint: '有 {n} 个已归档空会话已在 DSH 中打开,重启后才能清理。',
|
|
52
|
+
archiveBlank: '归档空会话({n})',
|
|
53
|
+
archiveBlankHint: '把未归档的空会话批量归档;归档后即可用「清理空会话」彻底删除。',
|
|
52
54
|
hiddenBlankCount: '已隐藏 {n} 个空会话',
|
|
53
55
|
selectAll: '全选当前列表',
|
|
54
56
|
expandAll: '展开全部',
|
|
@@ -212,7 +214,9 @@ window.__ModuleLoader__.load({
|
|
|
212
214
|
showBlank: 'Show empty sessions',
|
|
213
215
|
cleanupBlank: 'Clean up empty sessions ({n})',
|
|
214
216
|
cleanupBlankHint: 'Permanently delete archived empty sessions; unarchived empty sessions must be archived first.',
|
|
215
|
-
|
|
217
|
+
cleanupBlankLockedHint: '{n} archived empty sessions are open in this DSH process; restart DSH before cleaning them up.',
|
|
218
|
+
archiveBlank: 'Archive empty sessions ({n})',
|
|
219
|
+
archiveBlankHint: 'Archive every unarchived empty session in one batch; they can then be cleaned up with "Clean up empty sessions".',
|
|
216
220
|
hiddenBlankCount: '{n} empty sessions hidden',
|
|
217
221
|
selectAll: 'Select all shown',
|
|
218
222
|
expandAll: 'Expand all',
|
|
@@ -356,10 +360,13 @@ window.__ModuleLoader__.load({
|
|
|
356
360
|
|
|
357
361
|
const CSS = `
|
|
358
362
|
.dsm-page{display:flex;flex-direction:column;gap:12px;font:inherit;color:var(--dsw-alias-label-primary);min-width:0}
|
|
359
|
-
|
|
363
|
+
/* Page head: title and its actions share the first row; the subtitles use the
|
|
364
|
+
full width below, so the buttons sit in the top-right corner. */
|
|
365
|
+
.dsm-head{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
366
|
+
.dsm-head-row{display:flex;align-items:center;justify-content:flex-start;gap:12px;flex-wrap:wrap;min-width:0}
|
|
360
367
|
.dsm-title{margin:0;font-size:17px;font-weight:600;line-height:24px}
|
|
361
368
|
.dsm-sub{margin:2px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
|
|
362
|
-
.dsm-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
369
|
+
.dsm-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-left:auto}
|
|
363
370
|
.dsm-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:28px;padding:0 10px;
|
|
364
371
|
border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);
|
|
365
372
|
font:inherit;font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap}
|
|
@@ -387,6 +394,12 @@ window.__ModuleLoader__.load({
|
|
|
387
394
|
.dsm-note.warn{border-color:var(--dsw-alias-state-warn-primary)}
|
|
388
395
|
.dsm-note-actions{display:flex;gap:6px;flex-wrap:wrap;flex:0 0 auto}
|
|
389
396
|
.dsm-note detail{flex:1 1 100%}
|
|
397
|
+
/* Operation-result toast: floats above the pane so scrolling never hides it. */
|
|
398
|
+
.dsm-toast{position:fixed;bottom:18px;right:18px;z-index:1200;width:max-content;
|
|
399
|
+
max-width:min(460px,calc(100vw - 36px));padding:10px 12px;border-radius:8px;
|
|
400
|
+
background:var(--dsw-alias-bg-layer-2);box-shadow:0 14px 40px rgba(0,0,0,.45);
|
|
401
|
+
animation:dsm-toast-in .16s ease-out}
|
|
402
|
+
@keyframes dsm-toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
390
403
|
.dsm-sticky{position:sticky;top:0;z-index:3;display:flex;flex-direction:column;gap:8px;
|
|
391
404
|
padding:6px 0 8px;background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
392
405
|
.dsm-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:7px;
|
|
@@ -438,7 +451,10 @@ window.__ModuleLoader__.load({
|
|
|
438
451
|
.dsm-empty{padding:22px 10px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary);
|
|
439
452
|
border:1px dashed var(--dsw-alias-border-l2);border-radius:8px}
|
|
440
453
|
.dsm-detail{display:flex;flex-direction:column;gap:12px}
|
|
441
|
-
.
|
|
454
|
+
/* The title row and its actions stay in view while the detail page scrolls. */
|
|
455
|
+
.dsm-detail-head{position:sticky;top:0;z-index:2;display:flex;align-items:flex-start;justify-content:space-between;
|
|
456
|
+
gap:10px;flex-wrap:wrap;padding:6px 0 8px;background:var(--dsw-alias-bg-layer-2);
|
|
457
|
+
border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
442
458
|
.dsm-detail-title{margin:0;font-size:16px;line-height:23px;font-weight:600;word-break:break-word}
|
|
443
459
|
.dsm-card{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);padding:10px}
|
|
444
460
|
.dsm-card h3{margin:0 0 8px;font-size:12px;line-height:18px;font-weight:600}
|
|
@@ -655,11 +671,33 @@ window.__ModuleLoader__.load({
|
|
|
655
671
|
return h('span', null, h('span', { className: 'dsm-spin', 'aria-hidden': 'true' }), ' ', label ?? t('working'))
|
|
656
672
|
}
|
|
657
673
|
|
|
674
|
+
/** How long a plain success toast stays before hiding itself. */
|
|
675
|
+
const NOTICE_HIDE_MS = 5000
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Operation-result toast.
|
|
679
|
+
*
|
|
680
|
+
* It floats above the settings pane (instead of sitting in the scroll flow),
|
|
681
|
+
* so a result is visible no matter where the list is scrolled. A plain
|
|
682
|
+
* success toast hides itself after a few seconds; failures and notices with
|
|
683
|
+
* an action stay until the user dismisses them, because they carry details
|
|
684
|
+
* worth reading.
|
|
685
|
+
*/
|
|
658
686
|
function Notice({ notice, t, onDismiss }) {
|
|
687
|
+
const dismiss = React.useRef(onDismiss)
|
|
688
|
+
dismiss.current = onDismiss
|
|
689
|
+
const transient = notice !== null && notice !== undefined
|
|
690
|
+
&& notice.tone !== 'error' && notice.tone !== 'warn'
|
|
691
|
+
&& notice.details === undefined && notice.action === undefined
|
|
692
|
+
React.useEffect(() => {
|
|
693
|
+
if (!transient) return undefined
|
|
694
|
+
const timer = setTimeout(() => dismiss.current?.(), NOTICE_HIDE_MS)
|
|
695
|
+
return () => clearTimeout(timer)
|
|
696
|
+
}, [notice, transient])
|
|
659
697
|
if (notice === null || notice === undefined) return null
|
|
660
698
|
return h(
|
|
661
699
|
'div',
|
|
662
|
-
{ className: `dsm-note ${notice.tone ?? ''}`, role: 'status' },
|
|
700
|
+
{ className: `dsm-note dsm-toast ${notice.tone ?? ''}`, role: 'status' },
|
|
663
701
|
h('div', { style: { flex: '1 1 auto', minWidth: 0 } },
|
|
664
702
|
h('div', null, notice.text),
|
|
665
703
|
notice.detail ? h('div', { className: 'dsm-hint', style: { marginTop: 3 } }, notice.detail) : null,
|
|
@@ -982,30 +1020,44 @@ window.__ModuleLoader__.load({
|
|
|
982
1020
|
// Delete scope (shared by single and batch)
|
|
983
1021
|
// ---------------------------------------------------------------------
|
|
984
1022
|
|
|
985
|
-
function DeleteScope({ ctx, t, ids, onConfirmed, onCancel, busy }) {
|
|
1023
|
+
function DeleteScope({ ctx, t, ids, maxBatch, onConfirmed, onCancel, busy }) {
|
|
986
1024
|
const [phase, setPhase] = React.useState('loading')
|
|
987
1025
|
const [preview, setPreview] = React.useState(null)
|
|
988
1026
|
const [error, setError] = React.useState(null)
|
|
989
1027
|
const [checked, setChecked] = React.useState(false)
|
|
990
1028
|
const idsKey = ids.join('\u0000')
|
|
991
1029
|
|
|
1030
|
+
// One confirmation may name more sessions than a single request admits
|
|
1031
|
+
// (the empty-session shortcut does). Preview in batches and merge the
|
|
1032
|
+
// scopes so the dialog still shows one combined result.
|
|
992
1033
|
React.useEffect(() => {
|
|
993
1034
|
const controller = new AbortController()
|
|
994
1035
|
setPhase('loading')
|
|
995
1036
|
setChecked(false)
|
|
996
|
-
|
|
997
|
-
|
|
1037
|
+
const size = Number.isFinite(maxBatch) && maxBatch > 0 ? maxBatch : 50
|
|
1038
|
+
const merge = async () => {
|
|
1039
|
+
const merged = { roots: [], errors: [], sessions: [], descendantCount: 0, forkChildren: [], totalBytes: 0 }
|
|
1040
|
+
for (let index = 0; index < ids.length; index += size) {
|
|
1041
|
+
const value = await post('delete/preview', { sessionIds: ids.slice(index, index + size) }, controller.signal)
|
|
998
1042
|
if (controller.signal.aborted) return
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1043
|
+
merged.roots.push(...(value?.roots ?? []))
|
|
1044
|
+
merged.errors.push(...(value?.errors ?? []))
|
|
1045
|
+
merged.sessions.push(...(value?.sessions ?? []))
|
|
1046
|
+
merged.descendantCount += value?.descendantCount ?? 0
|
|
1047
|
+
merged.forkChildren.push(...(value?.forkChildren ?? []))
|
|
1048
|
+
merged.totalBytes += value?.totalBytes ?? 0
|
|
1049
|
+
}
|
|
1050
|
+
if (controller.signal.aborted) return
|
|
1051
|
+
setPreview(merged)
|
|
1052
|
+
setPhase('ready')
|
|
1053
|
+
}
|
|
1054
|
+
merge().catch((failure) => {
|
|
1055
|
+
if (failure?.name === 'AbortError') return
|
|
1056
|
+
setError(errorText(t, failure))
|
|
1057
|
+
setPhase('error')
|
|
1058
|
+
})
|
|
1007
1059
|
return () => controller.abort()
|
|
1008
|
-
}, [idsKey, t])
|
|
1060
|
+
}, [idsKey, maxBatch, t])
|
|
1009
1061
|
|
|
1010
1062
|
React.useEffect(() => {
|
|
1011
1063
|
const onKeyDown = (event) => {
|
|
@@ -1272,6 +1324,7 @@ window.__ModuleLoader__.load({
|
|
|
1272
1324
|
ctx,
|
|
1273
1325
|
t,
|
|
1274
1326
|
ids: deleteIds,
|
|
1327
|
+
maxBatch: state.limits?.maxBatch ?? 50,
|
|
1275
1328
|
busy,
|
|
1276
1329
|
onCancel: () => setShowDelete(false),
|
|
1277
1330
|
onConfirmed: (ids) => onDelete(ids),
|
|
@@ -1559,17 +1612,43 @@ window.__ModuleLoader__.load({
|
|
|
1559
1612
|
return value
|
|
1560
1613
|
}
|
|
1561
1614
|
|
|
1615
|
+
/**
|
|
1616
|
+
* Permanently delete sessions in backend-sized batches. One confirmation
|
|
1617
|
+
* may cover more ids than a single request admits (the empty-session
|
|
1618
|
+
* shortcut can), so the request is split and the totals are summed into
|
|
1619
|
+
* one notice.
|
|
1620
|
+
*/
|
|
1562
1621
|
const deleteRows = async (ids) => {
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
)
|
|
1567
|
-
|
|
1622
|
+
if (ids.length === 0) return undefined
|
|
1623
|
+
const max = state?.limits?.maxBatch ?? 50
|
|
1624
|
+
setBusy(true)
|
|
1625
|
+
setNotice(null)
|
|
1626
|
+
try {
|
|
1627
|
+
let succeeded = 0
|
|
1628
|
+
let failed = 0
|
|
1629
|
+
const failures = []
|
|
1630
|
+
for (let index = 0; index < ids.length; index += max) {
|
|
1631
|
+
const value = await post('delete', { sessionIds: ids.slice(index, index + max), confirm: true })
|
|
1632
|
+
succeeded += value?.totals?.succeeded ?? 0
|
|
1633
|
+
failed += value?.totals?.failed ?? 0
|
|
1634
|
+
for (const item of value?.results ?? []) if (item.ok === false) failures.push(item)
|
|
1635
|
+
}
|
|
1636
|
+
setNotice(
|
|
1637
|
+
failed === 0
|
|
1638
|
+
? { tone: 'ok', text: t('batchDone', { ok: succeeded, failed: 0 }) }
|
|
1639
|
+
: { tone: 'error', text: t('batchDone', { ok: succeeded, failed }), details: failures.slice(0, 20) },
|
|
1640
|
+
)
|
|
1568
1641
|
setBulkDeleteIds(null)
|
|
1569
1642
|
setSelected(new Set())
|
|
1570
1643
|
if (selectedId !== null && ids.includes(selectedId)) setSelectedId(null)
|
|
1644
|
+
await load({ silent: true })
|
|
1645
|
+
return { totals: { succeeded, failed } }
|
|
1646
|
+
} catch (failure) {
|
|
1647
|
+
setNotice({ tone: 'error', text: errorText(t, failure), detail: failure?.message })
|
|
1648
|
+
return undefined
|
|
1649
|
+
} finally {
|
|
1650
|
+
setBusy(false)
|
|
1571
1651
|
}
|
|
1572
|
-
return value
|
|
1573
1652
|
}
|
|
1574
1653
|
|
|
1575
1654
|
const cleanupOrphans = async () => {
|
|
@@ -1593,6 +1672,40 @@ window.__ModuleLoader__.load({
|
|
|
1593
1672
|
}
|
|
1594
1673
|
}
|
|
1595
1674
|
|
|
1675
|
+
/**
|
|
1676
|
+
* Archive every unarchived empty session, in backend-sized batches. The
|
|
1677
|
+
* user can then clean them up with one confirm dialog.
|
|
1678
|
+
*/
|
|
1679
|
+
const archiveBlankSessions = async (ids) => {
|
|
1680
|
+
if (ids.length === 0) return
|
|
1681
|
+
const max = state?.limits?.maxBatch ?? 50
|
|
1682
|
+
setBusy(true)
|
|
1683
|
+
setNotice(null)
|
|
1684
|
+
try {
|
|
1685
|
+
let archived = 0
|
|
1686
|
+
const failures = []
|
|
1687
|
+
for (let index = 0; index < ids.length; index += max) {
|
|
1688
|
+
const value = await post('archive', { sessionIds: ids.slice(index, index + max) })
|
|
1689
|
+
archived += value?.totals?.succeeded ?? 0
|
|
1690
|
+
for (const item of value?.results ?? []) if (item.ok === false) failures.push(item)
|
|
1691
|
+
}
|
|
1692
|
+
if (failures.length === 0) {
|
|
1693
|
+
setNotice({ tone: 'ok', text: t('archiveDone', { n: archived }) })
|
|
1694
|
+
} else {
|
|
1695
|
+
setNotice({
|
|
1696
|
+
tone: 'error',
|
|
1697
|
+
text: t('batchDone', { ok: archived, failed: failures.length }),
|
|
1698
|
+
details: failures.slice(0, 20),
|
|
1699
|
+
})
|
|
1700
|
+
}
|
|
1701
|
+
await load({ silent: true })
|
|
1702
|
+
} catch (failure) {
|
|
1703
|
+
setNotice({ tone: 'error', text: errorText(t, failure) })
|
|
1704
|
+
} finally {
|
|
1705
|
+
setBusy(false)
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1596
1709
|
const openSession = (id) => {
|
|
1597
1710
|
let service
|
|
1598
1711
|
try {
|
|
@@ -1629,8 +1742,9 @@ window.__ModuleLoader__.load({
|
|
|
1629
1742
|
|
|
1630
1743
|
const rowsForTabs = rows.filter((row) => showBlank || row.blank !== true)
|
|
1631
1744
|
const blankRows = rows.filter((row) => row.blank === true)
|
|
1632
|
-
const
|
|
1633
|
-
const
|
|
1745
|
+
const blankUnarchivedRows = blankRows.filter((row) => row.archived !== true)
|
|
1746
|
+
const blankArchivedRows = blankRows.filter((row) => row.archived === true)
|
|
1747
|
+
const blankDeletableRows = blankArchivedRows.filter((row) => row.can?.delete === true)
|
|
1634
1748
|
const tabs = [
|
|
1635
1749
|
{ key: 'all', label: t('tabAll'), count: rowsForTabs.length },
|
|
1636
1750
|
{ key: 'active', label: t('tabActive'), count: rowsForTabs.filter((row) => !row.archived).length },
|
|
@@ -1731,38 +1845,48 @@ window.__ModuleLoader__.load({
|
|
|
1731
1845
|
h(
|
|
1732
1846
|
'div',
|
|
1733
1847
|
{ className: 'dsm-head' },
|
|
1734
|
-
h(
|
|
1848
|
+
h(
|
|
1849
|
+
'div',
|
|
1850
|
+
{ className: 'dsm-head-row' },
|
|
1735
1851
|
h('h2', { className: 'dsm-title' }, t('pageTitle')),
|
|
1736
|
-
h('
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
:
|
|
1746
|
-
|
|
1747
|
-
?
|
|
1748
|
-
|
|
1852
|
+
h('div', { className: 'dsm-head-actions' },
|
|
1853
|
+
blankUnarchivedRows.length > 0
|
|
1854
|
+
? h('button', {
|
|
1855
|
+
type: 'button',
|
|
1856
|
+
className: 'dsm-btn',
|
|
1857
|
+
disabled: busy,
|
|
1858
|
+
title: t('archiveBlankHint'),
|
|
1859
|
+
onClick: () => void archiveBlankSessions(blankUnarchivedRows.map((row) => row.id)),
|
|
1860
|
+
}, t('archiveBlank', { n: blankUnarchivedRows.length }))
|
|
1861
|
+
: null,
|
|
1862
|
+
blankArchivedRows.length > 0
|
|
1863
|
+
? h('button', {
|
|
1864
|
+
type: 'button',
|
|
1865
|
+
className: 'dsm-btn',
|
|
1866
|
+
disabled: busy || blankDeletableRows.length === 0,
|
|
1867
|
+
title: blankDeletableRows.length === blankArchivedRows.length
|
|
1868
|
+
? t('cleanupBlankHint')
|
|
1869
|
+
: t('cleanupBlankLockedHint', { n: blankArchivedRows.length - blankDeletableRows.length }),
|
|
1870
|
+
onClick: () => setBulkDeleteIds(blankDeletableRows.map((row) => row.id)),
|
|
1871
|
+
}, t('cleanupBlank', { n: blankArchivedRows.length }))
|
|
1872
|
+
: null,
|
|
1873
|
+
h('button', { type: 'button', className: 'dsm-btn', disabled: busy, onClick: () => void load({ silent: true }) },
|
|
1874
|
+
busy ? h(Spinner, { t, label: t('working') }) : t('refresh')),
|
|
1749
1875
|
),
|
|
1750
|
-
blankActiveCount > 0 && showBlank
|
|
1751
|
-
? h('p', { className: 'dsm-hint' }, t('blankActiveHint', { n: blankActiveCount }))
|
|
1752
|
-
: null,
|
|
1753
1876
|
),
|
|
1754
|
-
h('
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
:
|
|
1764
|
-
|
|
1765
|
-
|
|
1877
|
+
h('p', { className: 'dsm-sub' }, t('pageSubtitle')),
|
|
1878
|
+
h('p', { className: 'dsm-sub' },
|
|
1879
|
+
t('totalLine', {
|
|
1880
|
+
all: state.totals.all,
|
|
1881
|
+
archived: state.totals.archived,
|
|
1882
|
+
size: formatBytes(state.totals.bytes),
|
|
1883
|
+
}),
|
|
1884
|
+
blankRows.length > 0 && !showBlank
|
|
1885
|
+
? ` · ${t('hiddenBlankCount', { n: blankRows.length })}`
|
|
1886
|
+
: '',
|
|
1887
|
+
(state.totals?.hiddenSubagents ?? 0) > 0 && !includeSubagents
|
|
1888
|
+
? ` · ${t('hiddenSubagentCount', { n: state.totals.hiddenSubagents })}`
|
|
1889
|
+
: '',
|
|
1766
1890
|
),
|
|
1767
1891
|
),
|
|
1768
1892
|
h(Notice, { notice, t, onDismiss: () => setNotice(null) }),
|
|
@@ -1932,6 +2056,7 @@ window.__ModuleLoader__.load({
|
|
|
1932
2056
|
ctx,
|
|
1933
2057
|
t,
|
|
1934
2058
|
ids: bulkDeleteIds,
|
|
2059
|
+
maxBatch: state.limits?.maxBatch ?? 50,
|
|
1935
2060
|
busy,
|
|
1936
2061
|
onCancel: () => setBulkDeleteIds(null),
|
|
1937
2062
|
onConfirmed: (ids) => void deleteRows(ids),
|
|
@@ -139,3 +139,11 @@ curl -s https://awesome-dsh-plugin.com/plugins.json | grep -i session-mng
|
|
|
139
139
|
- `npm publish`:`dsh-session-mng@0.1.2`,33 个文件,568.3 kB / unpacked 789.1 kB;约 3 分钟后 packument 出现,`latest` = 0.1.2,版本端点 200。
|
|
140
140
|
- Git:main 已 push;tag `v0.1.2`;Release 见仓库 Releases。
|
|
141
141
|
- marketplace 条目不变;卡片版本随 npm 自动更新,PR gate 仍只差仓库满 1 天。
|
|
142
|
+
|
|
143
|
+
## 10. 0.1.3 更新发布(2026-10-28)
|
|
144
|
+
|
|
145
|
+
- 功能修复:分支会话三态模型(根/分支/子代理)、分支默认可见并缩进;删除父会话不再级联普通分支;父会话有历史分支时也能移动;兼容声明增加 `0.2.1-rc.3`。
|
|
146
|
+
- `npm publish`:`dsh-session-mng@0.1.3`,36 个文件,579.7 kB / unpacked 821.7 kB,shasum `e524c50988bb55a97b37ed08b6d94abcfc372b03`;约 1 分钟后 packument 出现 0.1.3,`latest` = 0.1.3。
|
|
147
|
+
- Git:main 已 push(功能提交 `a827495`);tag `v0.1.3`;Release https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.3
|
|
148
|
+
- marketplace:条目不需要改 YAML,卡片版本随 npm 自动更新;PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 仍只差仓库年龄 gate。
|
|
149
|
+
- 遗留:当前安装的 DSH `0.2.1-rc.3` 构建仍写 fork 的 `parentSession`(「独立 fork」变更只在 DSH 源码里),所以历史分支形态会继续出现,0.1.3 的删除/移动策略长期有效。
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
- [ ] 子龙实机复核完整流程(重启/重载 Desktop 后):工作区从 1 条变 2 条,分支缩进在父会话下;勾选开关后子代理出现;搜索父标题命中分支;删除父会话的预览里出现分支保留提示;父会话移动不再报 `has-descendants`。
|
|
31
31
|
- [ ] 若缩进/徽标观感需要微调,再改一轮。
|
|
32
32
|
|
|
33
|
-
## C.
|
|
33
|
+
## C. 发布(已完成)
|
|
34
34
|
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
35
|
+
- [x] `npm --cache /tmp/dsh-npm-cache --logs-dir /tmp/dsh-npm-logs pack --dry-run`:36 个文件 / 579.7 kB(unpacked 821.7 kB),shasum `e524c50988bb55a97b37ed08b6d94abcfc372b03`。
|
|
36
|
+
- [x] `npm publish` 0.1.3 成功;约 1 分钟后 packument 出现 0.1.3,`latest` = 0.1.3,`dist.fileCount` = 36。
|
|
37
|
+
- [x] Git:功能提交 `a827495`,main 已 push;tag `v0.1.3`;GitHub Release https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.3
|
|
38
|
+
- [x] 更新 CHANGELOG、README 当前状态、ROADMAP、`docs/02-open-source-and-marketplace.md`(0.1.3 发布记录)。
|
|
39
|
+
- [x] marketplace 条目不需要改 YAML(版本随 npm 自动更新;PR #9672 仍等仓库年龄 gate)。
|
|
40
|
+
- [ ] 子龙实机复核(见 B 节):缩进/徽标观感、删除预览分支提示、父会话移动。
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# dsh-session-mng 0.1.4 发布检查清单
|
|
2
|
+
|
|
3
|
+
> 记录时间:2026-10-28。0.1.4 是详情页吸顶 + 操作提示与空会话入口三个交互修复,Host API 未变。
|
|
4
|
+
|
|
5
|
+
## A. 实现
|
|
6
|
+
|
|
7
|
+
- [x] `client.js`:`.dsm-detail-head` 改为 `position: sticky`(不透明背景 + 底部分隔线),返回列表/标题/徽标/打开归档始终可见。
|
|
8
|
+
- [x] `client.js`:操作结果提示改为右下角浮层 toast(`position: fixed`),不随滚动消失。
|
|
9
|
+
- [x] `client.js`:纯成功提示 5 秒后自动隐藏;失败与带 action 的提示保留到手动关闭。
|
|
10
|
+
- [x] `client.js`:新增「归档空会话(N)」按钮(有未归档空会话时显示),按 `maxBatch` 分批归档并刷新。
|
|
11
|
+
- [x] `client.js`:「清理空会话(N)」改为只要有已归档空会话就显示;数量按全部已归档空会话计;存在已打开(不可删)项时禁用并提示「重启后才能清理」。
|
|
12
|
+
- [x] 中英双语文案(`archiveBlank` / `archiveBlankHint` / `cleanupBlankLockedHint`,移除 `blankActiveHint`)。
|
|
13
|
+
- [x] 页面头部重排:标题与三个操作按钮同一行(按钮右对齐),副标题/统计行在下方独占整行(`.dsm-head-row` + `margin-left:auto`)。
|
|
14
|
+
- [x] 删除范围预览与删除请求按 `maxBatch` 分批(空会话快捷清理可超过 50 条),结果合并成一个确认/一条提示。
|
|
15
|
+
|
|
16
|
+
## B. 验证
|
|
17
|
+
|
|
18
|
+
- [x] `node --check client.js`;i18n 键覆盖检查(zh/en 各 164 项,引用键全部存在)。
|
|
19
|
+
- [x] `npm test`:29 个测试全绿(Host 未改)。
|
|
20
|
+
- [x] 客户端无头渲染冒烟(假 state:2 条未归档空会话 + 2 条已归档空会话,其中 1 条已打开):
|
|
21
|
+
- 头部出现「归档空会话(2)」与「清理空会话(2)」,清理按钮可用、悬停提示含「重启后才能清理」。
|
|
22
|
+
- 点击归档 → 发出 `POST /archive`(2 条),出现 toast「已归档 2 个会话。」。
|
|
23
|
+
- 等待 5.3 秒后 toast 自动消失。
|
|
24
|
+
- 头部结构为「标题行(标题 + 按钮)→ 副标题 → 统计行」;「归档空会话」「刷新」等按钮都在标题行内。
|
|
25
|
+
- 80 条已归档空会话:点「清理空会话(80)」→ 预览分 2 批(50+30)请求并合并显示「根会话: 80」;勾选确认后删除也分 2 批,toast 汇总「成功 80,失败 0」。
|
|
26
|
+
- [ ] 子龙实机:滚动详情页时标题行保持可见;操作后提示在右下角且自动消失;空会话按钮行为符合预期。
|
|
27
|
+
|
|
28
|
+
## C. 发布(未开始)
|
|
29
|
+
|
|
30
|
+
- [ ] `npm --cache /tmp/dsh-npm-cache --logs-dir /tmp/dsh-npm-logs pack --dry-run`。
|
|
31
|
+
- [ ] `npm publish` 0.1.4;确认 `latest` = 0.1.4。
|
|
32
|
+
- [ ] Git commit / push / tag `v0.1.4` / GitHub Release。
|
|
33
|
+
- [ ] 更新 README 当前状态、ROADMAP、`docs/02-open-source-and-marketplace.md`。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-mng",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Session management for DeepSeek Harness: one Settings page for every session — whole-session title generation, archive/restore, cross-workspace move, and permanent deletion of archived sessions.",
|
|
6
6
|
"license": "MIT",
|