dsh-sessions-manager 3.7.4 → 3.7.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-sessions-manager",
3
3
  "description": "DSH 设置面板会话管理器:归档 / 恢复 / 彻底删除 / 移动到其他工作区,带工作区标签与会话日期;统一「会话管理」面板。Session manager for the DeepSeek Harness settings panel — archive / restore / permanently delete / move sessions across workspaces, with workspace tags & session dates in one unified panel.",
4
- "version": "3.7.4",
4
+ "version": "3.7.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -24,7 +24,8 @@ const CSS = `
24
24
  .archv{--dsm-radius-tag:9px;--dsm-radius-ctl:9px;--dsm-radius-sheet:10px;--dsm-radius-card:12px;display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
25
25
  .archv-head{display:flex;align-items:center;gap:10px;margin:0 0 2px}
26
26
  .archv-title{font-size:16px;font-weight:650;color:var(--dsw-alias-label-primary);letter-spacing:-0.01em;margin:0}
27
- .archv-stamp{font-size:11px;font-weight:500;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:2px 8px;white-space:nowrap;cursor:default}
27
+ .archv-stamp{font-size:11px;font-weight:500;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:999px;padding:2px 8px;white-space:nowrap;cursor:pointer;transition:color .15s ease,border-color .15s ease}
28
+ .archv-stamp:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
28
29
  .archv-count{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-subtle);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:1px 8px;flex:none}
29
30
  .archv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
30
31
  .archv-err{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 12px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);border-radius:var(--dsm-radius-sheet);color:var(--dsw-alias-state-error-primary);font-size:12px;margin-bottom:10px}
@@ -49,8 +50,11 @@ const CSS = `
49
50
  .sess-fbtn-on{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3)}
50
51
  .sess-tools{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin:0 0 8px}
51
52
  .sess-tools-4{grid-template-columns:repeat(auto-fit,minmax(132px,1fr))}
52
- /* T4 起搜索行有 5 个字段(搜索/工作区/标签/排序/分组):收窄下限保证 800px 内一行排满。 */
53
- .sess-tools-5{grid-template-columns:repeat(auto-fit,minmax(124px,1fr))}
53
+ /* T4 起搜索行有 5 个字段(搜索/工作区/标签/排序/分组):固定 5 列 + minmax(0,1fr)
54
+ 一行排满——auto-fit(minmax(124px,…)) 在面板宽度不足 5×124+gap 时会把最后
55
+ 一列挤到第二行(分组孤行)。minmax(0,1fr) 允许列均分收缩,select/input
56
+ 各自截断;<640px 仍由下方断点收成单列。 */
57
+ .sess-tools-5{grid-template-columns:repeat(5,minmax(0,1fr))}
54
58
  .dsm-kids{display:flex;flex-direction:column;gap:6px;margin:10px 0 2px;margin-left:43px;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l3)}
55
59
  .dsm-kid .dsm-kids{margin-left:8px;margin-top:6px}
56
60
  .dsm-kids-toggle{appearance:none;min-height:22px;padding:0 9px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-business-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary);border-radius:var(--dsm-radius-tag);font:inherit;font-size:11px;font-weight:500;cursor:pointer;flex:none;white-space:nowrap}
@@ -886,6 +890,37 @@ function SessionPanel({ workspacesSvc }) {
886
890
  timer.current = setTimeout(() => setToast(null), toastDurationFor(msg, kind))
887
891
  }
888
892
 
893
+ // 版本徽章点击复制完整 buildStamp。webview 里 navigator.clipboard 可能缺失
894
+ // 或被权限拒绝(此前失败被静默吞掉,用户以为按钮坏了)——失败落
895
+ // execCommand 兜底;两条路都不通时至少把完整版本号亮在 toast 里。
896
+ const copyBuildStamp = async () => {
897
+ const full = String(capabilities && capabilities.buildStamp || '')
898
+ if (!full) return
899
+ try {
900
+ if (navigator.clipboard && navigator.clipboard.writeText) {
901
+ await navigator.clipboard.writeText(full)
902
+ showToast('已复制完整版本号 ' + full, 'ok')
903
+ return
904
+ }
905
+ throw new Error('clipboard unavailable')
906
+ } catch (e) {
907
+ try {
908
+ const ta = document.createElement('textarea')
909
+ ta.value = full
910
+ ta.style.position = 'fixed'
911
+ ta.style.opacity = '0'
912
+ document.body.appendChild(ta)
913
+ ta.select()
914
+ const ok = document.execCommand('copy')
915
+ document.body.removeChild(ta)
916
+ if (!ok) throw new Error('execCommand failed')
917
+ showToast('已复制完整版本号 ' + full, 'ok')
918
+ } catch (e2) {
919
+ showToast('复制失败,完整版本号:' + full, 'err')
920
+ }
921
+ }
922
+ }
923
+
889
924
  const refresh = () => {
890
925
  setError(null)
891
926
  Promise.all([
@@ -1608,7 +1643,13 @@ function SessionPanel({ workspacesSvc }) {
1608
1643
  <div className="archv-head">
1609
1644
  <h2 className="archv-title">会话管理</h2>
1610
1645
  {capabilities && capabilities.buildStamp
1611
- ? <span className="archv-stamp" title={capabilities.buildStamp} onClick={() => navigator.clipboard && navigator.clipboard.writeText(capabilities.buildStamp).catch(() => {})}>{capabilities.buildStamp}</span>
1646
+ ? <span
1647
+ className="archv-stamp"
1648
+ role="button"
1649
+ aria-label="复制完整版本号"
1650
+ title={capabilities.buildStamp}
1651
+ onClick={copyBuildStamp}
1652
+ >v{String(capabilities.buildStamp).split('+')[0]}</span>
1612
1653
  : null}
1613
1654
  {sessions !== null && <span className="archv-count" aria-label={`${sessions.length} 个会话`}>{sessions.length}</span>}
1614
1655
  </div>
@@ -1895,8 +1936,8 @@ function SessionPanel({ workspacesSvc }) {
1895
1936
  <div className="sess-field">
1896
1937
  <label htmlFor="dsm-group">分组</label>
1897
1938
  <select id="dsm-group" value={groupByLineage ? 'lineage' : 'flat'} onChange={(e) => setGroupByLineage(e.target.value === 'lineage')} title="血缘分组:子代理折叠、分支聚拢成组;平铺:与 DSH 原生一致,全部并列">
1898
- <option value="lineage">血缘(折叠分组)</option>
1899
- <option value="flat">平铺(全部并列)</option>
1939
+ <option value="lineage">血缘(折叠)</option>
1940
+ <option value="flat">平铺(并列)</option>
1900
1941
  </select>
1901
1942
  </div>
1902
1943
  </div>
package/src/index.js CHANGED
@@ -251,6 +251,31 @@ export function apply(ctx) {
251
251
  return result
252
252
  }
253
253
 
254
+ // —— 0.2.0+ 官方 title 投影快路径(2026-09-29 审计 P2)————————————————
255
+ // dsh-session-title(0.2.0 起 dsh-base 默认组合挂载)把标题折叠官方化为
256
+ // O(1) 投影(key='title',state 为 string|null),注册表对已 materialize 的
257
+ // cell 提供 cachedSnapshot——只读 watermark 缓存,**零折叠、零磁盘 I/O**
258
+ // (对比 readTitleSnapshots 每条整本解码,issue #8)。命中即免预热。
259
+ // 红线:
260
+ // 1. 只信 cachedSnapshot(绝不调 stateOf——那会对内存日志做同步折叠,
261
+ // 违反「请求路径零重活」纪律);
262
+ // 2. 服务不存在(0.1.7)/会话未实例化/任何异常 → 静默返回 null,行为与
263
+ // 3.7.4 完全一致;
264
+ // 3. cachedSnapshot 的 title=null 只说明「该 cell 还没有非空标题」(可能
265
+ // 会话从未打开、cell 未折叠)——一律当**未知**交回预热队列,绝不冒充
266
+ // 「会话真的无标题」(empty 三态纪律,v3.7.0 T1)。
267
+ function projectionTitleOf(liveStore, id) {
268
+ try {
269
+ const reg = ctx.get('sessionProjections')
270
+ if (!reg || typeof reg.cachedSnapshot !== 'function') return null
271
+ const session = liveStore && liveStore.get && liveStore.get(id)
272
+ if (!session) return null
273
+ const snap = reg.cachedSnapshot(session, ['title'])
274
+ const title = snap && snap.values ? snap.values.title : null
275
+ return typeof title === 'string' && title.length > 0 ? title : null
276
+ } catch (e) { return null }
277
+ }
278
+
254
279
  async function archivedState() {
255
280
  const d = dom()
256
281
  if (!d) throw new Error('workspace domain is not open')
@@ -1478,7 +1503,26 @@ export function apply(ctx) {
1478
1503
  createdAt: (header && header.createdAt != null) ? header.createdAt : meta.createdAt,
1479
1504
  })
1480
1505
  }
1481
- const stillMissing = missing.filter((id) => !persisted.has(id))
1506
+ const stillMissing0 = missing.filter((id) => !persisted.has(id))
1507
+ // 0.2.0+ 官方投影快路径:先把「已实例化且 cell 已 materialize」的会话从
1508
+ // 预热队列里捞回来(cachedSnapshot 零 I/O),剩下的才进后台预热。
1509
+ // 注意:metaCache.set 在指纹不可算(无 stat / revision 缺失)时**拒绝写入**,
1510
+ // 所以命中结果另存 projectionMeta,本请求的渲染链直接可用——缓存只是增益。
1511
+ const projectionMeta = new Map()
1512
+ const stillMissing = []
1513
+ for (const id of stillMissing0) {
1514
+ const title = projectionTitleOf(live, id)
1515
+ if (title == null) { stillMissing.push(id); continue }
1516
+ const entry = entryById.get(id)
1517
+ const header = entry && entry.header ? entry.header : null
1518
+ const meta = {
1519
+ title,
1520
+ cwd: (header && typeof header.cwd === 'string' && header.cwd) ? header.cwd : null,
1521
+ createdAt: (header && header.createdAt != null) ? header.createdAt : null,
1522
+ }
1523
+ projectionMeta.set(id, meta)
1524
+ metaCache.set(id, statsById.get(id), meta)
1525
+ }
1482
1526
  // issue #8:缺失部分交后台预热(分批 + 让步),请求路径零投影、零日志解码。
1483
1527
  if (stillMissing.length) enqueueWarm(stillMissing, statsById, entryById)
1484
1528
  const items = []
@@ -1487,7 +1531,7 @@ export function apply(ctx) {
1487
1531
  // metaCache(persisted.get 里的 cwd 可能因移动会话而陈旧)。
1488
1532
  // 完全未命中的会话也不能丢 cwd/createdAt——它们来自 list header,零成本
1489
1533
  // 且是工作区归属/创建时间的权威来源;只有标题允许占位等后台预热(issue #8)。
1490
- let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || null
1534
+ let meta = metaCache.get(id, statsById.get(id)) || persisted.get(id) || projectionMeta.get(id) || null
1491
1535
  if (!meta) {
1492
1536
  meta = { title: null, cwd: null, createdAt: null }
1493
1537
  const entry = entryById.get(id)