dsh-sessions-manager 3.6.0 → 3.6.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/README.en.md +4 -4
- package/README.md +4 -4
- package/lib/client.js +100 -28
- package/lib/client.js.map +2 -2
- package/lib/index.js +323 -169
- package/lib/index.js.map +2 -2
- package/package.json +1 -1
- package/src/client/index.jsx +104 -28
- package/src/client/logic.js +29 -0
- package/src/index.js +409 -190
- package/src/session-meta-cache.js +18 -0
- package/src/title-persist-index.js +13 -3
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.6.
|
|
4
|
+
"version": "3.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/client/index.jsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
14
|
-
import {
|
|
14
|
+
import { canDropOnWorkspace, dotStateFor, effectiveTitleOf, foldSubagents, openSubagentToast, sessionForNodes, shortId, starredOf, titleBackfillDecision, toastDurationFor, workspaceForNodes } from './logic.js'
|
|
15
15
|
|
|
16
16
|
export const inject = ['slots']
|
|
17
17
|
|
|
@@ -359,6 +359,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
359
359
|
const [trashCheck, setTrashCheck] = useState(null)
|
|
360
360
|
const [purgeTarget, setPurgeTarget] = useState(null)
|
|
361
361
|
const [details, setDetails] = useState({})
|
|
362
|
+
const detailsAt = useRef({})
|
|
362
363
|
const [openDetails, setOpenDetails] = useState(null)
|
|
363
364
|
const [detailsLoading, setDetailsLoading] = useState(null)
|
|
364
365
|
const [mdBusy, setMdBusy] = useState(null)
|
|
@@ -440,6 +441,14 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
440
441
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
441
442
|
}, [])
|
|
442
443
|
|
|
444
|
+
// v3.6.2 #5:面板只在 mount 拉一次,预热窗口内打开 = 整列占位冻结到手点
|
|
445
|
+
// 「重新加载」。这里挂一个 warm-done 钩子:host 预热从「在途」翻到「完成」
|
|
446
|
+
// 的瞬间自动补一次刷新(标题/血缘/空白分类一起浮现)。空闲时该转换不发生,
|
|
447
|
+
// 不引入任何周期性刷新。
|
|
448
|
+
const refreshRef = useRef(null)
|
|
449
|
+
refreshRef.current = refresh
|
|
450
|
+
useEffect(() => dsmOnWarmDone(() => { if (refreshRef.current) refreshRef.current() }), [])
|
|
451
|
+
|
|
443
452
|
useEffect(() => {
|
|
444
453
|
try { localStorage.setItem(PANEL_PREFS_KEY, JSON.stringify({ filter, workspaceFilter, sortBy, groupByLineage })) } catch (e) {}
|
|
445
454
|
}, [filter, workspaceFilter, sortBy, groupByLineage])
|
|
@@ -556,11 +565,13 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
556
565
|
const filtered = base.filter((item) => {
|
|
557
566
|
if (workspaceFilter !== 'all' && (item.workspacePath || '') !== workspaceFilter) return false
|
|
558
567
|
if (!needle) return true
|
|
559
|
-
|
|
568
|
+
// v3.6.2 #6:占位标题的会话按「有效标题」(列表值 → 侧栏权威回落)匹配,
|
|
569
|
+
// 预热补齐但面板未刷新时也能按关键词搜到。
|
|
570
|
+
return [effectiveTitleOf(item, dsmAuthoritativeTitles), item.sessionId, item.workspaceTitle, item.workspacePath].some((value) => String(value || '').toLocaleLowerCase().includes(needle))
|
|
560
571
|
})
|
|
561
572
|
return [...filtered].sort((a, b) => {
|
|
562
573
|
if (sortBy === 'oldest') return Number(a.createdAt || 0) - Number(b.createdAt || 0)
|
|
563
|
-
if (sortBy === 'title') return
|
|
574
|
+
if (sortBy === 'title') return effectiveTitleOf(a, dsmAuthoritativeTitles).localeCompare(effectiveTitleOf(b, dsmAuthoritativeTitles), 'zh-CN')
|
|
564
575
|
return Number(b.createdAt || 0) - Number(a.createdAt || 0)
|
|
565
576
|
})
|
|
566
577
|
}, [sessions, filter, query, workspaceFilter, sortBy, emptyList, starredList])
|
|
@@ -917,10 +928,22 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
917
928
|
|
|
918
929
|
const toggleDetails = (it) => {
|
|
919
930
|
if (openDetails === it.sessionId) { setOpenDetails(null); return }
|
|
920
|
-
if (details[it.sessionId]) {
|
|
931
|
+
if (details[it.sessionId]) {
|
|
932
|
+
// v3.6.2 #11:详情是同生命周期内永不失效的快照,重开永远显示旧数。
|
|
933
|
+
// 加 30s TTL:过期即后台重取(先展示旧数据不闪空,回来再替换)。
|
|
934
|
+
setOpenDetails(it.sessionId)
|
|
935
|
+
const at = detailsAt.current[it.sessionId] || 0
|
|
936
|
+
if (Date.now() - at > 30000) {
|
|
937
|
+
postJSON('/archived-sessions/details', { sessionId: it.sessionId })
|
|
938
|
+
.then((d) => { detailsAt.current[it.sessionId] = Date.now(); setDetails((m) => ({ ...m, [it.sessionId]: d })) })
|
|
939
|
+
.catch(() => {})
|
|
940
|
+
}
|
|
941
|
+
return
|
|
942
|
+
}
|
|
921
943
|
setDetailsLoading(it.sessionId)
|
|
922
944
|
postJSON('/archived-sessions/details', { sessionId: it.sessionId })
|
|
923
945
|
.then((d) => {
|
|
946
|
+
detailsAt.current[it.sessionId] = Date.now()
|
|
924
947
|
setDetails((m) => ({ ...m, [it.sessionId]: d }))
|
|
925
948
|
setDetailsLoading(null)
|
|
926
949
|
setOpenDetails(it.sessionId)
|
|
@@ -1638,6 +1661,16 @@ let dsmCapabilities = null
|
|
|
1638
1661
|
// 血缘分层披露(issue #6):服务端血缘分类 + 本地视图状态。
|
|
1639
1662
|
// dsmLineage: id → { origin, parentSession, delegationDepth, empty }
|
|
1640
1663
|
const dsmLineage = new Map()
|
|
1664
|
+
// v3.6.2:host 预热是否还在途(sidebar-state/sessions 的 warmPending 旗标)。
|
|
1665
|
+
// 在途 → 侧栏轮询从 32s 台阶收紧到每拍(4s),标题/血缘一补齐就浮现;
|
|
1666
|
+
// 完成瞬间(true→false)触发一次性回调(面板自动补刷新,见 dsmOnWarmDone)。
|
|
1667
|
+
let dsmWarmPending = false
|
|
1668
|
+
let dsmTrashRetryTimer = null
|
|
1669
|
+
const dsmWarmDoneHooks = new Set()
|
|
1670
|
+
function dsmOnWarmDone(fn) {
|
|
1671
|
+
dsmWarmDoneHooks.add(fn)
|
|
1672
|
+
return () => dsmWarmDoneHooks.delete(fn)
|
|
1673
|
+
}
|
|
1641
1674
|
// 空白会话一律隐藏(原「纯净视图」的默认行为固化为唯一行为,开关已于
|
|
1642
1675
|
// 0.1.3 移除:上游本就不把空白会话渲染进侧栏,开关没有可服务的场景)。
|
|
1643
1676
|
// 只隐藏,永不删除;子代理行按 DSH 设计不进侧栏(ui-subagent README 明文
|
|
@@ -1665,8 +1698,19 @@ async function dsmLoadTrashIds() {
|
|
|
1665
1698
|
for (const [id, info] of Object.entries((r && r.lineage) || {})) {
|
|
1666
1699
|
if (info && typeof info === 'object') dsmLineage.set(String(id), info)
|
|
1667
1700
|
}
|
|
1701
|
+
// v3.6.2:预热完成的瞬间广播一次(面板挂自动补刷新;侧栏靠节拍恢复 32s)。
|
|
1702
|
+
const wasPending = dsmWarmPending
|
|
1703
|
+
dsmWarmPending = !!(r && r.warmPending)
|
|
1704
|
+
if (wasPending && !dsmWarmPending) { try { for (const fn of dsmWarmDoneHooks) fn() } catch (e) { /* hook 出错不断主流程 */ } }
|
|
1668
1705
|
if (dsmRepaintDots) dsmRepaintDots()
|
|
1669
|
-
} catch (e) {
|
|
1706
|
+
} catch (e) {
|
|
1707
|
+
/* keep last known set */
|
|
1708
|
+
// v3.6.2 C:失败不再等下一个 32s 台阶——2s 后快速重试一次(已有待重试则跳过)。
|
|
1709
|
+
if (dsmTrashRetryTimer == null) {
|
|
1710
|
+
dsmTrashRetryTimer = setTimeout(() => { dsmTrashRetryTimer = null; dsmLoadTrashIds() }, 2000)
|
|
1711
|
+
if (typeof dsmTrashRetryTimer.unref === 'function') dsmTrashRetryTimer.unref()
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1670
1714
|
return dsmTrashIds
|
|
1671
1715
|
}
|
|
1672
1716
|
|
|
@@ -2068,7 +2112,10 @@ function installSidebarStatusDots() {
|
|
|
2068
2112
|
error: 'var(--dsw-alias-state-error-primary)', // 红 出错/需关注 (DSH error)
|
|
2069
2113
|
}
|
|
2070
2114
|
const manualUnread = dsmLoadManual()
|
|
2071
|
-
|
|
2115
|
+
// v3.6.2 #2:原 WeakSet「首绘一次即终身免改」升级为 WeakMap 所有权记录
|
|
2116
|
+
// row → { id, text, authoritative }。权威标题**后到**时回填仍由我们持有文本的
|
|
2117
|
+
// 行;官方(重命名等)改写文本后所有权移交,绝不覆盖 DSH Core 的实时更新。
|
|
2118
|
+
const titleBaselines = new WeakMap()
|
|
2072
2119
|
// 空分组兜底:DSH 会把「日志已不在原处」的会话重新归到「未分组」组里(删
|
|
2073
2120
|
// 除后最明显)。我们把行隐藏了,但空组标题会留下来——所以组内所有行都被
|
|
2074
2121
|
// 隐藏时,把整个分组(含它的标题行)一起隐藏;组内重新出现可见行时自动
|
|
@@ -2185,20 +2232,24 @@ function installSidebarStatusDots() {
|
|
|
2185
2232
|
if (row.style.display === 'none') row.style.display = ''
|
|
2186
2233
|
if (!isEmpty && li && li.parentSession && li.origin !== 'subagent') {
|
|
2187
2234
|
// fork 分支行:绿「⑂ 分支」chip,点击滚动并高亮父会话行。
|
|
2188
|
-
|
|
2189
|
-
|
|
2235
|
+
// v3.6.2 #3:chip 每次 paint 幂等同步(存在才不建,但 tooltip/父 id
|
|
2236
|
+
// 随最新权威数据刷新)——旧实现「创建即定格」,父标题后到时 tooltip
|
|
2237
|
+
// 永远停在降级文案;点击改读 dataset,React 复用行 DOM 也不会切错父。
|
|
2238
|
+
const parentTitle = dsmAuthoritativeTitles.get(li.parentSession)
|
|
2239
|
+
const tip = parentTitle ? '分支于:' + parentTitle : '分支会话(点击查看来源会话)'
|
|
2240
|
+
let fork = row.querySelector('[data-dsm-tag="fork"]')
|
|
2241
|
+
if (!fork) {
|
|
2242
|
+
fork = document.createElement('button')
|
|
2190
2243
|
fork.type = 'button'
|
|
2191
2244
|
fork.dataset.dsmTag = 'fork'
|
|
2192
2245
|
fork.className = 'dsm-tag dsm-tag-fork'
|
|
2193
|
-
const parentTitle = dsmAuthoritativeTitles.get(li.parentSession)
|
|
2194
2246
|
fork.textContent = '⑂ 分支'
|
|
2195
|
-
fork.title = parentTitle ? '分支于:' + parentTitle : '分支会话(点击查看来源会话)'
|
|
2196
|
-
fork.setAttribute('aria-label', fork.title)
|
|
2197
2247
|
fork.addEventListener('click', (e) => {
|
|
2198
2248
|
e.stopPropagation(); e.preventDefault()
|
|
2249
|
+
const target = e.currentTarget.dataset.dsmForkParent || ''
|
|
2199
2250
|
const rows = document.querySelectorAll('[role="treeitem"]')
|
|
2200
2251
|
for (const other of rows) {
|
|
2201
|
-
if (rowId(other) ===
|
|
2252
|
+
if (rowId(other) === target) {
|
|
2202
2253
|
other.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
2203
2254
|
other.classList.add('dsm-row-flash')
|
|
2204
2255
|
setTimeout(() => other.classList.remove('dsm-row-flash'), 1200)
|
|
@@ -2208,6 +2259,8 @@ function installSidebarStatusDots() {
|
|
|
2208
2259
|
})
|
|
2209
2260
|
row.appendChild(fork)
|
|
2210
2261
|
}
|
|
2262
|
+
if (fork.dataset.dsmForkParent !== li.parentSession) fork.dataset.dsmForkParent = String(li.parentSession)
|
|
2263
|
+
if (fork.title !== tip) { fork.title = tip; fork.setAttribute('aria-label', tip) }
|
|
2211
2264
|
} else {
|
|
2212
2265
|
const staleFork = row.querySelector('[data-dsm-tag="fork"]')
|
|
2213
2266
|
if (staleFork) staleFork.remove()
|
|
@@ -2261,19 +2314,31 @@ function installSidebarStatusDots() {
|
|
|
2261
2314
|
// Session is opened. Paint the latest log-folded title from the host
|
|
2262
2315
|
// authority without materializing the Session or changing its log.
|
|
2263
2316
|
const authoritativeTitle = dsmAuthoritativeTitles.get(id)
|
|
2264
|
-
|
|
2265
|
-
const
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
const
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2317
|
+
{
|
|
2318
|
+
const base = titleBaselines.get(row)
|
|
2319
|
+
// 零成本快路径:已定型且权威没更新 → 不查 DOM 不比较。
|
|
2320
|
+
if (!base || base.id !== id || (authoritativeTitle && authoritativeTitle !== base.authoritative)) {
|
|
2321
|
+
const node = rowNode(row) || {}
|
|
2322
|
+
const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === 'string'))
|
|
2323
|
+
const spans = [...row.children].filter((el) => el.tagName === 'SPAN' && !el.querySelector('[data-state]') && (el.textContent || '').trim())
|
|
2324
|
+
let titleEl = null
|
|
2325
|
+
if (base && base.id === id) {
|
|
2326
|
+
// 回填路径:只认我们上次写入/记录的那段文本,绝不猜别的 span。
|
|
2327
|
+
titleEl = spans.find((el) => (el.textContent || '') === base.text) || spans.find((el) => expected.has((el.textContent || '').trim())) || null
|
|
2328
|
+
} else {
|
|
2329
|
+
titleEl = spans.find((el) => expected.has((el.textContent || '').trim())) || spans[0] || null
|
|
2330
|
+
}
|
|
2331
|
+
if (titleEl) {
|
|
2332
|
+
const d = titleBackfillDecision({
|
|
2333
|
+
rendered: titleEl.textContent || '',
|
|
2334
|
+
baseline: base && base.id === id ? { text: base.text, authoritative: base.authoritative } : null,
|
|
2335
|
+
authoritative: authoritativeTitle || '',
|
|
2336
|
+
})
|
|
2337
|
+
if (d) {
|
|
2338
|
+
if (d.changed && titleEl.textContent !== d.text) titleEl.textContent = d.text
|
|
2339
|
+
titleBaselines.set(row, { id, text: d.nextBaseline.text, authoritative: d.nextBaseline.authoritative })
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2277
2342
|
}
|
|
2278
2343
|
}
|
|
2279
2344
|
// Read DSH's REAL session status from the row's own StateDot and recolor
|
|
@@ -2407,7 +2472,10 @@ function installSidebarStatusDots() {
|
|
|
2407
2472
|
const flat = []
|
|
2408
2473
|
const walk = (nodes, depth) => { for (const n of nodes || []) { flat.push([n, depth]); if (depth < 4) walk(n.children, depth + 1) } }
|
|
2409
2474
|
walk(rec.nodes, 0)
|
|
2410
|
-
|
|
2475
|
+
// v3.6.2 #8:树里 title 为 null(未暖/失败)的子节点用侧栏权威标题回落
|
|
2476
|
+
// 参与签名与渲染——权威后到时签名变化,展开态就地补写,不再定格短 id。
|
|
2477
|
+
const effTitle = (n) => n.title || dsmAuthoritativeTitles.get(String(n.sessionId)) || ''
|
|
2478
|
+
const sig = rec.status + '|' + flat.map(([n, d]) => [n.sessionId, effTitle(n), n.live ? 1 : 0, n.sizeBytes, d].join(':')).join(';')
|
|
2411
2479
|
if (box.dataset.dsmSig === sig) return
|
|
2412
2480
|
box.dataset.dsmSig = sig
|
|
2413
2481
|
box.textContent = ''
|
|
@@ -2436,7 +2504,7 @@ function installSidebarStatusDots() {
|
|
|
2436
2504
|
})
|
|
2437
2505
|
const name = document.createElement('span')
|
|
2438
2506
|
name.className = 'dsm-sub-name'
|
|
2439
|
-
name.textContent = n
|
|
2507
|
+
name.textContent = effTitle(n) || (String(n.sessionId).slice(0, 8) + '…')
|
|
2440
2508
|
name.title = n.sessionId
|
|
2441
2509
|
r.appendChild(name)
|
|
2442
2510
|
const meta = document.createElement('span')
|
|
@@ -2466,11 +2534,18 @@ function installSidebarStatusDots() {
|
|
|
2466
2534
|
const FALLBACK_TICK_MS = 4000
|
|
2467
2535
|
const tick = () => {
|
|
2468
2536
|
if (typeof document !== 'undefined' && document.hidden) return
|
|
2469
|
-
|
|
2537
|
+
// v3.6.2 C:预热在途时每拍(4s)都拉 sidebar-state,补齐结果立刻浮现;
|
|
2538
|
+
// 空闲时维持低频 32s(3.4.1 移除全表轮询的性能决策不回退——这里轮询的
|
|
2539
|
+
// 是轻量 authority 路由,且仅在有活可补的窗口期加速)。
|
|
2540
|
+
const cadence = dsmWarmPending ? 1 : 8
|
|
2541
|
+
if (++dsmTrashTick % cadence === 0) dsmLoadTrashIds()
|
|
2470
2542
|
paint()
|
|
2471
2543
|
}
|
|
2472
2544
|
tick()
|
|
2473
2545
|
dsmLoadTrashIds()
|
|
2546
|
+
// v3.6.2 C:冷启动快速二拍——首拉响应可能仍被空白精判/预热排队压着,
|
|
2547
|
+
// 1.5s 后再拉一次,把「⑂ 分支」等后到信息的台阶从 32s 缩到秒级。
|
|
2548
|
+
const coldSecondBeat = setTimeout(() => dsmLoadTrashIds(), 1500)
|
|
2474
2549
|
paint()
|
|
2475
2550
|
const obs = new MutationObserver(schedulePaint)
|
|
2476
2551
|
obs.observe(document.body, {
|
|
@@ -2485,6 +2560,7 @@ function installSidebarStatusDots() {
|
|
|
2485
2560
|
// 跟着旧闭包活到页面关闭,注入节点也会留在侧栏上。
|
|
2486
2561
|
return () => {
|
|
2487
2562
|
clearInterval(tickTimer)
|
|
2563
|
+
clearTimeout(coldSecondBeat)
|
|
2488
2564
|
obs.disconnect()
|
|
2489
2565
|
dsmTeardownSidebarAug()
|
|
2490
2566
|
}
|
package/src/client/logic.js
CHANGED
|
@@ -30,6 +30,35 @@ export function authoritativeTitleForFirstPaint({ firstPaint = false, rendered =
|
|
|
30
30
|
return authoritative
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// v3.6.2 #2:占位/冷标题的行在权威标题**后到**时回填。返回 null 表示不动 DOM。
|
|
34
|
+
// 所有权规则(与首绘一致,绝不覆盖 DSH Core 的实时更新):
|
|
35
|
+
// - 首次见到该行 → 有权威就用权威(changed 仅在权威≠当前文本时为 true);
|
|
36
|
+
// - 之后:当前文本仍等于我们上次记录/写入的值(baseline.text)且权威更新 →
|
|
37
|
+
// 回填新权威;当前文本被官方改过 → 只重新记录所有权,绝不写。
|
|
38
|
+
export function titleBackfillDecision({ rendered = '', baseline = null, authoritative = '' } = {}) {
|
|
39
|
+
const auth = authoritative || ''
|
|
40
|
+
if (!baseline) {
|
|
41
|
+
const next = auth || rendered
|
|
42
|
+
return { text: next, changed: !!auth && auth !== rendered, nextBaseline: { text: next, authoritative: auth || null } }
|
|
43
|
+
}
|
|
44
|
+
if (rendered !== baseline.text) {
|
|
45
|
+
// 文本已被(官方渲染或用户重命名)改动:所有权移交,仅重新记录。
|
|
46
|
+
return { changed: false, nextBaseline: { text: rendered, authoritative: baseline.authoritative || null } }
|
|
47
|
+
}
|
|
48
|
+
if (!auth || auth === baseline.authoritative) return null
|
|
49
|
+
return { text: auth, changed: true, nextBaseline: { text: auth, authoritative: auth } }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// v3.6.2 #6:列表搜索/标题排序的「有效标题」= 列表项标题,缺时回落到侧栏
|
|
53
|
+
// 权威标题缓存(预热补齐后即使面板还没刷新,搜索/排序也按真标题工作)。
|
|
54
|
+
export function effectiveTitleOf(item, authoritativeTitles) {
|
|
55
|
+
if (!item) return ''
|
|
56
|
+
if (item.title) return String(item.title)
|
|
57
|
+
const id = String(item.sessionId)
|
|
58
|
+
const auth = authoritativeTitles && authoritativeTitles.get(id)
|
|
59
|
+
return auth ? String(auth) : ''
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
// 拖拽迁移前置校验:同工作区拦截(workspacePath 相等即拒绝)。
|
|
34
63
|
// 无 workspacePath 的会话(如侧栏 live 行尚未同步)放行,由 host 最终裁决。
|
|
35
64
|
export function canDropOnWorkspace(item, target) {
|