dsh-session-mng 0.1.0 → 0.1.2
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 +29 -0
- package/README.md +25 -1
- package/ROADMAP.md +29 -1
- package/assets/screenshot-1-overview.png +0 -0
- package/assets/screenshot-4-expanded.png +0 -0
- package/client.js +369 -106
- package/docs/01-design.md +13 -0
- package/docs/02-open-source-and-marketplace.md +32 -3
- package/docs/HANDOVER-2026-10-27.md +133 -0
- package/docs/PLAN-2026-10-26.md +2 -0
- package/docs/PLAN-2026-10-27.md +71 -0
- package/docs/RELEASE-CHECKLIST-0.1.0.md +5 -5
- package/docs/RELEASE-CHECKLIST-0.1.1.md +55 -0
- package/docs/RELEASE-CHECKLIST-0.1.2.md +31 -0
- package/package.json +1 -1
- package/screenshots.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-10-27(unreleased)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- 默认隐藏空会话(`blank: true`):这类会话本来就不出现在侧栏会话列表;现在「会话管理」默认也不显示,可用「显示空会话」临时查看。
|
|
8
|
+
- 「清理空会话(N)」:批量清理**已归档且可删除**的空会话,仍先弹出删除范围确认;未归档空会话会提示先归档。
|
|
9
|
+
- 全部/活跃/已归档 tab 计数与隐藏空会话后的可见列表一致;总览行显示「已隐藏 N 个空会话」。
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- 删除范围确认改为**模态对话框**:内容区独立滚动,底部「我已了解」勾选与「确认彻底删除 / 取消」固定可见,不再随列表滚走。
|
|
14
|
+
- 删除范围弹窗支持点击遮罩取消、Esc 取消;加载/错误状态仍显示取消按钮。
|
|
15
|
+
|
|
16
|
+
## 0.1.1 — 2026-10-27
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- 会话列表改为**工作区优先**:会话总数 >100 时默认折叠所有工作区,点击展开;≤100 保持展开。折叠时只渲染工作区头,不渲染行。
|
|
21
|
+
- 每个工作区默认渲染 50 条,支持「加载更多(剩余 N 条)」;搜索/筛选后自动回到前 50 条。
|
|
22
|
+
- 搜索命中时自动展开命中的工作区,并禁用折叠/展开全部/收起全部,提示“搜索时自动展开命中工作区”。
|
|
23
|
+
- 过滤工具栏 + 批量操作条合并为同一个 sticky 头部,滚动时筛选条件与批量操作始终可见。
|
|
24
|
+
- 选择集裁剪到当前筛选与已展开工作区的可见集:切换 tab、工作区、搜索、折叠工作区、刷新后,隐藏会话自动取消选择;`全选当前列表` 只作用于可见集。
|
|
25
|
+
- 批量条增加“选择仅作用于当前筛选与展开结果”提示;批量移动不可用时区分 live 会话与一般不可移动项。
|
|
26
|
+
- 新增「展开全部 / 收起全部」。
|
|
27
|
+
|
|
28
|
+
### Compatibility
|
|
29
|
+
|
|
30
|
+
- 与 0.1.0 相同:DSH `0.2.1-rc.2` 实测;移动需要 Node `>=22.15`;移动/删除仅支持本地 JSONL 后端。
|
|
31
|
+
|
|
3
32
|
## 0.1.0 — 2026-10-27
|
|
4
33
|
|
|
5
34
|
首个公开版本。
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# dsh-session-mng
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-session-mng)
|
|
4
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
**One Settings page for every DeepSeek Harness session — generate a title from the whole conversation, archive or restore, move a cold session to another workspace, and permanently delete archived sessions.**
|
|
4
8
|
|
|
5
9
|
一个设置页管理全部 DSH 会话:按整段对话生成标题、归档/恢复、跨工作区移动、彻底删除已归档会话。Host + Web Client 双半插件,无运行时依赖,不注册模型可见工具,不注册标题 provider。
|
|
@@ -11,6 +15,10 @@
|
|
|
11
15
|
## 功能
|
|
12
16
|
|
|
13
17
|
- **统一会话总览**:按工作区分组列出当前 profile 的全部会话(活跃 + 已归档)。显示标题、路径、会话 id、大小、最后活动时间、运行/已打开/已归档/已置顶/子会话标记;支持搜索、工作区筛选、排序、刷新、批量选择。
|
|
18
|
+
- **工作区优先 + 分页**:会话数 >100 时默认折叠工作区;每个工作区默认 50 条,「加载更多」继续展开;搜索自动展开命中工作区。过滤工具栏和批量条整体吸顶。
|
|
19
|
+
- **选择跟随可见集**:切换筛选/折叠/搜索/刷新后,隐藏会话自动取消选择;`全选当前列表` 只作用于当前筛选与已展开的工作区。
|
|
20
|
+
- **空会话默认隐藏**:侧栏本来就不显示的 `blank` 会话,会话管理默认也不显示;勾选「显示空会话」才出现。提供「清理空会话(N)」快速清理已归档空会话。
|
|
21
|
+
- **删除范围模态确认**:范围内容在弹窗内独立滚动,底部勾选与确认/取消按钮固定可见;支持遮罩点击/Esc 取消。
|
|
14
22
|
- **生成标题**:选中任意会话(**包括已归档的冷会话**),让模型总结整段对话,给出可编辑的标题提案;**确认后才写入**。活跃会话走官方 `sessionTitle.rename`,冷会话按官方 `session/title` 事件形状追加,不需要恢复会话。
|
|
15
23
|
- **归档 / 恢复**:调用官方 `workspaceRegistry.archiveSession` / `unarchiveSession`。运行中的会话先提示,用户二次确认后才停止并归档;恢复后回到原工作区原位置。
|
|
16
24
|
- **跨工作区移动**:把冷会话真正迁移到目标工作区——改写会话日志 header 的 `cwd`、把日志目录迁到目标项目目录、同步 workspace 账本。SessionId、归档状态、pin 状态不变,事件字节不变。
|
|
@@ -19,6 +27,8 @@
|
|
|
19
27
|
- **失效归档标记清理**:归档集合里有、但磁盘上已无日志的 id,会以横幅提示并允许清理。
|
|
20
28
|
- **中英双语**、浅色/深色主题跟随宿主。
|
|
21
29
|
|
|
30
|
+

|
|
31
|
+
|
|
22
32
|

|
|
23
33
|
|
|
24
34
|

|
|
@@ -46,6 +56,8 @@ dsh plugin --profile <profile> add link:/绝对路径/dsh-session-mng
|
|
|
46
56
|
|
|
47
57
|
Host 半是进程内模块:装完如果看不到页面,重启一次 dsh/应用再刷新页面;Client 半刷新页面即可。
|
|
48
58
|
|
|
59
|
+
> 市场状态:awesome-dsh-plugin 收录 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 的 `check` 已通过;`Submission gate` 目前只差仓库满 1 天(约 2026-10-28 00:44 Asia/Shanghai 自动重跑),无需重新提交。合并后 dshmarket 读取同一份目录,卡片版本随 npm 显示 `0.1.1`。
|
|
60
|
+
|
|
49
61
|
## 使用
|
|
50
62
|
|
|
51
63
|
1. 打开 **设置 → 会话管理**。
|
|
@@ -84,6 +96,7 @@ Host route (lib/api.js) ──► service (lib/service.js)
|
|
|
84
96
|
| 会话后端 | 移动/删除只支持本地 JSONL 后端(`sessionPersistence.locate`);其他后端仍可用总览/标题/归档/恢复 |
|
|
85
97
|
| 官方依赖 | 无 runtime 依赖;只使用 `ctx` 服务 |
|
|
86
98
|
| 已知限制 | 已打开(live)会话不能移动或彻底删除,重启 DSH 后重试;有子代理后代的会话不能单独移动;子会话不出现在主列表(可勾选「显示子会话」查看) |
|
|
99
|
+
| 规模限制 | 0.1.1 的折叠/分页只在客户端压 DOM,`state` 仍一次拉全量(202 条约 103 KB;10000 条约 5 MB)。Host 侧 overview + paged sessions 是 0.2.0 的主任务 |
|
|
87
100
|
|
|
88
101
|
### 版本敏感点(请读)
|
|
89
102
|
|
|
@@ -126,12 +139,23 @@ node --check index.js client.js lib/*.js
|
|
|
126
139
|
npm --cache /tmp/dsh-npm-cache pack --dry-run
|
|
127
140
|
```
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
实机验收:安装到 `desktop` profile → 确认 `settings.section` 出现 `session-mng-section` → 用**一次性测试会话**验证标题/归档/恢复/移动/删除 → 清理测试数据。0.1.1 的列表验收见 `docs/RELEASE-CHECKLIST-0.1.1.md`。
|
|
143
|
+
|
|
144
|
+
## 当前状态与下一步
|
|
145
|
+
|
|
146
|
+
- `0.1.1` 已发布到 npm / GitHub / Release;26 个测试全绿,31 个文件。
|
|
147
|
+
- `0.1.2`(空会话清理 + 删除范围弹窗)已在工作区实现,待实机测试/发布。
|
|
148
|
+
- 市场 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 等 1 天仓库年龄 gate,之后等维护者合并。
|
|
149
|
+
- 下一项主任务:**0.2.0 Host 侧 overview + 分页 sessions**,为 1000–10000 条会话设计;详细任务、验收标准、环境坑见 [接手说明](docs/HANDOVER-2026-10-27.md)。
|
|
130
150
|
|
|
131
151
|
## 文档索引
|
|
132
152
|
|
|
153
|
+
- [接手说明(下一步任务)](docs/HANDOVER-2026-10-27.md)
|
|
133
154
|
- [实施计划(批准时原样归档)](docs/PLAN-2026-10-26.md)
|
|
134
155
|
- [0.1.0 发布检查清单](docs/RELEASE-CHECKLIST-0.1.0.md)
|
|
156
|
+
- [0.1.1 实施计划](docs/PLAN-2026-10-27.md)
|
|
157
|
+
- [0.1.1 发布检查清单](docs/RELEASE-CHECKLIST-0.1.1.md)
|
|
158
|
+
- [0.1.2 发布检查清单(待发布)](docs/RELEASE-CHECKLIST-0.1.2.md)
|
|
135
159
|
- [设计与实现说明](docs/01-design.md)
|
|
136
160
|
- [开源、发 npm 与市场收录记录](docs/02-open-source-and-marketplace.md)
|
|
137
161
|
- [路线图](ROADMAP.md)
|
package/ROADMAP.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Roadmap
|
|
2
2
|
|
|
3
|
+
> 当前:`0.1.1` 已发布(npm latest / tag `v0.1.1` / Release)。市场 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 的 `check` 已过,只差仓库 1 天年龄 gate(约 2026-10-28 00:44 Asia/Shanghai 自动重跑)。下一项主任务是 0.2.0 的 Host 分页;详细接手任务见 [docs/HANDOVER-2026-10-27.md](docs/HANDOVER-2026-10-27.md)。
|
|
4
|
+
|
|
3
5
|
## 0.1.0 — delivered
|
|
4
6
|
|
|
5
7
|
- 统一会话总览(活跃 + 已归档,工作区分组,搜索/筛选/排序,日志大小)。
|
|
@@ -9,7 +11,33 @@
|
|
|
9
11
|
- 彻底删除已归档会话(范围预览 + 子代理级联 + 事务 + 账本清理)。
|
|
10
12
|
- 最近消息预览、失效归档标记清理、中英双语。
|
|
11
13
|
|
|
12
|
-
## 0.
|
|
14
|
+
## 0.1.1 — delivered
|
|
15
|
+
|
|
16
|
+
- 工作区优先:会话数 >100 默认折叠,点击展开;每个工作区 50 条分页 + 加载更多。
|
|
17
|
+
- 搜索命中自动展开,搜索期间禁止折叠;展开全部 / 收起全部。
|
|
18
|
+
- 过滤工具栏 + 批量条合并为 sticky 头部。
|
|
19
|
+
- 选择集裁剪到当前筛选与展开可见集;批量操作不触碰隐藏会话。
|
|
20
|
+
- 批量移动提示区分 live 与一般不可移动项。
|
|
21
|
+
|
|
22
|
+
## 0.1.2 — in progress
|
|
23
|
+
|
|
24
|
+
- 空会话默认隐藏 + 「显示空会话」开关;tab 计数与可见列表一致。
|
|
25
|
+
- 「清理空会话(N)」批量清理已归档空会话,复用删除范围预览/确认。
|
|
26
|
+
- 删除范围确认改为模态框,内容独立滚动、底部确认区固定,支持遮罩/Esc 取消。
|
|
27
|
+
|
|
28
|
+
## 0.2.0 — next (planned)
|
|
29
|
+
|
|
30
|
+
首要目标:真正支撑 1000–10000 条会话,不再首屏拉全量 `state`。实施前先写 `docs/PLAN-2026-10-28.md` 并获批准。
|
|
31
|
+
|
|
32
|
+
- `GET /session-mng/api/overview`:只返回工作区计数/大小/归档数、totals、capabilities、orphans;不读全部标题/日志。
|
|
33
|
+
- `GET /session-mng/api/sessions`:按 `workspaceId/tab/offset/limit/query/sort` 分页;只对当前页 id 读标题/大小;返回 `hasMore`。
|
|
34
|
+
- 未分组会话懒加载 + 短 TTL 缓存;全局搜索先只读验证宿主 search 是否覆盖标题。
|
|
35
|
+
- Client:overview 首屏 → 工作区展开时取第一页 → 加载更多;保留 sticky、选择裁剪、搜索自动展开。
|
|
36
|
+
- 兼容:保留 `/state` 给 0.1.x 客户端;Host 同时提供新旧接口。
|
|
37
|
+
- 验收:合成 10k 数据首屏不返回 session rows;单页 ≤50 行;不触发全量 `sessionController.list`;现有 26 测试 + 新增分页/overview/search 测试。
|
|
38
|
+
- 详细接口建议、验收与坑:[docs/HANDOVER-2026-10-27.md](docs/HANDOVER-2026-10-27.md)。
|
|
39
|
+
|
|
40
|
+
## Later candidates
|
|
13
41
|
|
|
14
42
|
- 侧栏会话「...」菜单快捷入口(生成标题 / 会话管理详情),复用同一套 Host 操作。
|
|
15
43
|
- 内容全文搜索(复用 `sessionQuery.searchSessions` / `searchEvents`),不只按标题/路径/id。
|
|
Binary file
|
|
Binary file
|
package/client.js
CHANGED
|
@@ -20,6 +20,8 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
const NS = 'dsh-session-mng'
|
|
21
21
|
const API = '/session-mng/api'
|
|
22
22
|
const STYLE_ID = 'dsh-session-mng-style'
|
|
23
|
+
const GROUP_PAGE_SIZE = 50
|
|
24
|
+
const DEFAULT_COLLAPSE_AFTER = 100
|
|
23
25
|
|
|
24
26
|
const DICT = {
|
|
25
27
|
zh: {
|
|
@@ -42,7 +44,17 @@ window.__ModuleLoader__.load({
|
|
|
42
44
|
sortSize: '大小',
|
|
43
45
|
sortTitle: '标题',
|
|
44
46
|
showSubagents: '显示子会话',
|
|
47
|
+
showBlank: '显示空会话',
|
|
48
|
+
cleanupBlank: '清理空会话({n})',
|
|
49
|
+
cleanupBlankHint: '批量删除已归档的空会话;未归档的空会话需先归档。',
|
|
50
|
+
blankActiveHint: '另有 {n} 个未归档空会话,归档后才能清理。',
|
|
51
|
+
hiddenBlankCount: '已隐藏 {n} 个空会话',
|
|
45
52
|
selectAll: '全选当前列表',
|
|
53
|
+
expandAll: '展开全部',
|
|
54
|
+
collapseAll: '收起全部',
|
|
55
|
+
loadMore: '加载更多(剩余 {n} 条)',
|
|
56
|
+
selectionScopeHint: '选择仅作用于当前筛选与展开结果。',
|
|
57
|
+
searchAutoExpand: '搜索时自动展开命中工作区。',
|
|
46
58
|
selectRow: '选择会话 {name}',
|
|
47
59
|
justNow: '刚刚',
|
|
48
60
|
titleApplied: '标题已写入。',
|
|
@@ -152,6 +164,7 @@ window.__ModuleLoader__.load({
|
|
|
152
164
|
timeout: '模型调用超时。',
|
|
153
165
|
llmError: '模型调用失败:{message}',
|
|
154
166
|
liveHint: '已打开的会话不能移动或彻底删除;重启 DSH 后重试。',
|
|
167
|
+
moveUnavailableHint: '所选会话中存在不可移动项(如跨工作区、有子会话或状态未就绪)。',
|
|
155
168
|
archiveHint: '归档后会话会从活跃列表移到已归档,可在本页恢复。',
|
|
156
169
|
moveArchivedHint: '移动不会改变归档状态,会话仍会留在已归档。',
|
|
157
170
|
working: '处理中…',
|
|
@@ -184,7 +197,17 @@ window.__ModuleLoader__.load({
|
|
|
184
197
|
sortSize: 'Size',
|
|
185
198
|
sortTitle: 'Title',
|
|
186
199
|
showSubagents: 'Show subagent sessions',
|
|
200
|
+
showBlank: 'Show empty sessions',
|
|
201
|
+
cleanupBlank: 'Clean up empty sessions ({n})',
|
|
202
|
+
cleanupBlankHint: 'Permanently delete archived empty sessions; unarchived empty sessions must be archived first.',
|
|
203
|
+
blankActiveHint: '{n} unarchived empty sessions must be archived before cleanup.',
|
|
204
|
+
hiddenBlankCount: '{n} empty sessions hidden',
|
|
187
205
|
selectAll: 'Select all shown',
|
|
206
|
+
expandAll: 'Expand all',
|
|
207
|
+
collapseAll: 'Collapse all',
|
|
208
|
+
loadMore: 'Load {n} more',
|
|
209
|
+
selectionScopeHint: 'Selection applies to the current filter and expanded workspaces.',
|
|
210
|
+
searchAutoExpand: 'Matching workspaces expand automatically while searching.',
|
|
188
211
|
selectRow: 'Select session {name}',
|
|
189
212
|
justNow: 'just now',
|
|
190
213
|
titleApplied: 'Title applied.',
|
|
@@ -294,6 +317,7 @@ window.__ModuleLoader__.load({
|
|
|
294
317
|
timeout: 'The model call timed out.',
|
|
295
318
|
llmError: 'The model call failed: {message}',
|
|
296
319
|
liveHint: 'An open session cannot be moved or deleted; restart DSH and retry.',
|
|
320
|
+
moveUnavailableHint: 'Some selected sessions cannot be moved (cross-workspace, subagent descendants, or not ready).',
|
|
297
321
|
archiveHint: 'Archiving moves the session from the active list to the archived list; restore it here.',
|
|
298
322
|
moveArchivedHint: 'Moving does not change the archive state; the session stays archived.',
|
|
299
323
|
working: 'Working…',
|
|
@@ -341,11 +365,33 @@ window.__ModuleLoader__.load({
|
|
|
341
365
|
.dsm-note.warn{border-color:var(--dsw-alias-state-warn-primary)}
|
|
342
366
|
.dsm-note-actions{display:flex;gap:6px;flex-wrap:wrap;flex:0 0 auto}
|
|
343
367
|
.dsm-note detail{flex:1 1 100%}
|
|
368
|
+
.dsm-sticky{position:sticky;top:0;z-index:3;display:flex;flex-direction:column;gap:8px;
|
|
369
|
+
padding:6px 0 8px;background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
344
370
|
.dsm-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;border-radius:7px;
|
|
345
|
-
border:1px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)
|
|
346
|
-
|
|
371
|
+
border:1px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}
|
|
372
|
+
.dsm-list-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:0 2px}
|
|
347
373
|
.dsm-group{border:1px solid var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;background:var(--dsw-alias-bg-layer-1)}
|
|
348
|
-
.dsm-group-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;
|
|
374
|
+
.dsm-group-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;width:100%;
|
|
375
|
+
background:var(--dsw-alias-bg-layer-2);border:none;font:inherit;color:inherit;text-align:left;cursor:pointer}
|
|
376
|
+
.dsm-group-head:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
|
|
377
|
+
.dsm-group-head:disabled{cursor:default;opacity:.85}
|
|
378
|
+
.dsm-chev{flex:0 0 18px;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;
|
|
379
|
+
color:var(--dsw-alias-label-secondary)}
|
|
380
|
+
.dsm-row input[type="checkbox"],.dsm-check input[type="checkbox"]{
|
|
381
|
+
appearance:none;-webkit-appearance:none;width:18px;height:18px;margin:0;flex:0 0 auto;
|
|
382
|
+
border:1.5px solid var(--dsw-alias-border-l2);border-radius:4px;background:transparent;
|
|
383
|
+
display:inline-grid;place-content:center;cursor:pointer;transition:background .12s,border-color .12s}
|
|
384
|
+
.dsm-row input[type="checkbox"]::before,.dsm-check input[type="checkbox"]::before{
|
|
385
|
+
content:'';width:9px;height:5px;border-left:2px solid #fff;border-bottom:2px solid #fff;
|
|
386
|
+
transform:rotate(-45deg) scale(0);transition:transform .12s}
|
|
387
|
+
.dsm-row input[type="checkbox"]:checked,.dsm-check input[type="checkbox"]:checked{
|
|
388
|
+
background:#0f766e;border-color:#0f766e}
|
|
389
|
+
.dsm-row input[type="checkbox"]:checked::before,.dsm-check input[type="checkbox"]:checked::before{
|
|
390
|
+
transform:rotate(-45deg) scale(1)}
|
|
391
|
+
.dsm-row input[type="checkbox"]:focus-visible,.dsm-check input[type="checkbox"]:focus-visible{
|
|
392
|
+
outline:2px solid #14b8a6;outline-offset:2px}
|
|
393
|
+
.dsm-more{width:100%;justify-content:center;border-radius:0;border-left:none;border-right:none;
|
|
394
|
+
border-bottom:none;border-top:1px solid var(--dsw-alias-border-l1);min-height:30px}
|
|
349
395
|
.dsm-group-title{font-size:12px;font-weight:600;line-height:18px}
|
|
350
396
|
.dsm-group-path{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);word-break:break-all}
|
|
351
397
|
.dsm-group-meta{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
@@ -394,6 +440,22 @@ window.__ModuleLoader__.load({
|
|
|
394
440
|
.dsm-scope-row .dsm-mono{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
395
441
|
.dsm-danger-box{border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-1)}
|
|
396
442
|
.dsm-danger-box h3{color:var(--dsw-alias-state-error-primary)}
|
|
443
|
+
.dsm-scope-overlay{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;
|
|
444
|
+
padding:20px;background:rgba(0,0,0,.58)}
|
|
445
|
+
.dsm-scope-dialog{display:flex;flex-direction:column;width:min(660px,100%);max-height:min(82vh,760px);
|
|
446
|
+
border:1px solid var(--dsw-alias-state-error-primary);border-radius:10px;overflow:hidden;
|
|
447
|
+
background:var(--dsw-alias-bg-layer-2);box-shadow:0 24px 70px rgba(0,0,0,.5)}
|
|
448
|
+
.dsm-scope-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
|
|
449
|
+
padding:11px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
450
|
+
.dsm-scope-head h3{margin:0;font-size:13px;line-height:18px;color:var(--dsw-alias-state-error-primary)}
|
|
451
|
+
.dsm-scope-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 14px}
|
|
452
|
+
.dsm-scope-foot{display:flex;flex-direction:column;gap:8px;padding:10px 14px;
|
|
453
|
+
border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
|
|
454
|
+
.dsm-scope-dialog .dsm-scope-list{max-height:none;overflow:visible;margin:8px 0}
|
|
455
|
+
@media (max-width:640px){
|
|
456
|
+
.dsm-scope-overlay{padding:10px}
|
|
457
|
+
.dsm-scope-dialog{max-height:92vh}
|
|
458
|
+
}
|
|
397
459
|
.dsm-footer{font-size:11px;line-height:17px;color:var(--dsw-alias-label-secondary)}
|
|
398
460
|
.dsm-spin{display:inline-block;width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);
|
|
399
461
|
border-top-color:var(--dsw-alias-brand-primary);border-radius:50%;animation:dsm-spin .8s linear infinite;vertical-align:-2px}
|
|
@@ -634,14 +696,34 @@ window.__ModuleLoader__.load({
|
|
|
634
696
|
)
|
|
635
697
|
}
|
|
636
698
|
|
|
637
|
-
function Group({ group, selected, onToggle, onOpen, t }) {
|
|
699
|
+
function Group({ group, selected, onToggle, onSelect, onOpen, onMore, t, locked }) {
|
|
700
|
+
const remaining = group.rows.length - group.renderedRows.length
|
|
638
701
|
return h(
|
|
639
702
|
'section',
|
|
640
703
|
{ className: 'dsm-group' },
|
|
641
704
|
h(
|
|
642
|
-
'
|
|
643
|
-
{
|
|
644
|
-
|
|
705
|
+
'button',
|
|
706
|
+
{
|
|
707
|
+
type: 'button',
|
|
708
|
+
className: 'dsm-group-head',
|
|
709
|
+
'aria-expanded': group.expanded === true ? 'true' : 'false',
|
|
710
|
+
disabled: locked === true,
|
|
711
|
+
title: locked === true ? t('searchAutoExpand') : undefined,
|
|
712
|
+
onClick: onToggle,
|
|
713
|
+
},
|
|
714
|
+
h('span', { className: 'dsm-chev', 'aria-hidden': 'true' },
|
|
715
|
+
h('svg', {
|
|
716
|
+
viewBox: '0 0 16 16',
|
|
717
|
+
width: 16,
|
|
718
|
+
height: 16,
|
|
719
|
+
fill: 'none',
|
|
720
|
+
stroke: 'currentColor',
|
|
721
|
+
'stroke-width': 1.6,
|
|
722
|
+
'stroke-linecap': 'round',
|
|
723
|
+
'stroke-linejoin': 'round',
|
|
724
|
+
}, h('path', { d: group.expanded === true ? 'M4 6l4 4 4-4' : 'M6 4l4 4-4 4' })),
|
|
725
|
+
),
|
|
726
|
+
h('div', { style: { minWidth: 0, flex: '1 1 auto' } },
|
|
645
727
|
h('div', { className: 'dsm-group-title' }, group.title || t('workspaceUngrouped')),
|
|
646
728
|
group.path ? h('div', { className: 'dsm-group-path' }, group.path) : null,
|
|
647
729
|
group.status !== undefined && group.status !== 'ok'
|
|
@@ -650,16 +732,23 @@ window.__ModuleLoader__.load({
|
|
|
650
732
|
),
|
|
651
733
|
h('div', { className: 'dsm-group-meta' }, t('groupLine', { n: group.rows.length, size: formatBytes(group.bytes) })),
|
|
652
734
|
),
|
|
653
|
-
group.
|
|
735
|
+
group.renderedRows.map((row) =>
|
|
654
736
|
h(Row, {
|
|
655
737
|
key: row.id,
|
|
656
738
|
row,
|
|
657
739
|
selected: selected.has(row.id),
|
|
658
|
-
onToggle: () =>
|
|
740
|
+
onToggle: () => onSelect(row.id),
|
|
659
741
|
onOpen: () => onOpen(row.id),
|
|
660
742
|
t,
|
|
661
743
|
}),
|
|
662
744
|
),
|
|
745
|
+
group.expanded === true && remaining > 0
|
|
746
|
+
? h(
|
|
747
|
+
'button',
|
|
748
|
+
{ type: 'button', className: 'dsm-btn ghost dsm-more', onClick: onMore },
|
|
749
|
+
t('loadMore', { n: remaining }),
|
|
750
|
+
)
|
|
751
|
+
: null,
|
|
663
752
|
)
|
|
664
753
|
}
|
|
665
754
|
|
|
@@ -856,6 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
856
945
|
React.useEffect(() => {
|
|
857
946
|
const controller = new AbortController()
|
|
858
947
|
setPhase('loading')
|
|
948
|
+
setChecked(false)
|
|
859
949
|
post('delete/preview', { sessionIds: ids }, controller.signal)
|
|
860
950
|
.then((value) => {
|
|
861
951
|
if (controller.signal.aborted) return
|
|
@@ -870,43 +960,86 @@ window.__ModuleLoader__.load({
|
|
|
870
960
|
return () => controller.abort()
|
|
871
961
|
}, [idsKey, t])
|
|
872
962
|
|
|
963
|
+
React.useEffect(() => {
|
|
964
|
+
const onKeyDown = (event) => {
|
|
965
|
+
if (event.key !== 'Escape') return
|
|
966
|
+
event.preventDefault()
|
|
967
|
+
event.stopPropagation()
|
|
968
|
+
if (busy !== true) onCancel()
|
|
969
|
+
}
|
|
970
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
971
|
+
return () => document.removeEventListener('keydown', onKeyDown, true)
|
|
972
|
+
}, [busy, onCancel])
|
|
973
|
+
|
|
873
974
|
const rows = preview?.sessions ?? []
|
|
874
975
|
const blocked = rows.some((row) => row.live || row.running)
|
|
976
|
+
const ready = phase === 'ready' && preview !== null
|
|
875
977
|
return h(
|
|
876
978
|
'div',
|
|
877
|
-
{
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
979
|
+
{
|
|
980
|
+
className: 'dsm-scope-overlay',
|
|
981
|
+
onMouseDown: (event) => {
|
|
982
|
+
if (event.target === event.currentTarget && busy !== true) onCancel()
|
|
983
|
+
},
|
|
984
|
+
},
|
|
985
|
+
h(
|
|
986
|
+
'div',
|
|
987
|
+
{
|
|
988
|
+
className: 'dsm-scope-dialog',
|
|
989
|
+
role: 'dialog',
|
|
990
|
+
'aria-modal': 'true',
|
|
991
|
+
'aria-label': t('deleteScope'),
|
|
992
|
+
},
|
|
993
|
+
h(
|
|
994
|
+
'div',
|
|
995
|
+
{ className: 'dsm-scope-head' },
|
|
996
|
+
h('h3', null, t('deleteScope')),
|
|
997
|
+
h('button', {
|
|
998
|
+
type: 'button',
|
|
999
|
+
className: 'dsm-btn ghost',
|
|
1000
|
+
'aria-label': t('close'),
|
|
1001
|
+
disabled: busy,
|
|
1002
|
+
onClick: onCancel,
|
|
1003
|
+
}, '×'),
|
|
1004
|
+
),
|
|
1005
|
+
h(
|
|
1006
|
+
'div',
|
|
1007
|
+
{ className: 'dsm-scope-body' },
|
|
1008
|
+
h('p', { className: 'dsm-hint', style: { margin: '0 0 8px' } }, t('deleteHint')),
|
|
1009
|
+
phase === 'loading'
|
|
1010
|
+
? h('p', { className: 'dsm-hint' }, t('deletePreviewLoading'))
|
|
1011
|
+
: phase === 'error'
|
|
1012
|
+
? h('p', { className: 'dsm-error' }, error)
|
|
1013
|
+
: h(
|
|
1014
|
+
React.Fragment,
|
|
1015
|
+
null,
|
|
1016
|
+
h('div', { className: 'dsm-hint' },
|
|
1017
|
+
`${t('deleteRoots')}: ${preview?.roots?.length ?? 0} · ${t('deleteDescendants')}: ${preview?.descendantCount ?? 0} · ${t('deleteTotalSize', { size: formatBytes(preview?.totalBytes ?? 0) })}`,
|
|
902
1018
|
),
|
|
1019
|
+
h('div', { className: 'dsm-scope-list' },
|
|
1020
|
+
rows.map((row) =>
|
|
1021
|
+
h(
|
|
1022
|
+
'div',
|
|
1023
|
+
{ key: row.id, className: 'dsm-scope-row' },
|
|
1024
|
+
h('span', { className: 'dsm-badge' }, row.descendant ? t('scopeChild') : t('scopeRoot')),
|
|
1025
|
+
h('span', { className: 'dsm-mono', title: row.dir ?? row.id }, row.id),
|
|
1026
|
+
row.live ? h(Badge, { tone: 'brand' }, t('liveBadge')) : null,
|
|
1027
|
+
row.running ? h(Badge, { tone: 'run' }, t('running')) : null,
|
|
1028
|
+
row.missing ? h(Badge, { tone: 'warn' }, t('missingDir')) : null,
|
|
1029
|
+
row.readable === false ? h(Badge, { tone: 'warn' }, t('unreadable')) : null,
|
|
1030
|
+
h('span', { style: { marginLeft: 'auto' } }, formatBytes(row.bytes)),
|
|
1031
|
+
),
|
|
1032
|
+
),
|
|
1033
|
+
),
|
|
1034
|
+
h('p', { className: 'dsm-hint', style: { margin: '0 0 6px' } }, t('deleteCascadeNote')),
|
|
1035
|
+
blocked ? h('p', { className: 'dsm-error' }, t('deleteLiveHint')) : null,
|
|
903
1036
|
),
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1037
|
+
),
|
|
1038
|
+
h(
|
|
1039
|
+
'div',
|
|
1040
|
+
{ className: 'dsm-scope-foot' },
|
|
1041
|
+
ready
|
|
1042
|
+
? h('label', { className: 'dsm-check' },
|
|
910
1043
|
h('input', {
|
|
911
1044
|
type: 'checkbox',
|
|
912
1045
|
checked,
|
|
@@ -914,20 +1047,22 @@ window.__ModuleLoader__.load({
|
|
|
914
1047
|
onChange: (event) => setChecked(event.target.checked),
|
|
915
1048
|
}),
|
|
916
1049
|
t('deleteConfirmCheck'),
|
|
917
|
-
)
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1050
|
+
)
|
|
1051
|
+
: null,
|
|
1052
|
+
h(
|
|
1053
|
+
'div',
|
|
1054
|
+
{ className: 'dsm-actions' },
|
|
1055
|
+
h('button', {
|
|
1056
|
+
type: 'button',
|
|
1057
|
+
className: 'dsm-btn danger',
|
|
1058
|
+
disabled: busy || ready !== true || blocked || checked !== true,
|
|
1059
|
+
onClick: () => onConfirmed(ids),
|
|
1060
|
+
}, busy ? t('deleting') : t('deleteConfirm')),
|
|
1061
|
+
h('button', { type: 'button', className: 'dsm-btn ghost', disabled: busy, onClick: onCancel }, t('cancel')),
|
|
1062
|
+
blocked ? h('span', { className: 'dsm-hint' }, t('liveHint')) : null,
|
|
1063
|
+
),
|
|
1064
|
+
),
|
|
1065
|
+
),
|
|
931
1066
|
)
|
|
932
1067
|
}
|
|
933
1068
|
|
|
@@ -1086,6 +1221,72 @@ window.__ModuleLoader__.load({
|
|
|
1086
1221
|
// Page
|
|
1087
1222
|
// ---------------------------------------------------------------------
|
|
1088
1223
|
|
|
1224
|
+
/**
|
|
1225
|
+
* Derive the filtered / grouped / rendered view for the list.
|
|
1226
|
+
* Pure so the selection-pruning effect can use it before the loading guard.
|
|
1227
|
+
*/
|
|
1228
|
+
function computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank }) {
|
|
1229
|
+
const rows = state?.sessions ?? []
|
|
1230
|
+
const workspaceOrder = state?.workspaces ?? []
|
|
1231
|
+
const text = query.trim().toLowerCase()
|
|
1232
|
+
const filtering = text !== ''
|
|
1233
|
+
const filtered = rows.filter((row) => {
|
|
1234
|
+
if (tab === 'active' && row.archived) return false
|
|
1235
|
+
if (tab === 'archived' && !row.archived) return false
|
|
1236
|
+
if (workspaceFilter === 'ungrouped' && row.workspaceId !== null) return false
|
|
1237
|
+
if (workspaceFilter !== 'all' && workspaceFilter !== 'ungrouped' && row.workspaceId !== workspaceFilter) return false
|
|
1238
|
+
if (showBlank !== true && row.blank === true) return false
|
|
1239
|
+
if (filtering) {
|
|
1240
|
+
const haystack = `${row.title}\n${row.cwd}\n${row.id}`.toLowerCase()
|
|
1241
|
+
if (!haystack.includes(text)) return false
|
|
1242
|
+
}
|
|
1243
|
+
return true
|
|
1244
|
+
})
|
|
1245
|
+
const sorted = [...filtered].sort((left, right) => {
|
|
1246
|
+
if (sort === 'created') return right.createdAt - left.createdAt
|
|
1247
|
+
if (sort === 'size') return right.sizeBytes - left.sizeBytes
|
|
1248
|
+
if (sort === 'title') return (left.title || left.cwd).localeCompare(right.title || right.cwd)
|
|
1249
|
+
return right.updatedAt - left.updatedAt
|
|
1250
|
+
})
|
|
1251
|
+
const baseGroups = []
|
|
1252
|
+
for (const workspace of workspaceOrder) {
|
|
1253
|
+
const groupRows = sorted.filter((row) => row.workspaceId === workspace.id)
|
|
1254
|
+
if (groupRows.length === 0) continue
|
|
1255
|
+
baseGroups.push({
|
|
1256
|
+
...workspace,
|
|
1257
|
+
rows: groupRows,
|
|
1258
|
+
bytes: groupRows.reduce((sum, row) => sum + row.sizeBytes, 0),
|
|
1259
|
+
})
|
|
1260
|
+
}
|
|
1261
|
+
const ungroupedRows = sorted.filter((row) => row.workspaceId === null)
|
|
1262
|
+
if (ungroupedRows.length > 0) {
|
|
1263
|
+
baseGroups.push({
|
|
1264
|
+
id: 'ungrouped',
|
|
1265
|
+
title: '',
|
|
1266
|
+
path: '',
|
|
1267
|
+
rows: ungroupedRows,
|
|
1268
|
+
bytes: ungroupedRows.reduce((sum, row) => sum + row.sizeBytes, 0),
|
|
1269
|
+
})
|
|
1270
|
+
}
|
|
1271
|
+
const groups = baseGroups.map((group) => {
|
|
1272
|
+
const isExpanded = filtering ? true : (expanded === null ? defaultExpanded : expanded.has(group.id))
|
|
1273
|
+
const limit = limits[group.id] ?? GROUP_PAGE_SIZE
|
|
1274
|
+
return {
|
|
1275
|
+
...group,
|
|
1276
|
+
expanded: isExpanded,
|
|
1277
|
+
limit,
|
|
1278
|
+
renderedRows: isExpanded ? group.rows.slice(0, limit) : [],
|
|
1279
|
+
}
|
|
1280
|
+
})
|
|
1281
|
+
const visibleRows = groups.flatMap((group) => (group.expanded ? group.rows : []))
|
|
1282
|
+
return {
|
|
1283
|
+
filteredRows: sorted,
|
|
1284
|
+
groups,
|
|
1285
|
+
visibleRows,
|
|
1286
|
+
visibleIdSet: new Set(visibleRows.map((row) => row.id)),
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1089
1290
|
function SessionManagerPage(props) {
|
|
1090
1291
|
const { ctx, t } = props
|
|
1091
1292
|
useLocaleTick(ctx)
|
|
@@ -1103,6 +1304,9 @@ window.__ModuleLoader__.load({
|
|
|
1103
1304
|
const [notice, setNotice] = React.useState(null)
|
|
1104
1305
|
const [bulkDeleteIds, setBulkDeleteIds] = React.useState(null)
|
|
1105
1306
|
const [bulkTarget, setBulkTarget] = React.useState('')
|
|
1307
|
+
const [expanded, setExpanded] = React.useState(null)
|
|
1308
|
+
const [limits, setLimits] = React.useState({})
|
|
1309
|
+
const [showBlank, setShowBlank] = React.useState(false)
|
|
1106
1310
|
const abortRef = React.useRef(null)
|
|
1107
1311
|
const searchRef = React.useRef(null)
|
|
1108
1312
|
const listScroll = React.useRef(0)
|
|
@@ -1168,6 +1372,7 @@ window.__ModuleLoader__.load({
|
|
|
1168
1372
|
if (selectedId === null) return undefined
|
|
1169
1373
|
const onKeyDown = (event) => {
|
|
1170
1374
|
if (event.key === 'Escape') {
|
|
1375
|
+
if (document.querySelector('.dsm-scope-overlay') !== null) return
|
|
1171
1376
|
event.stopPropagation()
|
|
1172
1377
|
backToList()
|
|
1173
1378
|
}
|
|
@@ -1176,6 +1381,38 @@ window.__ModuleLoader__.load({
|
|
|
1176
1381
|
return () => document.removeEventListener('keydown', onKeyDown, true)
|
|
1177
1382
|
}, [selectedId])
|
|
1178
1383
|
|
|
1384
|
+
React.useEffect(() => {
|
|
1385
|
+
setLimits({})
|
|
1386
|
+
}, [tab, workspaceFilter, query, includeSubagents, showBlank])
|
|
1387
|
+
|
|
1388
|
+
const rows = state?.sessions ?? []
|
|
1389
|
+
const defaultExpanded = (state?.totals?.all ?? 0) <= DEFAULT_COLLAPSE_AFTER
|
|
1390
|
+
const view = React.useMemo(
|
|
1391
|
+
() => computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank }),
|
|
1392
|
+
[state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits, showBlank],
|
|
1393
|
+
)
|
|
1394
|
+
const viewKey = React.useMemo(
|
|
1395
|
+
() => [
|
|
1396
|
+
state?.generatedAt ?? 0,
|
|
1397
|
+
tab,
|
|
1398
|
+
workspaceFilter,
|
|
1399
|
+
query,
|
|
1400
|
+
includeSubagents ? 1 : 0,
|
|
1401
|
+
showBlank ? 1 : 0,
|
|
1402
|
+
defaultExpanded ? 1 : 0,
|
|
1403
|
+
expanded === null ? 'auto' : [...expanded].sort().join(','),
|
|
1404
|
+
].join('|'),
|
|
1405
|
+
[state?.generatedAt, tab, workspaceFilter, query, includeSubagents, showBlank, defaultExpanded, expanded],
|
|
1406
|
+
)
|
|
1407
|
+
React.useEffect(() => {
|
|
1408
|
+
setSelected((previous) => {
|
|
1409
|
+
if (previous.size === 0) return previous
|
|
1410
|
+
const next = new Set()
|
|
1411
|
+
for (const id of previous) if (view.visibleIdSet.has(id)) next.add(id)
|
|
1412
|
+
return next.size === previous.size ? previous : next
|
|
1413
|
+
})
|
|
1414
|
+
}, [viewKey])
|
|
1415
|
+
|
|
1179
1416
|
const run = async (operation, successText) => {
|
|
1180
1417
|
setBusy(true)
|
|
1181
1418
|
setNotice(null)
|
|
@@ -1294,53 +1531,17 @@ window.__ModuleLoader__.load({
|
|
|
1294
1531
|
return h('div', { className: 'dsm-page' }, h('p', { className: 'dsm-hint' }, h(Spinner, { t, label: t('loading') })))
|
|
1295
1532
|
}
|
|
1296
1533
|
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1534
|
+
const rowsForTabs = rows.filter((row) => showBlank || row.blank !== true)
|
|
1535
|
+
const blankRows = rows.filter((row) => row.blank === true)
|
|
1536
|
+
const blankArchivedRows = blankRows.filter((row) => row.archived === true && row.can?.delete === true)
|
|
1537
|
+
const blankActiveCount = blankRows.filter((row) => row.archived !== true || row.can?.delete !== true).length
|
|
1299
1538
|
const tabs = [
|
|
1300
|
-
{ key: 'all', label: t('tabAll'), count:
|
|
1301
|
-
{ key: 'active', label: t('tabActive'), count:
|
|
1302
|
-
{ key: 'archived', label: t('tabArchived'), count:
|
|
1539
|
+
{ key: 'all', label: t('tabAll'), count: rowsForTabs.length },
|
|
1540
|
+
{ key: 'active', label: t('tabActive'), count: rowsForTabs.filter((row) => !row.archived).length },
|
|
1541
|
+
{ key: 'archived', label: t('tabArchived'), count: rowsForTabs.filter((row) => row.archived).length },
|
|
1303
1542
|
]
|
|
1304
|
-
const
|
|
1305
|
-
|
|
1306
|
-
if (tab === 'archived' && !row.archived) return false
|
|
1307
|
-
if (workspaceFilter === 'ungrouped' && row.workspaceId !== null) return false
|
|
1308
|
-
if (workspaceFilter !== 'all' && workspaceFilter !== 'ungrouped' && row.workspaceId !== workspaceFilter) return false
|
|
1309
|
-
const text = query.trim().toLowerCase()
|
|
1310
|
-
if (text !== '') {
|
|
1311
|
-
const haystack = `${row.title}\n${row.cwd}\n${row.id}`.toLowerCase()
|
|
1312
|
-
if (!haystack.includes(text)) return false
|
|
1313
|
-
}
|
|
1314
|
-
return true
|
|
1315
|
-
})
|
|
1316
|
-
const sorted = [...visible].sort((left, right) => {
|
|
1317
|
-
if (sort === 'created') return right.createdAt - left.createdAt
|
|
1318
|
-
if (sort === 'size') return right.sizeBytes - left.sizeBytes
|
|
1319
|
-
if (sort === 'title') return (left.title || left.cwd).localeCompare(right.title || right.cwd)
|
|
1320
|
-
return right.updatedAt - left.updatedAt
|
|
1321
|
-
})
|
|
1322
|
-
const groups = []
|
|
1323
|
-
for (const workspace of workspaceOrder) {
|
|
1324
|
-
const groupRows = sorted.filter((row) => row.workspaceId === workspace.id)
|
|
1325
|
-
if (groupRows.length === 0) continue
|
|
1326
|
-
groups.push({
|
|
1327
|
-
...workspace,
|
|
1328
|
-
rows: groupRows,
|
|
1329
|
-
bytes: groupRows.reduce((sum, row) => sum + row.sizeBytes, 0),
|
|
1330
|
-
})
|
|
1331
|
-
}
|
|
1332
|
-
const ungroupedRows = sorted.filter((row) => row.workspaceId === null)
|
|
1333
|
-
if (ungroupedRows.length > 0) {
|
|
1334
|
-
groups.push({
|
|
1335
|
-
id: 'ungrouped',
|
|
1336
|
-
title: t('workspaceUngrouped'),
|
|
1337
|
-
path: '',
|
|
1338
|
-
rows: ungroupedRows,
|
|
1339
|
-
bytes: ungroupedRows.reduce((sum, row) => sum + row.sizeBytes, 0),
|
|
1340
|
-
})
|
|
1341
|
-
}
|
|
1342
|
-
const visibleAllSelected = sorted.length > 0 && sorted.every((row) => selected.has(row.id))
|
|
1343
|
-
const selectedRows = rows.filter((row) => selected.has(row.id))
|
|
1543
|
+
const visibleAllSelected = view.visibleRows.length > 0 && view.visibleRows.every((row) => selected.has(row.id))
|
|
1544
|
+
const selectedRows = view.visibleRows.filter((row) => selected.has(row.id))
|
|
1344
1545
|
const overBatch = selectedRows.length > (state.limits?.maxBatch ?? 50)
|
|
1345
1546
|
const overMove = selectedRows.length > (state.limits?.maxMoveBatch ?? 20)
|
|
1346
1547
|
const selectedArchived = selectedRows.length > 0 && !overBatch && selectedRows.every((row) => row.archived)
|
|
@@ -1367,17 +1568,34 @@ window.__ModuleLoader__.load({
|
|
|
1367
1568
|
if (visibleAllSelected) {
|
|
1368
1569
|
setSelected((previous) => {
|
|
1369
1570
|
const next = new Set(previous)
|
|
1370
|
-
for (const row of
|
|
1571
|
+
for (const row of view.visibleRows) next.delete(row.id)
|
|
1371
1572
|
return next
|
|
1372
1573
|
})
|
|
1373
1574
|
} else {
|
|
1374
1575
|
setSelected((previous) => {
|
|
1375
1576
|
const next = new Set(previous)
|
|
1376
|
-
for (const row of
|
|
1577
|
+
for (const row of view.visibleRows) next.add(row.id)
|
|
1377
1578
|
return next
|
|
1378
1579
|
})
|
|
1379
1580
|
}
|
|
1380
1581
|
}
|
|
1582
|
+
const toggleGroup = (id) => {
|
|
1583
|
+
setExpanded((previous) => {
|
|
1584
|
+
const base = previous === null
|
|
1585
|
+
? new Set(view.groups.filter((group) => group.expanded).map((group) => group.id))
|
|
1586
|
+
: new Set(previous)
|
|
1587
|
+
const group = view.groups.find((item) => item.id === id)
|
|
1588
|
+
if (group?.expanded === true) base.delete(id)
|
|
1589
|
+
else base.add(id)
|
|
1590
|
+
return base
|
|
1591
|
+
})
|
|
1592
|
+
}
|
|
1593
|
+
const expandAll = () => setExpanded(new Set(view.groups.map((group) => group.id)))
|
|
1594
|
+
const collapseAll = () => setExpanded(new Set())
|
|
1595
|
+
const loadMore = (id) => {
|
|
1596
|
+
setLimits((previous) => ({ ...previous, [id]: (previous[id] ?? GROUP_PAGE_SIZE) + GROUP_PAGE_SIZE }))
|
|
1597
|
+
}
|
|
1598
|
+
const isSearch = query.trim() !== ''
|
|
1381
1599
|
|
|
1382
1600
|
if (selectedId !== null) {
|
|
1383
1601
|
const row = state.sessions.find((item) => item.id === selectedId)
|
|
@@ -1426,9 +1644,24 @@ window.__ModuleLoader__.load({
|
|
|
1426
1644
|
archived: state.totals.archived,
|
|
1427
1645
|
size: formatBytes(state.totals.bytes),
|
|
1428
1646
|
}),
|
|
1647
|
+
blankRows.length > 0 && !showBlank
|
|
1648
|
+
? ` · ${t('hiddenBlankCount', { n: blankRows.length })}`
|
|
1649
|
+
: '',
|
|
1429
1650
|
),
|
|
1651
|
+
blankActiveCount > 0 && showBlank
|
|
1652
|
+
? h('p', { className: 'dsm-hint' }, t('blankActiveHint', { n: blankActiveCount }))
|
|
1653
|
+
: null,
|
|
1430
1654
|
),
|
|
1431
1655
|
h('div', { className: 'dsm-head-actions' },
|
|
1656
|
+
blankArchivedRows.length > 0
|
|
1657
|
+
? h('button', {
|
|
1658
|
+
type: 'button',
|
|
1659
|
+
className: 'dsm-btn',
|
|
1660
|
+
disabled: busy,
|
|
1661
|
+
title: `${t('cleanupBlankHint')}${blankActiveCount > 0 ? ` ${t('blankActiveHint', { n: blankActiveCount })}` : ''}`,
|
|
1662
|
+
onClick: () => setBulkDeleteIds(blankArchivedRows.map((row) => row.id)),
|
|
1663
|
+
}, t('cleanupBlank', { n: blankArchivedRows.length }))
|
|
1664
|
+
: null,
|
|
1432
1665
|
h('button', { type: 'button', className: 'dsm-btn', disabled: busy, onClick: () => void load({ silent: true }) },
|
|
1433
1666
|
busy ? h(Spinner, { t, label: t('working') }) : t('refresh')),
|
|
1434
1667
|
),
|
|
@@ -1445,6 +1678,7 @@ window.__ModuleLoader__.load({
|
|
|
1445
1678
|
),
|
|
1446
1679
|
)
|
|
1447
1680
|
: null,
|
|
1681
|
+
h('div', { className: 'dsm-sticky' },
|
|
1448
1682
|
h(
|
|
1449
1683
|
'div',
|
|
1450
1684
|
{ className: 'dsm-toolbar' },
|
|
@@ -1500,8 +1734,16 @@ window.__ModuleLoader__.load({
|
|
|
1500
1734
|
}),
|
|
1501
1735
|
t('showSubagents'),
|
|
1502
1736
|
),
|
|
1737
|
+
h('label', { className: 'dsm-check' },
|
|
1738
|
+
h('input', {
|
|
1739
|
+
type: 'checkbox',
|
|
1740
|
+
checked: showBlank,
|
|
1741
|
+
onChange: (event) => setShowBlank(event.target.checked),
|
|
1742
|
+
}),
|
|
1743
|
+
t('showBlank'),
|
|
1744
|
+
),
|
|
1503
1745
|
),
|
|
1504
|
-
|
|
1746
|
+
view.visibleRows.length > 0
|
|
1505
1747
|
? h(
|
|
1506
1748
|
'div',
|
|
1507
1749
|
{ className: 'dsm-batch' },
|
|
@@ -1512,6 +1754,7 @@ window.__ModuleLoader__.load({
|
|
|
1512
1754
|
selectedRows.length > 0
|
|
1513
1755
|
? h(React.Fragment, null,
|
|
1514
1756
|
h('span', { className: 'dsm-hint' }, t('selectedCount', { n: selectedRows.length })),
|
|
1757
|
+
h('span', { className: 'dsm-hint' }, t('selectionScopeHint')),
|
|
1515
1758
|
overBatch ? h('span', { className: 'dsm-warn' }, t('tooMany', { max: state.limits?.maxBatch ?? 50 })) : null,
|
|
1516
1759
|
overMove ? h('span', { className: 'dsm-warn' }, t('tooMany', { max: state.limits?.maxMoveBatch ?? 20 })) : null,
|
|
1517
1760
|
h('button', {
|
|
@@ -1558,12 +1801,29 @@ window.__ModuleLoader__.load({
|
|
|
1558
1801
|
onClick: () => setSelected(new Set()),
|
|
1559
1802
|
}, t('clearSelection')),
|
|
1560
1803
|
!selectedMovable && selectedRows.length > 0
|
|
1561
|
-
? h('span', { className: 'dsm-hint' },
|
|
1804
|
+
? h('span', { className: 'dsm-hint' },
|
|
1805
|
+
selectedRows.some((row) => row.live || row.running) ? t('liveHint') : t('moveUnavailableHint'))
|
|
1562
1806
|
: null,
|
|
1563
1807
|
)
|
|
1564
1808
|
: null,
|
|
1565
1809
|
)
|
|
1566
1810
|
: null,
|
|
1811
|
+
),
|
|
1812
|
+
h('div', { className: 'dsm-list-actions' },
|
|
1813
|
+
h('button', {
|
|
1814
|
+
type: 'button',
|
|
1815
|
+
className: 'dsm-btn ghost',
|
|
1816
|
+
disabled: busy || isSearch || view.groups.length === 0 || view.groups.every((group) => group.expanded),
|
|
1817
|
+
onClick: expandAll,
|
|
1818
|
+
}, t('expandAll')),
|
|
1819
|
+
h('button', {
|
|
1820
|
+
type: 'button',
|
|
1821
|
+
className: 'dsm-btn ghost',
|
|
1822
|
+
disabled: busy || isSearch || view.groups.length === 0 || view.groups.every((group) => !group.expanded),
|
|
1823
|
+
onClick: collapseAll,
|
|
1824
|
+
}, t('collapseAll')),
|
|
1825
|
+
isSearch ? h('span', { className: 'dsm-hint' }, t('searchAutoExpand')) : null,
|
|
1826
|
+
),
|
|
1567
1827
|
bulkDeleteIds !== null
|
|
1568
1828
|
? h(DeleteScope, {
|
|
1569
1829
|
ctx,
|
|
@@ -1574,14 +1834,17 @@ window.__ModuleLoader__.load({
|
|
|
1574
1834
|
onConfirmed: (ids) => void deleteRows(ids),
|
|
1575
1835
|
})
|
|
1576
1836
|
: null,
|
|
1577
|
-
groups.length > 0
|
|
1578
|
-
? groups.map((group) =>
|
|
1837
|
+
view.groups.length > 0
|
|
1838
|
+
? view.groups.map((group) =>
|
|
1579
1839
|
h(Group, {
|
|
1580
1840
|
key: group.id,
|
|
1581
1841
|
group,
|
|
1582
1842
|
selected,
|
|
1583
|
-
onToggle:
|
|
1843
|
+
onToggle: () => toggleGroup(group.id),
|
|
1844
|
+
onSelect: toggle,
|
|
1584
1845
|
onOpen: openDetail,
|
|
1846
|
+
onMore: () => loadMore(group.id),
|
|
1847
|
+
locked: isSearch,
|
|
1585
1848
|
t,
|
|
1586
1849
|
}),
|
|
1587
1850
|
)
|
package/docs/01-design.md
CHANGED
|
@@ -202,3 +202,16 @@ targetDir = dirname(locate({id, cwd: target.path}))
|
|
|
202
202
|
- workspace 缓存 seam 是版本敏感点;升级 DSH 后先跑测试与实机验收。
|
|
203
203
|
- 侧栏标题在冷会话重命名后可能要刷新页面才更新(与宿主投影缓存有关)。
|
|
204
204
|
- 0.2.0 候选:侧栏菜单、内容搜索、导出、pin、整枝移动、体检/修复。
|
|
205
|
+
|
|
206
|
+
## 13. 列表视图与选择语义(0.1.1)
|
|
207
|
+
|
|
208
|
+
0.1.1 在 Client 内新增纯函数 `computeView`,把“过滤 → 排序 → 分组 → 折叠 → 每组分页 → 可见集”集中到一处,避免选择集与过滤条件各算各的。
|
|
209
|
+
|
|
210
|
+
- **默认折叠**:`totals.all > 100` 时 `expanded === null` 解释为全部折叠;`<= 100` 解释为全部展开。用户点击后 `expanded` 变成显式 Set。
|
|
211
|
+
- **每组分页**:`limits[groupId]` 默认 50,`加载更多` 每次 +50;只影响 `renderedRows`,不影响 `visibleRows`。
|
|
212
|
+
- **可见集**:`visibleRows = 过滤结果 ∩ 已展开工作区`;搜索时所有命中工作区强制展开(并禁用折叠按钮)。
|
|
213
|
+
- **选择裁剪**:`viewKey` 由 `generatedAt + tab + workspaceFilter + query + includeSubagents + defaultExpanded + expanded` 组成;变化后 `selected = selected ∩ visibleIdSet`,隐藏会话自动取消选择。
|
|
214
|
+
- **批量操作**:只使用 `visibleRows` 中的已选行;`全选当前列表` 只选可见集;批量条提示“选择仅作用于当前筛选与展开结果”。
|
|
215
|
+
- **吸附**:过滤工具栏与批量条包在同一个 `.dsm-sticky`(`position: sticky; top: 0`)中,滚动时整体吸在设置弹窗内容区顶部;批量条不再是独立 sticky。
|
|
216
|
+
|
|
217
|
+
0.2.0 会把 `state` 拆成“工作区概览 + 按工作区分页 sessions”,`computeView` 的过滤/分组逻辑保留,但数据来源从全量数组改为服务端页 + 缓存。
|
|
@@ -42,7 +42,11 @@ gh repo edit ZilongYang/dsh-session-mng \
|
|
|
42
42
|
--add-topic dsh-plugin --add-topic deepseek-harness --add-topic dsh --add-topic session
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
结果(2026-10-27):
|
|
46
|
+
- 公开仓库:https://github.com/ZilongYang/dsh-session-mng
|
|
47
|
+
- 首次 push:main,本地提交 `df3730a`(功能)+ `2f324e6`(文档)
|
|
48
|
+
- tag:`v0.1.0` 已推送;GitHub Release:https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.0
|
|
49
|
+
- topics:`dsh-plugin` / `deepseek-harness` / `dsh` / `session`
|
|
46
50
|
|
|
47
51
|
## 3. npm
|
|
48
52
|
|
|
@@ -60,7 +64,13 @@ curl -s https://registry.npmjs.org/dsh-session-mng/0.1.0 | head -c 300 # 以
|
|
|
60
64
|
- 发布后 packument 可能有约 10 分钟负缓存;以版本端点 / tarball 直链为准。
|
|
61
65
|
- tarball 应包含 28 个文件:`index.js`、`lib/*`、`client.js`、`cordis.patch.yml`、`locale/*.json`、`icon.svg`、`screenshots.json`、`assets/*`、`README.md`、`CHANGELOG.md`、`ROADMAP.md`、`LICENSE`、`docs/*`;不含 `test/`。
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
结果(2026-10-27):
|
|
68
|
+
- `npm publish` 返回 `PUT 202 Accepted`,npm 先创建了公开占位版本 `0.0.0-stage`(`Temporary package placeholder for staged publishing`),数分钟后公开 packument 出现 `0.1.0`:
|
|
69
|
+
- `curl https://registry.npmjs.org/dsh-session-mng/0.1.0` → **200**
|
|
70
|
+
- `npm view dsh-session-mng version dist.fileCount` → `0.1.0`,`28`
|
|
71
|
+
- `dist.tarball` 直链 200;`npm pack dsh-session-mng@0.1.0` 可下载 28 个文件
|
|
72
|
+
- 注意:`0.0.0-stage` 占位版本仍会出现在 packument 的 versions 列表里,`dist-tags.latest` 是 `0.1.0`;安装时默认取 latest,不受影响。不要把它当作真实版本。
|
|
73
|
+
- 该账号的 token 不能读 `npm profile` / `npm access`(E403),但 publish 可用;后续如需 `npm stage` 子命令,需要 npm CLI ≥11.15.0(当前 11.9.0 没有 `stage`)。
|
|
64
74
|
|
|
65
75
|
## 4. awesome-dsh-plugin / dshmarket
|
|
66
76
|
|
|
@@ -84,7 +94,12 @@ PR 描述建议包含:
|
|
|
84
94
|
|
|
85
95
|
时间:仓库创建满 24h 后再提 PR(或先提,CI 自动重跑变绿)。
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
结果(2026-10-27):
|
|
98
|
+
- PR:https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672
|
|
99
|
+
- 只新增一个文件:`data/plugins/ZilongYang__dsh-session-mng.yml`
|
|
100
|
+
- CI 首次运行:entry 格式 / README 一致性 / awesome-lint / 回归测试已通过,site build 进行中;仓库创建于当天,1 天年龄门槛可能先红,workflow 会自动重跑。
|
|
101
|
+
- 已验证 `assets/icon-180.png` 与三张截图的 raw.githubusercontent.com 直链返回 200。
|
|
102
|
+
- PR 描述包含:npm 链接、分类、`dsh.bundle`、截图/图标、与现有条目的差异、实测版本与测试。
|
|
88
103
|
|
|
89
104
|
## 5. 市场验证
|
|
90
105
|
|
|
@@ -103,3 +118,17 @@ curl -s https://awesome-dsh-plugin.com/plugins.json | grep -i session-mng
|
|
|
103
118
|
- [ ] PR 编号 / 合并状态 / CI 结果
|
|
104
119
|
- [ ] dshmarket 卡片截图或文本记录
|
|
105
120
|
- [ ] `docs/PLAN-2026-10-26.md` 末尾追加「事后差异(补记)」
|
|
121
|
+
|
|
122
|
+
## 7. 0.1.1 更新发布(2026-10-27)
|
|
123
|
+
|
|
124
|
+
0.1.1 是客户端可用性修复(工作区优先/分页、sticky、选择裁剪、控件视觉),Host API 未变,marketplace 条目不需要改 YAML。
|
|
125
|
+
|
|
126
|
+
- 本地提交:`cea99f9 fix: align sticky surface and enlarge list controls`(功能提交 `d36e725`)。
|
|
127
|
+
- `npm publish`:`dsh-session-mng@0.1.1`,31 个文件,tarball 560.7 kB / unpacked 767.4 kB;发布后约 90 秒 packument 出现 0.1.1,`latest` = 0.1.1,版本端点 200。
|
|
128
|
+
- Git:main 已 push;tag `v0.1.1`;GitHub Release 见仓库 Releases。
|
|
129
|
+
- marketplace:PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 不变;`check` 已通过,`Submission gate` 仅因仓库 0.0 天(需要 1 天)失败;该检查会自动重跑,约 24h 后转绿,不需要重新提交。
|
|
130
|
+
- Release:https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.1
|
|
131
|
+
|
|
132
|
+
## 8. 接手
|
|
133
|
+
|
|
134
|
+
下一步市场验证与 0.2.0 任务见 [HANDOVER-2026-10-27.md](HANDOVER-2026-10-27.md)。
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# dsh-session-mng 接手说明(2026-10-27)
|
|
2
|
+
|
|
3
|
+
> 面向下一个接手的人/会话。先读这一页,再按链接进入详细文档。当前代码在 `main`,工作区干净。
|
|
4
|
+
|
|
5
|
+
## 1. 当前状态
|
|
6
|
+
|
|
7
|
+
| 项 | 值 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 版本 | `0.1.1`(npm `latest`) |
|
|
10
|
+
| npm | https://www.npmjs.com/package/dsh-session-mng |
|
|
11
|
+
| GitHub | https://github.com/ZilongYang/dsh-session-mng |
|
|
12
|
+
| Release | https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.1 |
|
|
13
|
+
| 当前 main | 发布代码/功能提交 `d36e725`、`cea99f9`;交接后的文档提交以远端 `main` 为准 |
|
|
14
|
+
| Tag | `v0.1.1` → `1b6a07f` |
|
|
15
|
+
| 测试 | `node --test` 26 个全绿 |
|
|
16
|
+
| 包内容 | `npm pack` 31 个文件 / 560.8 kB;发布 tarball 560.7 kB |
|
|
17
|
+
| 本地安装 | 已作为 symlink 安装到 Desktop `desktop` profile;改 client 后刷新页面即可,Host 改动需重启 DSH |
|
|
18
|
+
| 市场 PR | awesome-dsh-plugin [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672),open |
|
|
19
|
+
|
|
20
|
+
### 市场 PR 的真实状态
|
|
21
|
+
|
|
22
|
+
- 条目文件:`data/plugins/ZilongYang__dsh-session-mng.yml`,只新增 1 个文件;分支 `add-dsh-session-mng`(fork `ZilongYang/awesome-dsh-plugin`)。
|
|
23
|
+
- CI:`check` **已通过**;`Submission gate` 唯一失败项是**仓库 0.0 天,要求满 1 天**。
|
|
24
|
+
- Gate 原文:`repository is 0.0 days old (needs 1) — this check re-runs by itself and should clear in about 24h. No need to resubmit, push, or close and reopen.`
|
|
25
|
+
- 预计自动转绿:仓库创建于 `2026-10-26T16:44:42Z`,满 1 天约为 **2026-10-27T16:44Z / 2026-10-28 00:44 Asia/Shanghai**。
|
|
26
|
+
- **不要**重新提交 PR 或关闭重开;gate 自动重跑。转绿后等维护者合并。
|
|
27
|
+
- 合并后 dshmarket/awesome 目录才会出现;市场卡片读取 npm 版本,当前应显示 `0.1.1`。验证:`curl -s https://awesome-dsh-plugin.com/plugins.json | grep -i session-mng`,以及在 dshmarket 搜 `dsh-session-mng`。
|
|
28
|
+
|
|
29
|
+
## 2. 代码地图
|
|
30
|
+
|
|
31
|
+
| 文件 | 职责 / 接手要点 |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `index.js` | Host 入口、inject、幂等挂载 `/session-mng/api` |
|
|
34
|
+
| `lib/api.js` | 路由、64 KiB body 上限、错误信封;**新增加分页接口从这里开始** |
|
|
35
|
+
| `lib/fence.js` | loopback / trustedHosts / Origin 围栏 |
|
|
36
|
+
| `lib/service.js` | 操作层;state / title / archive / delete / move |
|
|
37
|
+
| `lib/state.js` | 总览组装;0.2.0 的 overview/page 应从这里拆分 |
|
|
38
|
+
| `lib/title.js` | 整段会话标题、模型路由、清洗/截断 |
|
|
39
|
+
| `lib/store-jsonl.js` | header、zstd frame、目录复制/事务/回滚 |
|
|
40
|
+
| `lib/workspace-sync.js` | 移动后的 registry 缓存对账;删除后的账本清理 |
|
|
41
|
+
| `client.js` | 设置页单文件 UI;列表逻辑在 `computeView` / `SessionManagerPage` |
|
|
42
|
+
| `test/` | title / store-jsonl / state / service / api,共 26 个 |
|
|
43
|
+
|
|
44
|
+
### 0.1.1 列表逻辑(不要改坏)
|
|
45
|
+
|
|
46
|
+
- `computeView`:过滤 → 排序 → 分组 → 折叠 → 每组分页 → `visibleRows`;纯函数。
|
|
47
|
+
- 默认 `>100` 折叠;每工作区 50 条;`loadMore` 只改 `limits`,不改可见集。
|
|
48
|
+
- `viewKey` 变化(tab/工作区/搜索/展开集合/刷新)后 `selected = selected ∩ visibleIdSet`。
|
|
49
|
+
- 行 checkbox 的 `onSelect` 与工作区头 `onToggle` 必须分开;曾因此误折叠工作区。
|
|
50
|
+
- sticky 使用 `--dsw-alias-bg-layer-2`(与设置面板一致),不要改回 `bg-base` 黑色。
|
|
51
|
+
|
|
52
|
+
## 3. 下一步任务
|
|
53
|
+
|
|
54
|
+
### P0 — 等市场 gate / 合并(无代码)
|
|
55
|
+
|
|
56
|
+
1. 满 1 天后重新查看 `gh pr checks 9672 --repo awesome-dsh-plugin/awesome-dsh-plugin`。
|
|
57
|
+
2. gate 转绿后不要重复提交;等待维护者合并。
|
|
58
|
+
3. 合并后验证市场目录:`plugins.json` / dshmarket 搜索 / 卡片版本 `0.1.1` / 4 张截图 / `icon-180.png`。
|
|
59
|
+
4. 把结果回填到 `docs/02-open-source-and-marketplace.md` 第 7 节和 `docs/RELEASE-CHECKLIST-0.1.1.md` 的 E 节。
|
|
60
|
+
5. 如果维护者要求改描述/分类,只改 PR 里的那 1 个 YAML;代码仓库的发版不受影响。
|
|
61
|
+
|
|
62
|
+
### P1 — 0.2.0:Host 侧 overview + 分页 sessions(主任务)
|
|
63
|
+
|
|
64
|
+
目标:真正支撑 **1000–10000 条会话**,不再首屏拉全量 state。0.1.1 只是客户端折叠/分页,state payload 仍是全量(202 条约 103 KB;10000 条约 5 MB)。
|
|
65
|
+
|
|
66
|
+
建议(实现前先写 `docs/PLAN-2026-10-28.md` 并让子龙批准):
|
|
67
|
+
|
|
68
|
+
1. **`GET /session-mng/api/overview`**
|
|
69
|
+
- 只返回 `workspaces[]`(id/title/path/status/sessionIds.length/archivedCount/总大小)+ `totals` + `capabilities` + `orphans`。
|
|
70
|
+
- 计数优先用 `workspaceRegistry` 的内存 `sessionIds` / archived set;不要为计数读全部标题或日志。
|
|
71
|
+
- 未分组会话数量:需要一次 header 扫描;做成懒加载 + 短 TTL 缓存,不阻塞首屏。
|
|
72
|
+
2. **`GET /session-mng/api/sessions?workspaceId=&tab=&offset=&limit=&query=&sort=`**
|
|
73
|
+
- 只对当前页 id 做 `readTitleSnapshots` / `stat`;返回 `rows/total/offset/limit/hasMore`。
|
|
74
|
+
- 已归档:用 `archivedSessionIds` 过滤 workspace 账本;不要扫全量日志。
|
|
75
|
+
- 排序必须在 host 完成,保证分页稳定。
|
|
76
|
+
3. **全局搜索**
|
|
77
|
+
- 先只读验证 `sessionController.search` / `sessionQuery.searchSessions` 是否覆盖标题;不覆盖就限制为“当前工作区/已加载页标题搜索”,内容搜索走宿主索引。
|
|
78
|
+
- 搜索结果返回 id 列表后再按页补标题/大小,避免全量读标题。
|
|
79
|
+
4. **Client**
|
|
80
|
+
- `overview` 首屏 → 默认折叠;展开工作区时请求第一页;`加载更多` 请求下一页;缓存每工作区页。
|
|
81
|
+
- 保持 0.1.1 的 sticky、选择裁剪、搜索自动展开、视觉规范。
|
|
82
|
+
- 选择语义:优先保留“可见集”语义;跨页选择需要明确“已加载页”边界,避免隐藏页误操作。
|
|
83
|
+
5. **兼容**
|
|
84
|
+
- 保留 `GET /state` 给 0.1.x 客户端;Host 同时提供新旧接口,客户端 feature-detect.
|
|
85
|
+
- 版本 0.2.0;升级 README/CHANGELOG/ROADMAP;新增 `docs/RELEASE-CHECKLIST-0.2.0.md`.
|
|
86
|
+
6. **验收**
|
|
87
|
+
- 合成 10k 会话数据:首屏 overview 不带 session rows;单页 ≤50 行;不触发全量 `sessionController.list`。
|
|
88
|
+
- 现有 26 个测试 + 新增 overview/page/search/分页/空态/归档/未分组测试。
|
|
89
|
+
- 实机只读验证 202 条真实数据,再用手动合成数据验证 1000/10000;写操作仍只用一次性测试会话。
|
|
90
|
+
|
|
91
|
+
### P2 — 后续候选(不急)
|
|
92
|
+
|
|
93
|
+
侧栏会话菜单、全文搜索、导出、置顶、整枝移动、会话体检/修复、回收站、增量移动。按 `ROADMAP.md` 逐项排,不要一次全做。
|
|
94
|
+
|
|
95
|
+
## 4. 发布 / 开发命令
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
cd /Users/zilong/zWork/ai/zl-plugins/dsh-session-mng
|
|
99
|
+
|
|
100
|
+
npm test
|
|
101
|
+
node --check index.js client.js lib/*.js
|
|
102
|
+
npm --cache /tmp/dsh-npm-cache pack --dry-run
|
|
103
|
+
|
|
104
|
+
# npm 发布(0.1.x 均出现 202 + 0.0.0-stage 占位,约 90s 后 packument 出现新版本;以版本端点为准)
|
|
105
|
+
npm --cache /tmp/dsh-npm-cache publish
|
|
106
|
+
curl -s -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/dsh-session-mng/<version>
|
|
107
|
+
|
|
108
|
+
# GitHub
|
|
109
|
+
git push origin main
|
|
110
|
+
git tag v<version> && git push origin v<version>
|
|
111
|
+
gh release create v<version> --repo ZilongYang/dsh-session-mng --title "dsh-session-mng <version>" --notes-file /tmp/notes.md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 5. 环境与坑
|
|
115
|
+
|
|
116
|
+
- DSH `0.2.1-rc.2`;Desktop `desktop` profile **由应用独占**,不要用 `dsh plugin --profile desktop …` 管理;用应用内插件管理器或 `plugin_manager`。
|
|
117
|
+
- Client 服务只能 `ctx.get('uiWorkspace')`;直接 `ctx.uiWorkspace` 在未 inject 时会抛错,且被 try/catch 吞掉会导致“恢复并打开”消失。
|
|
118
|
+
- 移动依赖 workspace registry 内部缓存 seam;已做特性检测和回滚,升级 DSH 先跑测试 + 实机验收。
|
|
119
|
+
- 移动/删除只支持本地 JSONL;live 会话拒绝移动/删除,重启后重试。
|
|
120
|
+
- 测试/截图只用一次性测试会话或 `/Users/demo/...` + `session-demo-*` 假数据;不要提交真实标题/路径/会话 id。
|
|
121
|
+
- 公开计划是脱敏版;逐字原文在 `private/raw-plans/`(gitignore,不进 npm)。
|
|
122
|
+
- 0.1.1 的 npm tarball 是在最终文档回填前打的,包内 checklist 可能显示 pending;功能/README 无影响。不值得为这个单独发 0.1.2。
|
|
123
|
+
- GitHub tag `v0.1.1` 指向 `1b6a07f`;之后只有文档提交 `7e1b457`,代码一致。
|
|
124
|
+
|
|
125
|
+
## 6. 关键文档
|
|
126
|
+
|
|
127
|
+
- [0.1.0 批准计划](PLAN-2026-10-26.md)
|
|
128
|
+
- [0.1.1 实施计划](PLAN-2026-10-27.md)
|
|
129
|
+
- [0.1.1 发布清单](RELEASE-CHECKLIST-0.1.1.md)
|
|
130
|
+
- [设计说明](01-design.md)
|
|
131
|
+
- [开源/市场记录](02-open-source-and-marketplace.md)
|
|
132
|
+
- [路线图](../ROADMAP.md)
|
|
133
|
+
- [变更记录](../CHANGELOG.md)
|
package/docs/PLAN-2026-10-26.md
CHANGED
|
@@ -570,3 +570,5 @@ description:
|
|
|
570
570
|
7. **截图使用 demo 数据 + 折叠侧栏**,避免把真实工作区名、会话标题、路径带进公开仓库;三张图分别对应总览、标题提案、删除范围。
|
|
571
571
|
8. **计划里的「子代理级联删除」保留**:实现通过 `sessionQuery.traceSession` 收集后代,确认面板显示根/子数量与总字节;未在真实用户数据上执行,只用测试会话与 demo 预览验证。
|
|
572
572
|
9. **发布步骤尚未执行**:本地 git 初始化/公开仓库、npm publish、awesome-dsh-plugin PR 待用户明确同意后逐项进行;当前 `docs/02-open-source-and-marketplace.md` 与 `docs/RELEASE-CHECKLIST-0.1.0.md` 已写好 runbook 与待勾选项。
|
|
573
|
+
|
|
574
|
+
10. **发布结果(2026-10-27)**:GitHub `ZilongYang/dsh-session-mng` 已公开并推送 `v0.1.0`;npm `dsh-session-mng@0.1.0` 已发布(`npm publish` 返回 202,出现 `0.0.0-stage` 占位版本,数分钟后 `0.1.0` 公开可见);awesome-dsh-plugin PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 已提交,等待 CI/维护者。
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# dsh-session-mng 0.1.1 实施计划(会话列表可用性修复)
|
|
2
|
+
|
|
3
|
+
> 日期:2026-10-27。来源:子龙在 0.1.0 交付后提出的三个问题。已确认的选择:
|
|
4
|
+
> 1. 大列表:**0.1.1 快修 + 0.2.0 分页**;
|
|
5
|
+
> 2. 选择联动:**裁剪到可见集**;
|
|
6
|
+
> 3. 吸附:**过滤区 + 批量区一体吸附**。
|
|
7
|
+
|
|
8
|
+
## 1. 问题
|
|
9
|
+
|
|
10
|
+
1. 默认平铺全部会话;202 条时 state 约 103 KB,10000 条会到 MB 级、DOM 一万行,首屏和滚动都不可接受。
|
|
11
|
+
2. 只有批量条 sticky,过滤区滚走后看不到当前 tab/工作区/搜索条件。
|
|
12
|
+
3. 已选集合独立于过滤条件;切换筛选后批量操作仍可能作用于不可见会话。
|
|
13
|
+
|
|
14
|
+
## 2. 0.1.1 范围(客户端改动,Host API 不变)
|
|
15
|
+
|
|
16
|
+
### 2.1 工作区优先 + 客户端分页
|
|
17
|
+
|
|
18
|
+
- `总会话数 > 100` 时默认全部工作区折叠;`<= 100` 保持展开。
|
|
19
|
+
- 点击工作区标题展开/收起;提供「展开全部 / 收起全部」。
|
|
20
|
+
- 每个工作区默认渲染 50 条,`加载更多(剩余 N)` 每次 +50。
|
|
21
|
+
- 搜索/筛选命中时自动展开命中的工作区;搜索期间禁用折叠并提示“搜索时自动展开命中工作区”。
|
|
22
|
+
- 分组头显示会话数、总大小;折叠时只渲染头部,不渲染行。
|
|
23
|
+
|
|
24
|
+
### 2.2 吸附
|
|
25
|
+
|
|
26
|
+
- 过滤工具栏 + 批量条合并为一个 `.dsm-sticky`:`position: sticky; top: 0`,背景 `--dsw-alias-bg-base`,底部分隔线;批量条不再是独立 sticky。
|
|
27
|
+
- 列表操作条(展开全部/收起全部、选择范围提示)放在 sticky 块下方,随内容滚动。
|
|
28
|
+
|
|
29
|
+
### 2.3 选择裁剪到可见集
|
|
30
|
+
|
|
31
|
+
- 可见集 = 当前 tab/工作区/搜索过滤后的行 ∩ 已展开的工作区(搜索时自动展开)。
|
|
32
|
+
- 过滤、折叠、搜索、刷新、切换“显示子会话”后,`selected = selected ∩ 可见集`;为空自动清空。
|
|
33
|
+
- 批量操作只使用可见的已选行;`全选当前列表` 只选当前可见集。
|
|
34
|
+
- 批量条加提示:“选择仅作用于当前筛选与展开结果。”
|
|
35
|
+
|
|
36
|
+
## 3. 非目标(留给 0.2.0)
|
|
37
|
+
|
|
38
|
+
- 不做 Host 侧 overview/paged sessions 接口;首屏仍然拉全量 state(0.1.1 只压 DOM,不压 payload)。
|
|
39
|
+
- 不做未分组懒加载、全局内容搜索、虚拟滚动。
|
|
40
|
+
- 10000 条量级的真正分页、计数和全局搜索在 0.2.0 做。
|
|
41
|
+
|
|
42
|
+
## 4. 验收
|
|
43
|
+
|
|
44
|
+
- 202 条真实数据默认折叠;展开单个工作区只渲染 50 行;加载更多正常。
|
|
45
|
+
- 搜索命中时自动展开;折叠在搜索期间禁用。
|
|
46
|
+
- 过滤/折叠/搜索/刷新后已选集合被裁剪;批量操作不触碰隐藏行。
|
|
47
|
+
- 滚动时过滤区 + 批量条一起吸顶;详情打开/返回滚动位置正常。
|
|
48
|
+
- `node --test` 全绿;`npm pack` 正常;实机 UI 截图更新。
|
|
49
|
+
- 0.1.1 发布后验证版本端点;市场条目无需改 YAML(npm 版本自动更新)。
|
|
50
|
+
|
|
51
|
+
## 5. 发布清单
|
|
52
|
+
|
|
53
|
+
- 更新 `package.json` 0.1.1、`CHANGELOG.md`、`ROADMAP.md`、README。
|
|
54
|
+
- 新增 `docs/RELEASE-CHECKLIST-0.1.1.md`。
|
|
55
|
+
- Git commit / tag `v0.1.1` / GitHub Release。
|
|
56
|
+
- `npm publish` 0.1.1;验证 `registry.npmjs.org/dsh-session-mng/0.1.1`。
|
|
57
|
+
- dshmarket/awesome 条目无需重新 PR;确认市场版本随 npm 更新。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 事后差异(补记,2026-10-27)
|
|
62
|
+
|
|
63
|
+
1. **默认折叠阈值定为 100**:计划只写“>100 默认折叠”,实现按 `totals.all <= 100` 展开、`>100` 折叠;当前 202 条会默认折叠。
|
|
64
|
+
2. **自动展开只对搜索生效**,不是对所有 tab/工作区筛选生效。计划原文写“搜索/筛选命中”,实现选择“搜索命中自动展开;tab/工作区筛选保留折叠状态,由用户点击”。这样避免切 tab 时整页突然铺开;如需“工作区筛选自动展开”可在 0.2.0 数据流重构时一起做。
|
|
65
|
+
3. **可见集 = 过滤结果 ∩ 已展开工作区**;分页只影响渲染,不影响可见集。因此「全选当前列表」会选中已展开工作区中所有过滤行(包括尚未点“加载更多”的行),批量条显示真实数量。这样做是为了避免“分页把已选行变成隐藏行”的歧义。
|
|
66
|
+
4. **批量移动提示拆分**:选中 live 会话时显示原来的“已打开不能移动/删除”;其他不可移动原因(跨工作区、有子会话、状态未就绪)显示新的通用提示。
|
|
67
|
+
5. **新增第 4 张截图** `screenshot-4-expanded.png`,展示工作区展开、选择 2 条活跃会话、sticky 过滤 + 批量条;市场可展示 4 张。
|
|
68
|
+
6. **实现中修了一个行选择事件绑定 bug**:Group 的行 checkbox 最初误调用工作区折叠函数(`toggleGroup`),已拆成 `onSelect` / `onToggle` 两个 prop;该 bug 只存在于工作区,未进入 0.1.0 发布。
|
|
69
|
+
7. **0.1.1 仍是客户端快修**:首屏继续拉全量 state(202 条约 103 KB;10000 条约 5 MB),真正按工作区懒加载/分页在 0.2.0 做。这一点在验收时明确,不把 0.1.1 当成 10000 条的解决方案。
|
|
70
|
+
|
|
71
|
+
8. **发布结果(2026-10-27)**:0.1.1 已发布;npm `latest` = 0.1.1(31 文件,560.7 kB),GitHub main / tag `v0.1.1` / Release 已更新;marketplace 条目不变,版本随 npm 自动更新。
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
- [x] 仓库内没有真实会话 id、用户路径、token、Gmail 提交邮箱。
|
|
51
51
|
- [x] 截图使用 `/Users/demo/...`、`session-demo-*` 假数据。
|
|
52
52
|
- [x] 本地 `git init -b main` + commit `df3730a`(34 个文件,6406 行);`private/` 已被忽略、未入库。
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
- [ ] 市场 `plugins.json` / dshmarket
|
|
57
|
-
- [
|
|
53
|
+
- [x] 公开仓库 `ZilongYang/dsh-session-mng` + topics(`dsh-plugin` / `deepseek-harness` / `dsh` / `session`);tag `v0.1.0` 已推送。
|
|
54
|
+
- [x] npm `dsh-session-mng@0.1.0` 发布并验证版本端点(200;28 files)。`npm publish` 返回 202,npm 先创建 `0.0.0-stage` 占位版本,数分钟后 `0.1.0` 公开可见。
|
|
55
|
+
- [x] awesome-dsh-plugin PR:[#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672)(一个文件;CI pending)。
|
|
56
|
+
- [ ] 市场 `plugins.json` / dshmarket 搜索可见(等待 PR 合并;CI site build 进行中)。
|
|
57
|
+
- [x] GitHub tag `v0.1.0` 已推送;Release 可选(市场优先 npm)。
|
|
58
58
|
|
|
59
59
|
## F. 发版后回填
|
|
60
60
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# dsh-session-mng 0.1.1 发布检查清单
|
|
2
|
+
|
|
3
|
+
> 记录时间:2026-10-27。0.1.1 是客户端可用性修复;Host API 未变。发布完成后回填 npm/GitHub 结果。
|
|
4
|
+
|
|
5
|
+
## A. 代码
|
|
6
|
+
|
|
7
|
+
- [x] `package.json` 版本 `0.1.1`。
|
|
8
|
+
- [x] `node --check index.js client.js lib/*.js` 通过。
|
|
9
|
+
- [x] `node --test` 26 个测试全绿(Host/标题/移动/删除未改)。
|
|
10
|
+
- [x] 新增纯函数 `computeView`:过滤、排序、分组、折叠、每组分页、可见集计算集中在一处。
|
|
11
|
+
- [x] 行选择与工作区折叠事件分开绑定(`onSelect` / `onToggle`),避免勾选行时折叠工作区。
|
|
12
|
+
- [x] 选择裁剪 effect 使用 `viewKey`,覆盖 tab / 工作区 / 搜索 / 展开集合 / 刷新。
|
|
13
|
+
- [x] 批量移动提示区分 live 与一般不可移动项(新增 `moveUnavailableHint`)。
|
|
14
|
+
|
|
15
|
+
## B. 实机 UI(真实 202 条数据只读)
|
|
16
|
+
|
|
17
|
+
- [x] 202 条会话:12 个工作区默认全部折叠,行数 0,批量条隐藏。
|
|
18
|
+
- [x] 展开 `dsh-session-mng`:2 行,批量条出现;勾选行 → `已选 1 个` + “选择仅作用于当前筛选与展开结果”。
|
|
19
|
+
- [x] 切到「已归档」tab:选择裁剪到 0,批量条隐藏。
|
|
20
|
+
- [x] 切回「全部」并展开、勾选一行后切换工作区筛选:选择裁剪到 0,批量条隐藏。
|
|
21
|
+
- [x] 展开 `zilong`(81 条):先渲染 50 行,显示“加载更多(剩余 31 条)”;点击后 81 行。
|
|
22
|
+
- [x] 搜索“DSH 会话管理”:命中工作区自动展开,`aria-expanded=true`,工作区头 `disabled=true`,展开/收起按钮 disabled,显示搜索提示。
|
|
23
|
+
- [x] 搜索无匹配:空态“没有匹配的会话”,选择清空,批量条隐藏。
|
|
24
|
+
- [x] 折叠已选中的工作区:选择裁剪到 0,批量条隐藏。
|
|
25
|
+
- [x] 滚动到 700px:sticky 头仍在 top≈157px(设置弹窗内容区顶部),过滤区与批量条均可见。
|
|
26
|
+
- [x] 详情打开/返回滚动位置仍正常(0.1.0 修复未回归)。
|
|
27
|
+
|
|
28
|
+
## C. 截图
|
|
29
|
+
|
|
30
|
+
- [x] `screenshot-1-overview.png`:120 条 demo 会话、3 个工作区默认折叠(工作区优先)。
|
|
31
|
+
- [x] `screenshot-4-expanded.png`:展开 Atlas,选中 2 条活跃会话,sticky 过滤 + 批量条。
|
|
32
|
+
- [x] `screenshot-2-title.png` / `screenshot-3-delete.png` 保留 0.1.0 的 demo 截图。
|
|
33
|
+
- [x] 所有截图使用 `/Users/demo/...` + `session-demo-*` 假数据,侧栏折叠,不含真实会话。
|
|
34
|
+
- [x] `screenshots.json` 更新为 4 张。
|
|
35
|
+
|
|
36
|
+
## D. 发布
|
|
37
|
+
|
|
38
|
+
- [x] `npm --cache /tmp/dsh-npm-cache pack --dry-run`:31 个文件 / 560.8 kB;npm 发布实际 tarball 560.7 kB / unpacked 767.4 kB;包含 4 张截图。
|
|
39
|
+
- [x] `npm publish` 0.1.1(`+ dsh-session-mng@0.1.1`);版本端点 HTTP 200,`latest` = 0.1.1,`dist.fileCount` = 31。发布后约 90 秒 packument 才出现 0.1.1(与 0.1.0 相同的 staged 处理)。
|
|
40
|
+
- [x] Git commit `cea99f9` + push main + tag `v0.1.1` + GitHub Release(https://github.com/ZilongYang/dsh-session-mng/releases/tag/v0.1.1)。
|
|
41
|
+
- [x] 确认 npm `latest` = 0.1.1;marketplace 条目无需改 YAML,市场卡片版本随 npm 自动更新。
|
|
42
|
+
- [x] 在 `docs/02-open-source-and-marketplace.md` 记录 0.1.1 发布结果。
|
|
43
|
+
|
|
44
|
+
## E. 市场收录(进行中)
|
|
45
|
+
|
|
46
|
+
- [x] PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672) 已提交,只新增 1 个 YAML;条目不需要随 0.1.1 改。
|
|
47
|
+
- [x] `check` 已通过。
|
|
48
|
+
- [ ] `Submission gate`:当前仅因仓库 0.0 天(需要 1 天)失败;约 2026-10-28 00:44 Asia/Shanghai 自动重跑,不要重新提交/关闭重开。
|
|
49
|
+
- [ ] gate 转绿后等待维护者合并。
|
|
50
|
+
- [ ] 合并后验证 `plugins.json` / dshmarket 搜索、卡片版本 `0.1.1`、4 张截图、icon。
|
|
51
|
+
- [ ] 结果回填 `docs/02-open-source-and-marketplace.md` 与本节。
|
|
52
|
+
|
|
53
|
+
## F. 接手
|
|
54
|
+
|
|
55
|
+
完整接手说明、0.2.0 任务、环境坑见 [HANDOVER-2026-10-27.md](HANDOVER-2026-10-27.md)。
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# dsh-session-mng 0.1.2 发布检查清单
|
|
2
|
+
|
|
3
|
+
> 记录时间:2026-10-27。0.1.2 是空会话清理 + 删除范围弹窗的客户端修复;Host API 未变。**当前未发布**,等子龙实机测试后再执行 npm/GitHub。
|
|
4
|
+
|
|
5
|
+
## A. 实现
|
|
6
|
+
|
|
7
|
+
- [x] 默认隐藏 `blank: true` 的空会话;新增「显示空会话」开关。
|
|
8
|
+
- [x] 全部/活跃/已归档 tab 计数与隐藏空会话后的可见列表一致。
|
|
9
|
+
- [x] 总览行显示「已隐藏 N 个空会话」;显示空会话时才显示“未归档空会话需先归档”提示。
|
|
10
|
+
- [x] 头部新增「清理空会话(N)」:只收集 `blank && archived && can.delete` 的会话,打开删除范围预览;未归档空会话不混入。
|
|
11
|
+
- [x] 删除范围确认改为模态:内容区独立滚动、底部勾选/确认/取消固定;支持遮罩点击与 Esc 取消。
|
|
12
|
+
- [x] 单条详情里的删除范围与批量删除共用同一模态。
|
|
13
|
+
|
|
14
|
+
## B. 验证
|
|
15
|
+
|
|
16
|
+
- [x] `node --check client.js`。
|
|
17
|
+
- [x] `node --test` 26 个测试全绿(Host/标题/移动/删除未改)。
|
|
18
|
+
- [x] React render smoke(4 条假会话、2 条空白归档):
|
|
19
|
+
- 默认只渲染 2 条非空会话;tab 全部/活跃/已归档 = 2/2/0;`清理空会话` 按钮存在。
|
|
20
|
+
- 打开清理 → `.dsm-scope-overlay` / `.dsm-scope-dialog` / `.dsm-scope-foot` 存在;确认按钮 disabled;取消后弹窗消失。
|
|
21
|
+
- 勾选「显示空会话」→ 行数 4;再取消。
|
|
22
|
+
- [ ] 子龙实机测试:默认隐藏、显示空会话、清理空会话(**不要确认删除真实空会话**,除非明确要清)、删除范围弹窗滚动时底部按钮可见。
|
|
23
|
+
- [ ] 若确认文案/按钮位置需要微调,再改一轮。
|
|
24
|
+
|
|
25
|
+
## C. 发布(待批准)
|
|
26
|
+
|
|
27
|
+
- [x] `npm pack --dry-run`:33 个文件 / 568.2 kB(比 0.1.1 多 2 个 docs)。
|
|
28
|
+
- [ ] `npm publish` 0.1.2;验证版本端点与 latest。
|
|
29
|
+
- [ ] Git commit / push / tag `v0.1.2` / GitHub Release。
|
|
30
|
+
- [ ] 市场条目不需要改 YAML;版本随 npm 自动更新。
|
|
31
|
+
- [ ] 更新 `CHANGELOG` 的 unreleased 标记、README 当前状态、ROADMAP、`docs/HANDOVER-2026-10-27.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.2",
|
|
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",
|