dsh-session-mng 0.1.0 → 0.1.1

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 — 2026-10-27
4
+
5
+ ### Changed
6
+
7
+ - 会话列表改为**工作区优先**:会话总数 >100 时默认折叠所有工作区,点击展开;≤100 保持展开。折叠时只渲染工作区头,不渲染行。
8
+ - 每个工作区默认渲染 50 条,支持「加载更多(剩余 N 条)」;搜索/筛选后自动回到前 50 条。
9
+ - 搜索命中时自动展开命中的工作区,并禁用折叠/展开全部/收起全部,提示“搜索时自动展开命中工作区”。
10
+ - 过滤工具栏 + 批量操作条合并为同一个 sticky 头部,滚动时筛选条件与批量操作始终可见。
11
+ - 选择集裁剪到当前筛选与已展开工作区的可见集:切换 tab、工作区、搜索、折叠工作区、刷新后,隐藏会话自动取消选择;`全选当前列表` 只作用于可见集。
12
+ - 批量条增加“选择仅作用于当前筛选与展开结果”提示;批量移动不可用时区分 live 会话与一般不可移动项。
13
+ - 新增「展开全部 / 收起全部」。
14
+
15
+ ### Compatibility
16
+
17
+ - 与 0.1.0 相同:DSH `0.2.1-rc.2` 实测;移动需要 Node `>=22.15`;移动/删除仅支持本地 JSONL 后端。
18
+
3
19
  ## 0.1.0 — 2026-10-27
4
20
 
5
21
  首个公开版本。
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # dsh-session-mng
2
2
 
3
+ [![npm package](https://img.shields.io/npm/v/dsh-session-mng.svg?label=npm)](https://www.npmjs.com/package/dsh-session-mng)
4
+ [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/deepseek-ai/deepseek-harness)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,8 @@
11
15
  ## 功能
12
16
 
13
17
  - **统一会话总览**:按工作区分组列出当前 profile 的全部会话(活跃 + 已归档)。显示标题、路径、会话 id、大小、最后活动时间、运行/已打开/已归档/已置顶/子会话标记;支持搜索、工作区筛选、排序、刷新、批量选择。
18
+ - **工作区优先 + 分页**:会话数 >100 时默认折叠工作区;每个工作区默认 50 条,「加载更多」继续展开;搜索自动展开命中工作区。过滤工具栏和批量条整体吸顶。
19
+ - **选择跟随可见集**:切换筛选/折叠/搜索/刷新后,隐藏会话自动取消选择;`全选当前列表` 只作用于当前筛选与已展开的工作区。
14
20
  - **生成标题**:选中任意会话(**包括已归档的冷会话**),让模型总结整段对话,给出可编辑的标题提案;**确认后才写入**。活跃会话走官方 `sessionTitle.rename`,冷会话按官方 `session/title` 事件形状追加,不需要恢复会话。
15
21
  - **归档 / 恢复**:调用官方 `workspaceRegistry.archiveSession` / `unarchiveSession`。运行中的会话先提示,用户二次确认后才停止并归档;恢复后回到原工作区原位置。
16
22
  - **跨工作区移动**:把冷会话真正迁移到目标工作区——改写会话日志 header 的 `cwd`、把日志目录迁到目标项目目录、同步 workspace 账本。SessionId、归档状态、pin 状态不变,事件字节不变。
@@ -19,6 +25,8 @@
19
25
  - **失效归档标记清理**:归档集合里有、但磁盘上已无日志的 id,会以横幅提示并允许清理。
20
26
  - **中英双语**、浅色/深色主题跟随宿主。
21
27
 
28
+ ![Expanded workspace](assets/screenshot-4-expanded.png)
29
+
22
30
  ![Title proposal](assets/screenshot-2-title.png)
23
31
 
24
32
  ![Deletion scope](assets/screenshot-3-delete.png)
@@ -46,6 +54,8 @@ dsh plugin --profile <profile> add link:/绝对路径/dsh-session-mng
46
54
 
47
55
  Host 半是进程内模块:装完如果看不到页面,重启一次 dsh/应用再刷新页面;Client 半刷新页面即可。
48
56
 
57
+ > 市场状态:已向 awesome-dsh-plugin 提交收录 PR [#9672](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/9672)(审核中;dshmarket 使用同一份目录,合并后可直接在插件市场搜索到)。
58
+
49
59
  ## 使用
50
60
 
51
61
  1. 打开 **设置 → 会话管理**。
@@ -132,6 +142,8 @@ npm --cache /tmp/dsh-npm-cache pack --dry-run
132
142
 
133
143
  - [实施计划(批准时原样归档)](docs/PLAN-2026-10-26.md)
134
144
  - [0.1.0 发布检查清单](docs/RELEASE-CHECKLIST-0.1.0.md)
145
+ - [0.1.1 实施计划](docs/PLAN-2026-10-27.md)
146
+ - [0.1.1 发布检查清单](docs/RELEASE-CHECKLIST-0.1.1.md)
135
147
  - [设计与实现说明](docs/01-design.md)
136
148
  - [开源、发 npm 与市场收录记录](docs/02-open-source-and-marketplace.md)
137
149
  - [路线图](ROADMAP.md)
package/ROADMAP.md CHANGED
@@ -9,8 +9,18 @@
9
9
  - 彻底删除已归档会话(范围预览 + 子代理级联 + 事务 + 账本清理)。
10
10
  - 最近消息预览、失效归档标记清理、中英双语。
11
11
 
12
+ ## 0.1.1 — delivered
13
+
14
+ - 工作区优先:会话数 >100 默认折叠,点击展开;每个工作区 50 条分页 + 加载更多。
15
+ - 搜索命中自动展开,搜索期间禁止折叠;展开全部 / 收起全部。
16
+ - 过滤工具栏 + 批量条合并为 sticky 头部。
17
+ - 选择集裁剪到当前筛选与展开可见集;批量操作不触碰隐藏会话。
18
+ - 批量移动提示区分 live 与一般不可移动项。
19
+
12
20
  ## 0.2.0 — candidates
13
21
 
22
+ - Host 侧 overview + paged sessions 接口:首屏只取工作区计数,按工作区懒加载 50 条,真正支撑 1000–10000 条会话。
23
+ - 未分组会话懒加载 + 短缓存;全局搜索复用宿主索引。
14
24
  - 侧栏会话「...」菜单快捷入口(生成标题 / 会话管理详情),复用同一套 Host 操作。
15
25
  - 内容全文搜索(复用 `sessionQuery.searchSessions` / `searchEvents`),不只按标题/路径/id。
16
26
  - 导出所选会话为 Markdown / JSON(删除前的安全网;批量)。
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: {
@@ -43,6 +45,11 @@ window.__ModuleLoader__.load({
43
45
  sortTitle: '标题',
44
46
  showSubagents: '显示子会话',
45
47
  selectAll: '全选当前列表',
48
+ expandAll: '展开全部',
49
+ collapseAll: '收起全部',
50
+ loadMore: '加载更多(剩余 {n} 条)',
51
+ selectionScopeHint: '选择仅作用于当前筛选与展开结果。',
52
+ searchAutoExpand: '搜索时自动展开命中工作区。',
46
53
  selectRow: '选择会话 {name}',
47
54
  justNow: '刚刚',
48
55
  titleApplied: '标题已写入。',
@@ -152,6 +159,7 @@ window.__ModuleLoader__.load({
152
159
  timeout: '模型调用超时。',
153
160
  llmError: '模型调用失败:{message}',
154
161
  liveHint: '已打开的会话不能移动或彻底删除;重启 DSH 后重试。',
162
+ moveUnavailableHint: '所选会话中存在不可移动项(如跨工作区、有子会话或状态未就绪)。',
155
163
  archiveHint: '归档后会话会从活跃列表移到已归档,可在本页恢复。',
156
164
  moveArchivedHint: '移动不会改变归档状态,会话仍会留在已归档。',
157
165
  working: '处理中…',
@@ -185,6 +193,11 @@ window.__ModuleLoader__.load({
185
193
  sortTitle: 'Title',
186
194
  showSubagents: 'Show subagent sessions',
187
195
  selectAll: 'Select all shown',
196
+ expandAll: 'Expand all',
197
+ collapseAll: 'Collapse all',
198
+ loadMore: 'Load {n} more',
199
+ selectionScopeHint: 'Selection applies to the current filter and expanded workspaces.',
200
+ searchAutoExpand: 'Matching workspaces expand automatically while searching.',
188
201
  selectRow: 'Select session {name}',
189
202
  justNow: 'just now',
190
203
  titleApplied: 'Title applied.',
@@ -294,6 +307,7 @@ window.__ModuleLoader__.load({
294
307
  timeout: 'The model call timed out.',
295
308
  llmError: 'The model call failed: {message}',
296
309
  liveHint: 'An open session cannot be moved or deleted; restart DSH and retry.',
310
+ moveUnavailableHint: 'Some selected sessions cannot be moved (cross-workspace, subagent descendants, or not ready).',
297
311
  archiveHint: 'Archiving moves the session from the active list to the archived list; restore it here.',
298
312
  moveArchivedHint: 'Moving does not change the archive state; the session stays archived.',
299
313
  working: 'Working…',
@@ -341,11 +355,33 @@ window.__ModuleLoader__.load({
341
355
  .dsm-note.warn{border-color:var(--dsw-alias-state-warn-primary)}
342
356
  .dsm-note-actions{display:flex;gap:6px;flex-wrap:wrap;flex:0 0 auto}
343
357
  .dsm-note detail{flex:1 1 100%}
358
+ .dsm-sticky{position:sticky;top:0;z-index:3;display:flex;flex-direction:column;gap:8px;
359
+ padding:6px 0 8px;background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
344
360
  .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
- position:sticky;top:0;z-index:2;box-shadow:0 1px 0 var(--dsw-alias-border-l1)}
361
+ border:1px dashed var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}
362
+ .dsm-list-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:0 2px}
347
363
  .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;background:var(--dsw-alias-bg-layer-2)}
364
+ .dsm-group-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 10px;width:100%;
365
+ background:var(--dsw-alias-bg-layer-2);border:none;font:inherit;color:inherit;text-align:left;cursor:pointer}
366
+ .dsm-group-head:hover:not(:disabled){background:var(--dsw-alias-bg-layer-1)}
367
+ .dsm-group-head:disabled{cursor:default;opacity:.85}
368
+ .dsm-chev{flex:0 0 18px;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;
369
+ color:var(--dsw-alias-label-secondary)}
370
+ .dsm-row input[type="checkbox"],.dsm-check input[type="checkbox"]{
371
+ appearance:none;-webkit-appearance:none;width:18px;height:18px;margin:0;flex:0 0 auto;
372
+ border:1.5px solid var(--dsw-alias-border-l2);border-radius:4px;background:transparent;
373
+ display:inline-grid;place-content:center;cursor:pointer;transition:background .12s,border-color .12s}
374
+ .dsm-row input[type="checkbox"]::before,.dsm-check input[type="checkbox"]::before{
375
+ content:'';width:9px;height:5px;border-left:2px solid #fff;border-bottom:2px solid #fff;
376
+ transform:rotate(-45deg) scale(0);transition:transform .12s}
377
+ .dsm-row input[type="checkbox"]:checked,.dsm-check input[type="checkbox"]:checked{
378
+ background:#0f766e;border-color:#0f766e}
379
+ .dsm-row input[type="checkbox"]:checked::before,.dsm-check input[type="checkbox"]:checked::before{
380
+ transform:rotate(-45deg) scale(1)}
381
+ .dsm-row input[type="checkbox"]:focus-visible,.dsm-check input[type="checkbox"]:focus-visible{
382
+ outline:2px solid #14b8a6;outline-offset:2px}
383
+ .dsm-more{width:100%;justify-content:center;border-radius:0;border-left:none;border-right:none;
384
+ border-bottom:none;border-top:1px solid var(--dsw-alias-border-l1);min-height:30px}
349
385
  .dsm-group-title{font-size:12px;font-weight:600;line-height:18px}
350
386
  .dsm-group-path{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);word-break:break-all}
351
387
  .dsm-group-meta{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
@@ -634,14 +670,34 @@ window.__ModuleLoader__.load({
634
670
  )
635
671
  }
636
672
 
637
- function Group({ group, selected, onToggle, onOpen, t }) {
673
+ function Group({ group, selected, onToggle, onSelect, onOpen, onMore, t, locked }) {
674
+ const remaining = group.rows.length - group.renderedRows.length
638
675
  return h(
639
676
  'section',
640
677
  { className: 'dsm-group' },
641
678
  h(
642
- 'header',
643
- { className: 'dsm-group-head' },
644
- h('div', { style: { minWidth: 0 } },
679
+ 'button',
680
+ {
681
+ type: 'button',
682
+ className: 'dsm-group-head',
683
+ 'aria-expanded': group.expanded === true ? 'true' : 'false',
684
+ disabled: locked === true,
685
+ title: locked === true ? t('searchAutoExpand') : undefined,
686
+ onClick: onToggle,
687
+ },
688
+ h('span', { className: 'dsm-chev', 'aria-hidden': 'true' },
689
+ h('svg', {
690
+ viewBox: '0 0 16 16',
691
+ width: 16,
692
+ height: 16,
693
+ fill: 'none',
694
+ stroke: 'currentColor',
695
+ 'stroke-width': 1.6,
696
+ 'stroke-linecap': 'round',
697
+ 'stroke-linejoin': 'round',
698
+ }, h('path', { d: group.expanded === true ? 'M4 6l4 4 4-4' : 'M6 4l4 4-4 4' })),
699
+ ),
700
+ h('div', { style: { minWidth: 0, flex: '1 1 auto' } },
645
701
  h('div', { className: 'dsm-group-title' }, group.title || t('workspaceUngrouped')),
646
702
  group.path ? h('div', { className: 'dsm-group-path' }, group.path) : null,
647
703
  group.status !== undefined && group.status !== 'ok'
@@ -650,16 +706,23 @@ window.__ModuleLoader__.load({
650
706
  ),
651
707
  h('div', { className: 'dsm-group-meta' }, t('groupLine', { n: group.rows.length, size: formatBytes(group.bytes) })),
652
708
  ),
653
- group.rows.map((row) =>
709
+ group.renderedRows.map((row) =>
654
710
  h(Row, {
655
711
  key: row.id,
656
712
  row,
657
713
  selected: selected.has(row.id),
658
- onToggle: () => onToggle(row.id),
714
+ onToggle: () => onSelect(row.id),
659
715
  onOpen: () => onOpen(row.id),
660
716
  t,
661
717
  }),
662
718
  ),
719
+ group.expanded === true && remaining > 0
720
+ ? h(
721
+ 'button',
722
+ { type: 'button', className: 'dsm-btn ghost dsm-more', onClick: onMore },
723
+ t('loadMore', { n: remaining }),
724
+ )
725
+ : null,
663
726
  )
664
727
  }
665
728
 
@@ -1086,6 +1149,71 @@ window.__ModuleLoader__.load({
1086
1149
  // Page
1087
1150
  // ---------------------------------------------------------------------
1088
1151
 
1152
+ /**
1153
+ * Derive the filtered / grouped / rendered view for the list.
1154
+ * Pure so the selection-pruning effect can use it before the loading guard.
1155
+ */
1156
+ function computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits }) {
1157
+ const rows = state?.sessions ?? []
1158
+ const workspaceOrder = state?.workspaces ?? []
1159
+ const text = query.trim().toLowerCase()
1160
+ const filtering = text !== ''
1161
+ const filtered = rows.filter((row) => {
1162
+ if (tab === 'active' && row.archived) return false
1163
+ if (tab === 'archived' && !row.archived) return false
1164
+ if (workspaceFilter === 'ungrouped' && row.workspaceId !== null) return false
1165
+ if (workspaceFilter !== 'all' && workspaceFilter !== 'ungrouped' && row.workspaceId !== workspaceFilter) return false
1166
+ if (filtering) {
1167
+ const haystack = `${row.title}\n${row.cwd}\n${row.id}`.toLowerCase()
1168
+ if (!haystack.includes(text)) return false
1169
+ }
1170
+ return true
1171
+ })
1172
+ const sorted = [...filtered].sort((left, right) => {
1173
+ if (sort === 'created') return right.createdAt - left.createdAt
1174
+ if (sort === 'size') return right.sizeBytes - left.sizeBytes
1175
+ if (sort === 'title') return (left.title || left.cwd).localeCompare(right.title || right.cwd)
1176
+ return right.updatedAt - left.updatedAt
1177
+ })
1178
+ const baseGroups = []
1179
+ for (const workspace of workspaceOrder) {
1180
+ const groupRows = sorted.filter((row) => row.workspaceId === workspace.id)
1181
+ if (groupRows.length === 0) continue
1182
+ baseGroups.push({
1183
+ ...workspace,
1184
+ rows: groupRows,
1185
+ bytes: groupRows.reduce((sum, row) => sum + row.sizeBytes, 0),
1186
+ })
1187
+ }
1188
+ const ungroupedRows = sorted.filter((row) => row.workspaceId === null)
1189
+ if (ungroupedRows.length > 0) {
1190
+ baseGroups.push({
1191
+ id: 'ungrouped',
1192
+ title: '',
1193
+ path: '',
1194
+ rows: ungroupedRows,
1195
+ bytes: ungroupedRows.reduce((sum, row) => sum + row.sizeBytes, 0),
1196
+ })
1197
+ }
1198
+ const groups = baseGroups.map((group) => {
1199
+ const isExpanded = filtering ? true : (expanded === null ? defaultExpanded : expanded.has(group.id))
1200
+ const limit = limits[group.id] ?? GROUP_PAGE_SIZE
1201
+ return {
1202
+ ...group,
1203
+ expanded: isExpanded,
1204
+ limit,
1205
+ renderedRows: isExpanded ? group.rows.slice(0, limit) : [],
1206
+ }
1207
+ })
1208
+ const visibleRows = groups.flatMap((group) => (group.expanded ? group.rows : []))
1209
+ return {
1210
+ filteredRows: sorted,
1211
+ groups,
1212
+ visibleRows,
1213
+ visibleIdSet: new Set(visibleRows.map((row) => row.id)),
1214
+ }
1215
+ }
1216
+
1089
1217
  function SessionManagerPage(props) {
1090
1218
  const { ctx, t } = props
1091
1219
  useLocaleTick(ctx)
@@ -1103,6 +1231,8 @@ window.__ModuleLoader__.load({
1103
1231
  const [notice, setNotice] = React.useState(null)
1104
1232
  const [bulkDeleteIds, setBulkDeleteIds] = React.useState(null)
1105
1233
  const [bulkTarget, setBulkTarget] = React.useState('')
1234
+ const [expanded, setExpanded] = React.useState(null)
1235
+ const [limits, setLimits] = React.useState({})
1106
1236
  const abortRef = React.useRef(null)
1107
1237
  const searchRef = React.useRef(null)
1108
1238
  const listScroll = React.useRef(0)
@@ -1176,6 +1306,37 @@ window.__ModuleLoader__.load({
1176
1306
  return () => document.removeEventListener('keydown', onKeyDown, true)
1177
1307
  }, [selectedId])
1178
1308
 
1309
+ React.useEffect(() => {
1310
+ setLimits({})
1311
+ }, [tab, workspaceFilter, query, includeSubagents])
1312
+
1313
+ const rows = state?.sessions ?? []
1314
+ const defaultExpanded = (state?.totals?.all ?? 0) <= DEFAULT_COLLAPSE_AFTER
1315
+ const view = React.useMemo(
1316
+ () => computeView({ state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits }),
1317
+ [state, tab, workspaceFilter, query, sort, expanded, defaultExpanded, limits],
1318
+ )
1319
+ const viewKey = React.useMemo(
1320
+ () => [
1321
+ state?.generatedAt ?? 0,
1322
+ tab,
1323
+ workspaceFilter,
1324
+ query,
1325
+ includeSubagents ? 1 : 0,
1326
+ defaultExpanded ? 1 : 0,
1327
+ expanded === null ? 'auto' : [...expanded].sort().join(','),
1328
+ ].join('|'),
1329
+ [state?.generatedAt, tab, workspaceFilter, query, includeSubagents, defaultExpanded, expanded],
1330
+ )
1331
+ React.useEffect(() => {
1332
+ setSelected((previous) => {
1333
+ if (previous.size === 0) return previous
1334
+ const next = new Set()
1335
+ for (const id of previous) if (view.visibleIdSet.has(id)) next.add(id)
1336
+ return next.size === previous.size ? previous : next
1337
+ })
1338
+ }, [viewKey])
1339
+
1179
1340
  const run = async (operation, successText) => {
1180
1341
  setBusy(true)
1181
1342
  setNotice(null)
@@ -1294,53 +1455,13 @@ window.__ModuleLoader__.load({
1294
1455
  return h('div', { className: 'dsm-page' }, h('p', { className: 'dsm-hint' }, h(Spinner, { t, label: t('loading') })))
1295
1456
  }
1296
1457
 
1297
- const rows = state?.sessions ?? []
1298
- const workspaceOrder = state?.workspaces ?? []
1299
1458
  const tabs = [
1300
1459
  { key: 'all', label: t('tabAll'), count: rows.length },
1301
1460
  { key: 'active', label: t('tabActive'), count: rows.filter((row) => !row.archived).length },
1302
1461
  { key: 'archived', label: t('tabArchived'), count: rows.filter((row) => row.archived).length },
1303
1462
  ]
1304
- const visible = rows.filter((row) => {
1305
- if (tab === 'active' && row.archived) return false
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))
1463
+ const visibleAllSelected = view.visibleRows.length > 0 && view.visibleRows.every((row) => selected.has(row.id))
1464
+ const selectedRows = view.visibleRows.filter((row) => selected.has(row.id))
1344
1465
  const overBatch = selectedRows.length > (state.limits?.maxBatch ?? 50)
1345
1466
  const overMove = selectedRows.length > (state.limits?.maxMoveBatch ?? 20)
1346
1467
  const selectedArchived = selectedRows.length > 0 && !overBatch && selectedRows.every((row) => row.archived)
@@ -1367,17 +1488,34 @@ window.__ModuleLoader__.load({
1367
1488
  if (visibleAllSelected) {
1368
1489
  setSelected((previous) => {
1369
1490
  const next = new Set(previous)
1370
- for (const row of sorted) next.delete(row.id)
1491
+ for (const row of view.visibleRows) next.delete(row.id)
1371
1492
  return next
1372
1493
  })
1373
1494
  } else {
1374
1495
  setSelected((previous) => {
1375
1496
  const next = new Set(previous)
1376
- for (const row of sorted) next.add(row.id)
1497
+ for (const row of view.visibleRows) next.add(row.id)
1377
1498
  return next
1378
1499
  })
1379
1500
  }
1380
1501
  }
1502
+ const toggleGroup = (id) => {
1503
+ setExpanded((previous) => {
1504
+ const base = previous === null
1505
+ ? new Set(view.groups.filter((group) => group.expanded).map((group) => group.id))
1506
+ : new Set(previous)
1507
+ const group = view.groups.find((item) => item.id === id)
1508
+ if (group?.expanded === true) base.delete(id)
1509
+ else base.add(id)
1510
+ return base
1511
+ })
1512
+ }
1513
+ const expandAll = () => setExpanded(new Set(view.groups.map((group) => group.id)))
1514
+ const collapseAll = () => setExpanded(new Set())
1515
+ const loadMore = (id) => {
1516
+ setLimits((previous) => ({ ...previous, [id]: (previous[id] ?? GROUP_PAGE_SIZE) + GROUP_PAGE_SIZE }))
1517
+ }
1518
+ const isSearch = query.trim() !== ''
1381
1519
 
1382
1520
  if (selectedId !== null) {
1383
1521
  const row = state.sessions.find((item) => item.id === selectedId)
@@ -1445,6 +1583,7 @@ window.__ModuleLoader__.load({
1445
1583
  ),
1446
1584
  )
1447
1585
  : null,
1586
+ h('div', { className: 'dsm-sticky' },
1448
1587
  h(
1449
1588
  'div',
1450
1589
  { className: 'dsm-toolbar' },
@@ -1501,7 +1640,7 @@ window.__ModuleLoader__.load({
1501
1640
  t('showSubagents'),
1502
1641
  ),
1503
1642
  ),
1504
- sorted.length > 0
1643
+ view.visibleRows.length > 0
1505
1644
  ? h(
1506
1645
  'div',
1507
1646
  { className: 'dsm-batch' },
@@ -1512,6 +1651,7 @@ window.__ModuleLoader__.load({
1512
1651
  selectedRows.length > 0
1513
1652
  ? h(React.Fragment, null,
1514
1653
  h('span', { className: 'dsm-hint' }, t('selectedCount', { n: selectedRows.length })),
1654
+ h('span', { className: 'dsm-hint' }, t('selectionScopeHint')),
1515
1655
  overBatch ? h('span', { className: 'dsm-warn' }, t('tooMany', { max: state.limits?.maxBatch ?? 50 })) : null,
1516
1656
  overMove ? h('span', { className: 'dsm-warn' }, t('tooMany', { max: state.limits?.maxMoveBatch ?? 20 })) : null,
1517
1657
  h('button', {
@@ -1558,12 +1698,29 @@ window.__ModuleLoader__.load({
1558
1698
  onClick: () => setSelected(new Set()),
1559
1699
  }, t('clearSelection')),
1560
1700
  !selectedMovable && selectedRows.length > 0
1561
- ? h('span', { className: 'dsm-hint' }, t('liveHint'))
1701
+ ? h('span', { className: 'dsm-hint' },
1702
+ selectedRows.some((row) => row.live || row.running) ? t('liveHint') : t('moveUnavailableHint'))
1562
1703
  : null,
1563
1704
  )
1564
1705
  : null,
1565
1706
  )
1566
1707
  : null,
1708
+ ),
1709
+ h('div', { className: 'dsm-list-actions' },
1710
+ h('button', {
1711
+ type: 'button',
1712
+ className: 'dsm-btn ghost',
1713
+ disabled: busy || isSearch || view.groups.length === 0 || view.groups.every((group) => group.expanded),
1714
+ onClick: expandAll,
1715
+ }, t('expandAll')),
1716
+ h('button', {
1717
+ type: 'button',
1718
+ className: 'dsm-btn ghost',
1719
+ disabled: busy || isSearch || view.groups.length === 0 || view.groups.every((group) => !group.expanded),
1720
+ onClick: collapseAll,
1721
+ }, t('collapseAll')),
1722
+ isSearch ? h('span', { className: 'dsm-hint' }, t('searchAutoExpand')) : null,
1723
+ ),
1567
1724
  bulkDeleteIds !== null
1568
1725
  ? h(DeleteScope, {
1569
1726
  ctx,
@@ -1574,14 +1731,17 @@ window.__ModuleLoader__.load({
1574
1731
  onConfirmed: (ids) => void deleteRows(ids),
1575
1732
  })
1576
1733
  : null,
1577
- groups.length > 0
1578
- ? groups.map((group) =>
1734
+ view.groups.length > 0
1735
+ ? view.groups.map((group) =>
1579
1736
  h(Group, {
1580
1737
  key: group.id,
1581
1738
  group,
1582
1739
  selected,
1583
- onToggle: toggle,
1740
+ onToggle: () => toggleGroup(group.id),
1741
+ onSelect: toggle,
1584
1742
  onOpen: openDetail,
1743
+ onMore: () => loadMore(group.id),
1744
+ locked: isSearch,
1585
1745
  t,
1586
1746
  }),
1587
1747
  )
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
 
@@ -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,69 @@
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 条的解决方案。
@@ -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
- - [ ] 公开仓库 `ZilongYang/dsh-session-mng` + topics(`dsh-plugin` / `deepseek-harness` / `dsh` / `session`)。
54
- - [ ] npm `dsh-session-mng@0.1.0` 发布并验证版本端点。
55
- - [ ] awesome-dsh-plugin `data/plugins/ZilongYang__dsh-session-mng.yml` PR。
56
- - [ ] 市场 `plugins.json` / dshmarket 搜索可见。
57
- - [ ] GitHub tag `v0.1.0` 与 Release。
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,42 @@
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 / unpacked 766.6 kB;包含 4 张截图。
39
+ - [ ] `npm publish` 0.1.1;验证 `registry.npmjs.org/dsh-session-mng/0.1.1`。
40
+ - [ ] Git commit + `git push origin main` + tag `v0.1.1` + GitHub Release。
41
+ - [ ] 确认 npm `latest` = 0.1.1;marketplace 条目无需改 YAML(npm 版本自动更新)。
42
+ - [ ] 在 `docs/02-open-source-and-marketplace.md` 记录 0.1.1 发布结果。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-session-mng",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
package/screenshots.json CHANGED
@@ -1,5 +1,6 @@
1
1
  [
2
2
  "assets/screenshot-1-overview.png",
3
+ "assets/screenshot-4-expanded.png",
3
4
  "assets/screenshot-2-title.png",
4
5
  "assets/screenshot-3-delete.png"
5
6
  ]