dsh-sessions-manager 3.5.0 → 3.5.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 +12 -4
- package/README.md +12 -4
- package/lib/client.js +130 -92
- package/lib/client.js.map +2 -2
- package/lib/index.js +904 -241
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/client/index.jsx +154 -90
- package/src/client/logic.js +13 -1
- package/src/compat/capabilities.js +64 -8
- package/src/compat/persistence.js +137 -18
- package/src/handle-era-ops.js +185 -0
- package/src/handle-era-paths.js +126 -0
- package/src/index.js +373 -96
- package/src/markdown.js +122 -71
- package/src/path-guard.js +59 -0
- package/src/session-meta-cache.js +40 -13
- package/src/title-persist-index.js +6 -1
- package/src/zstd-frame.js +68 -34
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.5.
|
|
4
|
+
"version": "3.5.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 { canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
14
|
+
import { authoritativeTitleForFirstPaint, canDropOnWorkspace, dotStateFor, sessionForNodes, starredOf, workspaceForNodes } from './logic.js'
|
|
15
15
|
|
|
16
16
|
export const inject = ['slots']
|
|
17
17
|
|
|
@@ -21,12 +21,12 @@ function loadPanelPrefs() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const CSS = `
|
|
24
|
-
.archv{--dsm-radius-tag:4px;display:flex;flex-direction:column;gap:4px;max-width:800px;padding:8px 2px 28px}
|
|
24
|
+
.archv{--dsm-radius-tag:4px;--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
27
|
.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}
|
|
28
28
|
.archv-sub{font-size:12px;line-height:1.55;color:var(--dsw-alias-label-tertiary);margin:0 0 12px;max-width:64ch}
|
|
29
|
-
.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:
|
|
29
|
+
.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}
|
|
30
30
|
.archv-errretry{appearance:none;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent);background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:11px;cursor:pointer;flex:none}
|
|
31
31
|
.sess-filter{display:flex;align-items:center;gap:6px;margin:0 0 12px}
|
|
32
32
|
.sess-fbtn{appearance:none;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:999px;font-size:12px;font-weight:500;cursor:pointer}
|
|
@@ -35,13 +35,13 @@ const CSS = `
|
|
|
35
35
|
.sess-tools{display:grid;grid-template-columns:minmax(180px,1fr) minmax(140px,.7fr) minmax(130px,.55fr);gap:8px;margin:0 0 8px}
|
|
36
36
|
.sess-field{display:flex;flex-direction:column;gap:5px;min-width:0}
|
|
37
37
|
.sess-field label{font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary)}
|
|
38
|
-
.sess-field input,.sess-field select{box-sizing:border-box;width:100%;min-height:36px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
38
|
+
.sess-field input,.sess-field select{box-sizing:border-box;width:100%;min-height:36px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-ctl);background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}
|
|
39
39
|
.sess-results{font-size:11px;color:var(--dsw-alias-label-tertiary);margin:0 0 4px}
|
|
40
40
|
.archv button:focus-visible,.archv input:focus-visible,.archv select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
41
41
|
.sess-batch{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px 2px 4px;margin-bottom:4px}
|
|
42
42
|
.sess-btntext{font-size:12px;color:var(--dsw-alias-label-tertiary);flex:none}
|
|
43
43
|
.archv-list{display:flex;flex-direction:column;gap:8px}
|
|
44
|
-
.archv-card{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
44
|
+
.archv-card{display:flex;flex-direction:column;align-items:stretch;gap:0;padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-card);background:var(--dsw-alias-fill-elevated);transition:border-color .15s ease,background-color .15s ease}
|
|
45
45
|
.archv-card:hover{border-color:var(--dsw-alias-border-l4)}
|
|
46
46
|
.archv-card-exp{border-color:var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1)}
|
|
47
47
|
.archv-row{display:flex;align-items:center;gap:14px;width:100%;min-width:0}
|
|
@@ -62,16 +62,16 @@ const CSS = `
|
|
|
62
62
|
.archv-star-on svg{fill:currentColor}
|
|
63
63
|
.archv-body{flex:1;min-width:0;display:flex;align-items:center;gap:12px}
|
|
64
64
|
.archv-actions{display:flex;gap:8px;flex:none;flex-wrap:nowrap;justify-content:flex-end}
|
|
65
|
-
.archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:
|
|
65
|
+
.archv-btn{appearance:none;min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-fill-subtle);color:var(--dsw-alias-label-secondary);border-radius:var(--dsm-radius-ctl);font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-align:center;transition:background-color .15s ease,border-color .15s ease,color .15s ease}
|
|
66
66
|
.archv-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
67
67
|
.archv-btn:disabled{opacity:.5;cursor:default}
|
|
68
68
|
.archv-del{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 45%,transparent)}
|
|
69
69
|
.archv-del:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
|
|
70
70
|
.archv-go{color:var(--dsw-alias-state-business-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 45%,transparent)}
|
|
71
71
|
.archv-go:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent);color:var(--dsw-alias-state-business-primary)}
|
|
72
|
-
.archv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:
|
|
72
|
+
.archv-empty{display:flex;align-items:center;gap:10px;padding:20px 14px;border:1px dashed var(--dsw-alias-border-l3);border-radius:var(--dsm-radius-card);color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
73
73
|
.archv-skel{display:flex;flex-direction:column;gap:8px}
|
|
74
|
-
.archv-skel-card{height:58px;border-radius:
|
|
74
|
+
.archv-skel-card{height:58px;border-radius:var(--dsm-radius-card);background:var(--dsw-alias-fill-subtle);position:relative;overflow:hidden}
|
|
75
75
|
.archv-skel-card::after{content:'';position:absolute;inset:0;transform:translateX(-100%);background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--dsw-alias-fill-elevated) 75%,transparent),transparent);animation:archv-shimmer 1.4s infinite}
|
|
76
76
|
.archv-status{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:60;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);padding:9px 16px;border-radius:999px;font-size:12px;box-shadow:0 8px 24px rgb(0 0 0/.18);display:flex;align-items:center;gap:8px;animation:archv-pop .18s ease-out;max-width:min(90vw,420px)}
|
|
77
77
|
.archv-spin{width:12px;height:12px;border:2px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 35%,transparent);border-top-color:var(--dsw-alias-label-secondary);border-radius:50%;animation:archv-rot .8s linear infinite;flex:none}
|
|
@@ -80,24 +80,24 @@ const CSS = `
|
|
|
80
80
|
@keyframes archv-pop{from{opacity:0;transform:translateX(-50%) translateY(10px)}}
|
|
81
81
|
@media (prefers-reduced-motion:reduce){.archv-skel-card::after{animation:none}.archv-card,.archv-btn{transition:none}.archv-status,.archv-spin{animation:none}}
|
|
82
82
|
@media (max-width:640px){.archv-card{flex-direction:column;align-items:stretch;gap:10px}.archv-actions{justify-content:flex-end}.sess-tools{grid-template-columns:1fr}.sess-fbtn,.archv-btn{min-height:40px}}
|
|
83
|
-
.mv-sheet{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
83
|
+
.mv-sheet{width:100%;box-sizing:border-box;display:flex;flex-direction:column;gap:12px;margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);background:var(--dsw-alias-fill-subtle)}
|
|
84
84
|
.mv-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
85
85
|
.mv-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
|
|
86
86
|
.mv-sheet-close{appearance:none;width:26px;height:26px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:7px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1}
|
|
87
87
|
.mv-sheet-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
88
|
-
.mv-seg{display:flex;gap:2px;padding:2px;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
88
|
+
.mv-seg{display:flex;gap:2px;padding:2px;background:var(--dsw-alias-fill-elevated);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);width:100%}
|
|
89
89
|
.mv-segbtn{appearance:none;flex:1;min-height:30px;padding:0 12px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px}
|
|
90
90
|
.mv-segbtn:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
91
91
|
.mv-segbtn-on{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px rgb(0 0 0/.08)}
|
|
92
92
|
.mv-field{display:flex;flex-direction:column;gap:6px}
|
|
93
93
|
.mv-field label.mv-field-label{font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary)}
|
|
94
|
-
.mv-field select,.mv-field input[type=text]{box-sizing:border-box;appearance:none;width:100%;min-height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
94
|
+
.mv-field select,.mv-field input[type=text]{box-sizing:border-box;appearance:none;width:100%;min-height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-ctl);background:var(--dsw-alias-fill-elevated);color:var(--dsw-alias-label-primary);font-size:12px;font-family:inherit}
|
|
95
95
|
.mv-field select:focus-visible,.mv-field input[type=text]:focus-visible,.mv-sheet-close:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
|
|
96
96
|
.mv-browse-row{display:flex;align-items:center;gap:8px}
|
|
97
97
|
.mv-browse-row input[type=text]{flex:1;min-width:0}
|
|
98
98
|
.mv-foot{display:flex;justify-content:flex-end;align-items:center;gap:8px;margin-top:2px}
|
|
99
99
|
@media (max-width:640px){.archv-row{flex-wrap:wrap}.mv-sheet{padding:12px}}
|
|
100
|
-
.dtl-sheet{margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:
|
|
100
|
+
.dtl-sheet{margin-top:12px;padding:14px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-sheet);background:var(--dsw-alias-fill-subtle)}
|
|
101
101
|
.dtl-sheet-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
102
102
|
.dtl-sheet-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}
|
|
103
103
|
.dtl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px;margin-top:10px}
|
|
@@ -118,7 +118,7 @@ const CSS = `
|
|
|
118
118
|
.more-wrap{position:relative;flex:none}
|
|
119
119
|
.more-btn{appearance:none;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;line-height:1}
|
|
120
120
|
.more-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
121
|
-
.more-menu{position:absolute;top:calc(100% + 4px);right:0;z-index:60;min-width:160px;padding:5px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:
|
|
121
|
+
.more-menu{position:absolute;top:calc(100% + 4px);right:0;z-index:60;min-width:160px;padding:5px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsm-radius-sheet);box-shadow:0 10px 32px rgb(0 0 0/.24);display:flex;flex-direction:column;gap:1px}
|
|
122
122
|
.more-item{appearance:none;display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:7px;font-size:12.5px;cursor:pointer;white-space:nowrap;text-align:left}
|
|
123
123
|
.more-item:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
124
124
|
.more-item-danger{color:var(--dsw-alias-state-error-primary)}
|
|
@@ -396,9 +396,14 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
396
396
|
const w = workspaces.find((x) => x.workspaceId === id)
|
|
397
397
|
return w ? w.path : ''
|
|
398
398
|
}
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
399
|
+
// 能力读取:优先 v3.5.2 规范键名,回退旧键名(旧 host / 缓存响应兼容)。
|
|
400
|
+
const actionCapability = (name, legacy) => {
|
|
401
|
+
const source = capabilities && capabilities.actions
|
|
402
|
+
return (source && (source[name] || (legacy && source[legacy]))) || { available: false, reason: '正在检查当前 DSH 的兼容能力…' }
|
|
403
|
+
}
|
|
404
|
+
const canPurge = actionCapability('physicalPurge', 'purge')
|
|
405
|
+
const canMove = actionCapability('relocateSession', 'move')
|
|
406
|
+
const canRestoreTrash = actionCapability('restoreIndexedSession', 'restoreTrash')
|
|
402
407
|
|
|
403
408
|
const archivedList = sessions ? sessions.filter((x) => x.archived) : []
|
|
404
409
|
const activeList = sessions ? sessions.filter((x) => !x.archived) : []
|
|
@@ -533,7 +538,12 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
533
538
|
if (trashBusy) return
|
|
534
539
|
setTrashBusy(sid)
|
|
535
540
|
postJSON('/archived-sessions/trash/restore', { sessionId: sid })
|
|
536
|
-
.then(() => {
|
|
541
|
+
.then((r) => {
|
|
542
|
+
setTrashBusy(null)
|
|
543
|
+
showToast(r && r.workspaceGone ? '已恢复会话(原工作区已删除,会话暂归「未分组」)' : '已恢复会话')
|
|
544
|
+
loadTrash()
|
|
545
|
+
refresh()
|
|
546
|
+
})
|
|
537
547
|
.catch((e) => { setTrashBusy(null); setError(String((e && e.message) || e)) })
|
|
538
548
|
}
|
|
539
549
|
const purgeTrash = (sid) => {
|
|
@@ -1052,9 +1062,14 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
1052
1062
|
</select>
|
|
1053
1063
|
</div>
|
|
1054
1064
|
<div className="sess-field"><label>数据检查</label><button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={verifyTrash}>{trashBusy === '__verify' ? '校验中…' : '校验日志完整性'}</button></div>
|
|
1055
|
-
<div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length || !canPurge.available} title={!canPurge.available ? canPurge.reason :
|
|
1065
|
+
<div className="sess-field"><label>永久清理</label><button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !trash.length || !canPurge.available} aria-disabled={!canPurge.available} title={!canPurge.available ? canPurge.reason : '物理删除这些日志,释放磁盘空间'} onClick={() => setPurgeTarget('__all')}>清空回收站</button></div>
|
|
1056
1066
|
</div>
|
|
1057
|
-
{!canPurge.available &&
|
|
1067
|
+
{!canPurge.available && (
|
|
1068
|
+
<div className="archv-empty" role="status">
|
|
1069
|
+
{canPurge.reason}。回收站条目会一直保留,可随时恢复;当前 DSH 版本下移入回收站<b>不会释放磁盘空间</b>,会话日志仍完整保留在原工作区目录。
|
|
1070
|
+
</div>
|
|
1071
|
+
)}
|
|
1072
|
+
<div className="dtl-note">回收站是软删除:日志仍留在原工作区目录,「彻底删除」才是真正释放磁盘空间的一步(当前版本未支持时会保持禁用)。</div>
|
|
1058
1073
|
{trashCheck && <div className={trashCheck.missing ? 'archv-err' : 'archv-empty'} role="status">校验完成:{trashCheck.healthy} 条正常,{trashCheck.missing} 条日志缺失,{trashCheck.unverified || 0} 条无法直接核验。</div>}
|
|
1059
1074
|
{trash.length === 0 ? (
|
|
1060
1075
|
<div className="dsm-trash-empty">回收站为空。删除的会话会先进入这里,可恢复或彻底删除。</div>
|
|
@@ -1065,7 +1080,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
1065
1080
|
<span className="dsm-trash-name" title={t.sessionId}>{t.title || t.sessionId}</span>
|
|
1066
1081
|
<span className="dsm-trash-date">{fmtDate(t.deletedAt)}</span>
|
|
1067
1082
|
<span className="dsm-trash-actions">
|
|
1068
|
-
<button type="button" className="archv-btn" disabled={trashBusy !== null} onClick={() => restoreTrash(t.sessionId)}>恢复</button>
|
|
1083
|
+
<button type="button" className="archv-btn" disabled={trashBusy !== null || !canRestoreTrash.available} title={!canRestoreTrash.available ? canRestoreTrash.reason : '恢复到删除前的位置与归档状态'} onClick={() => restoreTrash(t.sessionId)}>恢复</button>
|
|
1069
1084
|
<button type="button" className="archv-btn archv-del" disabled={trashBusy !== null || !canPurge.available} title={!canPurge.available ? canPurge.reason : undefined} onClick={() => setPurgeTarget(t.sessionId)}>彻底删除</button>
|
|
1070
1085
|
</span>
|
|
1071
1086
|
</div>
|
|
@@ -1077,7 +1092,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
1077
1092
|
<div className="dlg-backdrop" onClick={() => setPurgeTarget(null)}>
|
|
1078
1093
|
<div ref={dialogRef} className="dlg" role="alertdialog" aria-modal="true" aria-labelledby="dsm-purge-title" onClick={(e) => e.stopPropagation()}>
|
|
1079
1094
|
<h3 id="dsm-purge-title" className="dlg-title">永久删除{purgeTarget === '__all' ? '全部回收站会话' : '这个会话'}?</h3>
|
|
1080
|
-
<p className="dlg-text"
|
|
1095
|
+
<p className="dlg-text">此操作会物理删除日志并释放磁盘空间,无法恢复。归档、筛选和重新安装插件都不能找回这些数据。</p>
|
|
1081
1096
|
<div className="dlg-actions">
|
|
1082
1097
|
<button type="button" className="archv-btn" onClick={() => setPurgeTarget(null)}>取消</button>
|
|
1083
1098
|
<button type="button" className="archv-btn archv-del" onClick={() => { const target = purgeTarget; setPurgeTarget(null); target === '__all' ? purgeAllTrash() : purgeTrash(target) }}>确认永久删除</button>
|
|
@@ -1129,7 +1144,7 @@ const SIDEBAR_AUG_CSS = `
|
|
|
1129
1144
|
.dsm-sub-cur{font-size:11px;color:var(--dsw-alias-state-business-primary);flex:none;margin-left:8px}
|
|
1130
1145
|
.dsm-dot{position:absolute;left:6px;top:50%;transform:translateY(-50%);width:8px;height:8px;border-radius:50%;box-sizing:border-box;cursor:pointer;pointer-events:auto;z-index:1}
|
|
1131
1146
|
.dsm-dot-unread-manual{background:var(--dsw-alias-state-business-primary)}
|
|
1132
|
-
.dsm-dot-waiting{background:var(--dsw-alias-state-
|
|
1147
|
+
.dsm-dot-waiting{background:var(--dsw-alias-state-warn-primary,#F59E0B)}
|
|
1133
1148
|
.dsm-dot-unread{background:var(--dsw-alias-state-success-primary)}
|
|
1134
1149
|
.dsm-drag-source{opacity:.48}
|
|
1135
1150
|
.dsm-drop-target{position:relative;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 10%,transparent)!important;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px;border-radius:8px}
|
|
@@ -1193,41 +1208,91 @@ async function dsmLoadCapabilities() {
|
|
|
1193
1208
|
try { dsmCapabilities = await postJSON('/archived-sessions/capabilities', {}) } catch (e) { /* unknown stays safely unavailable */ }
|
|
1194
1209
|
return dsmCapabilities
|
|
1195
1210
|
}
|
|
1196
|
-
function dsmActionCapability(name) {
|
|
1197
|
-
|
|
1211
|
+
function dsmActionCapability(name, legacy) {
|
|
1212
|
+
const source = dsmCapabilities && dsmCapabilities.actions
|
|
1213
|
+
return source ? (source[name] || (legacy && source[legacy]) || null) : null
|
|
1198
1214
|
}
|
|
1199
1215
|
async function dsmLoadTrashIds() {
|
|
1200
1216
|
try {
|
|
1201
1217
|
const r = await postJSON('/archived-sessions/sidebar-state', {})
|
|
1202
1218
|
dsmTrashIds = new Set(((r && r.trashedSessionIds) || []).map(String))
|
|
1203
1219
|
dsmServerPurgedIds = new Set(((r && r.purgedSessionIds) || []).map(String))
|
|
1220
|
+
// 服务器已确认的乐观标记随即清除;未被确认的标记最多存活到下次刷新,
|
|
1221
|
+
// 绝不永久压住后来同 id 的新会话。
|
|
1222
|
+
for (const id of [...dsmPendingPurged]) {
|
|
1223
|
+
if (dsmServerPurgedIds.has(id)) dsmPendingPurged.delete(id)
|
|
1224
|
+
}
|
|
1204
1225
|
dsmAuthoritativeTitles = new Map(Object.entries((r && r.titles) || {}).map(([id, title]) => [String(id), String(title)]))
|
|
1205
1226
|
if (dsmRepaintDots) dsmRepaintDots()
|
|
1206
1227
|
} catch (e) { /* keep last known set */ }
|
|
1207
1228
|
return dsmTrashIds
|
|
1208
1229
|
}
|
|
1209
1230
|
|
|
1210
|
-
// Permanently-hidden set: sessions the user hard-purged from the 回收站.
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
//
|
|
1214
|
-
|
|
1215
|
-
|
|
1231
|
+
// Permanently-hidden set: sessions the user hard-purged from the 回收站. The
|
|
1232
|
+
// SERVER is the single source of truth: sidebar-state only reports tombstones
|
|
1233
|
+
// whose ids are NOT currently present, so a same-id session recreated later is
|
|
1234
|
+
// unhidden automatically (the tombstone must never permanently suppress a new
|
|
1235
|
+
// session). dsmPendingPurged only carries this tab's optimistic marks until
|
|
1236
|
+
// the next server fetch acks them — no localStorage persistence, so a stale
|
|
1237
|
+
// tombstone can never outlive its server record. (dsmServerPurgedIds is
|
|
1238
|
+
// declared with the trash-state group above.)
|
|
1239
|
+
const dsmPendingPurged = new Set()
|
|
1216
1240
|
function dsmLoadPurged() {
|
|
1217
|
-
|
|
1218
|
-
try { dsmPurgedIds = new Set(JSON.parse(localStorage.getItem(DSM_KEY_PURGED) || '[]')) } catch (e) { dsmPurgedIds = new Set() }
|
|
1219
|
-
}
|
|
1220
|
-
dsmServerPurgedIds.forEach((id) => dsmPurgedIds.add(id))
|
|
1221
|
-
return dsmPurgedIds
|
|
1241
|
+
return new Set([...dsmServerPurgedIds, ...dsmPendingPurged])
|
|
1222
1242
|
}
|
|
1223
|
-
function dsmSavePurged() { try { localStorage.setItem(DSM_KEY_PURGED, JSON.stringify([...dsmLoadPurged()])) } catch (e) {} }
|
|
1224
1243
|
function dsmMarkPurged(ids) {
|
|
1225
|
-
|
|
1226
|
-
ids.forEach((id) => s.add(String(id)))
|
|
1227
|
-
dsmSavePurged()
|
|
1244
|
+
ids.forEach((id) => dsmPendingPurged.add(String(id)))
|
|
1228
1245
|
if (dsmRepaintDots) dsmRepaintDots()
|
|
1229
1246
|
}
|
|
1230
1247
|
|
|
1248
|
+
// ---- Sidebar injection adapter (versioned, safe-degrading) ----------------
|
|
1249
|
+
// 每一处脆弱的 DOM / React fiber 识别都收敛到这个 adapter:识别规则集中、
|
|
1250
|
+
// 可版本化(上游改版时 bump version 并只改这里),并且**可安全降级**——
|
|
1251
|
+
// 识别连续落空说明上游内部结构变了,此时 adapter 自动停用全部注入,
|
|
1252
|
+
// 绝不把半残行为强加到官方侧栏上(拖拽 / 标题同步 / 状态圆点一并停用)。
|
|
1253
|
+
const SIDEBAR_ADAPTER_VERSION = 3
|
|
1254
|
+
const SIDEBAR_ADAPTER_MAX_MISSES = 200
|
|
1255
|
+
|
|
1256
|
+
function createSidebarAdapter() {
|
|
1257
|
+
let disabled = false
|
|
1258
|
+
let misses = 0
|
|
1259
|
+
const findFiber = (el) => {
|
|
1260
|
+
const k = Object.keys(el).find((kk) => kk.startsWith('__reactFiber') || kk.startsWith('__reactInternalInstance'))
|
|
1261
|
+
return k ? el[k] : null
|
|
1262
|
+
}
|
|
1263
|
+
// 识别一行侧栏树节点:返回 { node, group }(会话行 / 工作区标题行)。
|
|
1264
|
+
// 两种都没有 = 识别落空,累计到阈值即整体停用。
|
|
1265
|
+
const recognize = (row) => {
|
|
1266
|
+
const out = { node: null, group: null }
|
|
1267
|
+
if (disabled || !row || typeof row !== 'object') return out
|
|
1268
|
+
let f = findFiber(row)
|
|
1269
|
+
let guard = 0
|
|
1270
|
+
while (f && guard++ < 300) {
|
|
1271
|
+
const props = f.memoizedProps
|
|
1272
|
+
if (props && props.node && typeof props.node === 'object') out.node = props.node
|
|
1273
|
+
// DSH 的会话行接收 `node`,工作区标题行(ProjectRowItem)接收 `group`。
|
|
1274
|
+
if (props && props.group && typeof props.group === 'object') out.group = props.group
|
|
1275
|
+
f = f.return
|
|
1276
|
+
}
|
|
1277
|
+
if (!out.node && !out.group) {
|
|
1278
|
+
if (++misses >= SIDEBAR_ADAPTER_MAX_MISSES) {
|
|
1279
|
+
disabled = true
|
|
1280
|
+
try { console.warn('[dsh-sessions-manager] 侧栏注入 adapter v' + SIDEBAR_ADAPTER_VERSION + ' 已停用:未识别到已知的 DSH 侧栏节点结构(上游可能已改版)') } catch (e) {}
|
|
1281
|
+
}
|
|
1282
|
+
} else if (misses > 0) {
|
|
1283
|
+
misses = 0
|
|
1284
|
+
}
|
|
1285
|
+
return out
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
version: SIDEBAR_ADAPTER_VERSION,
|
|
1289
|
+
get disabled() { return disabled },
|
|
1290
|
+
findFiber,
|
|
1291
|
+
recognize,
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const sidebarAdapter = createSidebarAdapter()
|
|
1295
|
+
|
|
1231
1296
|
function installSidebarSessionMenuAug() {
|
|
1232
1297
|
if (typeof document === 'undefined') return
|
|
1233
1298
|
const AUG = 'data-dsm-aug'
|
|
@@ -1235,14 +1300,13 @@ function installSidebarSessionMenuAug() {
|
|
|
1235
1300
|
let activeSubClose = null
|
|
1236
1301
|
let hoverTimer = null
|
|
1237
1302
|
|
|
1238
|
-
const findFiber = (el) => {
|
|
1239
|
-
const k = Object.keys(el).find((kk) => kk.startsWith('__reactFiber') || kk.startsWith('__reactInternalInstance'))
|
|
1240
|
-
return k ? el[k] : null
|
|
1241
|
-
}
|
|
1242
1303
|
// Walk the React fiber chain from the portalled menu element up to the
|
|
1243
1304
|
// SessionNodeItem component, which carries `node.id` (the session id).
|
|
1305
|
+
// Portalled menus are one-off surfaces: fiber walking here does NOT count
|
|
1306
|
+
// toward the row adapter's miss budget (a non-session menu is normal, not a
|
|
1307
|
+
// sign of upstream drift).
|
|
1244
1308
|
const sessionInfoFromMenu = (menuEl) => {
|
|
1245
|
-
let f = findFiber(menuEl)
|
|
1309
|
+
let f = sidebarAdapter.findFiber(menuEl)
|
|
1246
1310
|
let guard = 0
|
|
1247
1311
|
while (f && guard++ < 300) {
|
|
1248
1312
|
const p = f.memoizedProps
|
|
@@ -1412,7 +1476,7 @@ function installSidebarSessionMenuAug() {
|
|
|
1412
1476
|
const move = mk('移动会话', ICON_MOVE, false)
|
|
1413
1477
|
const del = mk('删除会话', ICON_DEL, true)
|
|
1414
1478
|
const mark = mk(dsmLoadManual().has(info.id) ? '标记已读' : '标记未读', ICON_UNREAD, false)
|
|
1415
|
-
const moveCapability = dsmActionCapability('move')
|
|
1479
|
+
const moveCapability = dsmActionCapability('relocateSession') || dsmActionCapability('move')
|
|
1416
1480
|
if (!moveCapability || !moveCapability.available) {
|
|
1417
1481
|
move.btn.disabled = true
|
|
1418
1482
|
move.btn.title = (moveCapability && moveCapability.reason) || '正在检查当前版本的移动能力'
|
|
@@ -1450,6 +1514,7 @@ function installSidebarSessionMenuAug() {
|
|
|
1450
1514
|
|
|
1451
1515
|
const seen = new WeakSet()
|
|
1452
1516
|
const obs = new MutationObserver(() => {
|
|
1517
|
+
if (sidebarAdapter.disabled) return
|
|
1453
1518
|
const menus = document.querySelectorAll('body > [role="menu"]')
|
|
1454
1519
|
menus.forEach((menuEl) => {
|
|
1455
1520
|
if (seen.has(menuEl)) return
|
|
@@ -1465,7 +1530,7 @@ function installSidebarSessionMenuAug() {
|
|
|
1465
1530
|
// Left-side per-session status dot (DOM shim). The color is driven by DSH's
|
|
1466
1531
|
// REAL session status, read from the row's own StateDot ([data-state]):
|
|
1467
1532
|
// 手动标记未读(蓝) = 用户在 ⋯ 菜单或点击圆点手动标记,localStorage 持久化(最高优先级)
|
|
1468
|
-
// 工作中(黄) = DSH state 'running'
|
|
1533
|
+
// 工作中(黄) = DSH state 'ongoing'(旧版兼容 'running')
|
|
1469
1534
|
// 需用户反馈(琥珀) = DSH state 'warning'(有追问需用户反馈)
|
|
1470
1535
|
// 完成后未读(绿) = DSH state 'done' 且用户尚未读过(read 集合)
|
|
1471
1536
|
// 完成已读(不显示) = DSH state 'done' 且已读过(read 集合持久化)
|
|
@@ -1480,21 +1545,7 @@ function installSidebarStatusDots() {
|
|
|
1480
1545
|
s.textContent = SIDEBAR_AUG_CSS
|
|
1481
1546
|
document.head.appendChild(s)
|
|
1482
1547
|
}
|
|
1483
|
-
const
|
|
1484
|
-
const k = Object.keys(el).find((kk) => kk.startsWith('__reactFiber') || kk.startsWith('__reactInternalInstance'))
|
|
1485
|
-
return k ? el[k] : null
|
|
1486
|
-
}
|
|
1487
|
-
const fiberProp = (el, pred) => {
|
|
1488
|
-
let f = findFiber(el)
|
|
1489
|
-
let g = 0
|
|
1490
|
-
while (f && g++ < 300) {
|
|
1491
|
-
const p = f.memoizedProps
|
|
1492
|
-
if (p && pred(p)) return pred(p)
|
|
1493
|
-
f = f.return
|
|
1494
|
-
}
|
|
1495
|
-
return null
|
|
1496
|
-
}
|
|
1497
|
-
const rowNode = (row) => fiberProp(row, (p) => (p.node && typeof p.node.id === 'string' && p.node.id ? p.node : null))
|
|
1548
|
+
const rowNode = (row) => sidebarAdapter.recognize(row).node
|
|
1498
1549
|
const rowId = (row) => { const node = rowNode(row); return node ? node.id : null }
|
|
1499
1550
|
let curActive = null
|
|
1500
1551
|
const activeRowId = () => {
|
|
@@ -1503,7 +1554,7 @@ function installSidebarStatusDots() {
|
|
|
1503
1554
|
}
|
|
1504
1555
|
// Dot colors per the user's scheme. We read DSH's REAL session status from the
|
|
1505
1556
|
// row's own StateDot (its [data-state] attr) and recolor it:
|
|
1506
|
-
// running
|
|
1557
|
+
// ongoing/running → 黄 工作中
|
|
1507
1558
|
// warning → 琥珀 有追问需用户反馈
|
|
1508
1559
|
// done → 绿 完成后未读(读过则不再显示)
|
|
1509
1560
|
// error → 红 出错/需关注(与 DSH 自带 StateDot 一致)
|
|
@@ -1511,15 +1562,17 @@ function installSidebarStatusDots() {
|
|
|
1511
1562
|
const COLOR = {
|
|
1512
1563
|
manual: 'var(--dsw-alias-state-business-primary)', // 蓝 手动标记未读
|
|
1513
1564
|
running: '#EAB308', // 黄 工作中 (DSH running)
|
|
1514
|
-
feedback: 'var(--dsw-alias-state-
|
|
1565
|
+
feedback: 'var(--dsw-alias-state-warn-primary, #F59E0B)', // 琥珀 需用户反馈 (DSH warning)
|
|
1515
1566
|
done: 'var(--dsw-alias-state-success-primary)', // 绿 完成后未读 (DSH done)
|
|
1516
1567
|
error: 'var(--dsw-alias-state-error-primary)', // 红 出错/需关注 (DSH error)
|
|
1517
1568
|
}
|
|
1518
1569
|
const manualUnread = dsmLoadManual()
|
|
1570
|
+
const titleInitializedRows = new WeakSet()
|
|
1519
1571
|
const paint = () => {
|
|
1572
|
+
if (sidebarAdapter.disabled) return
|
|
1520
1573
|
// Recompute the active session on every paint so the active row is never
|
|
1521
1574
|
// shown from a stale `curActive` (the click that changes aria-selected
|
|
1522
|
-
// mutates the DOM and triggers paint immediately, before the
|
|
1575
|
+
// mutates the DOM and triggers paint immediately, before the tick()
|
|
1523
1576
|
// would have run — without this, the clicked session flashed green).
|
|
1524
1577
|
const activeId = activeRowId()
|
|
1525
1578
|
// Clicking into a manually-marked session auto-clears the manual unread
|
|
@@ -1549,12 +1602,20 @@ function installSidebarStatusDots() {
|
|
|
1549
1602
|
// Session is opened. Paint the latest log-folded title from the host
|
|
1550
1603
|
// authority without materializing the Session or changing its log.
|
|
1551
1604
|
const authoritativeTitle = dsmAuthoritativeTitles.get(id)
|
|
1552
|
-
if (
|
|
1605
|
+
if (!titleInitializedRows.has(row)) {
|
|
1553
1606
|
const node = rowNode(row) || {}
|
|
1554
1607
|
const expected = new Set([node.title, node.displayTitle, node.name].filter((value) => typeof value === 'string'))
|
|
1555
1608
|
const spans = [...row.children].filter((el) => el.tagName === 'SPAN' && !el.querySelector('[data-state]') && (el.textContent || '').trim())
|
|
1556
1609
|
const titleEl = spans.find((el) => expected.has((el.textContent || '').trim())) || spans[0]
|
|
1557
|
-
if (titleEl
|
|
1610
|
+
if (titleEl) {
|
|
1611
|
+
const correction = authoritativeTitleForFirstPaint({
|
|
1612
|
+
firstPaint: true,
|
|
1613
|
+
rendered: titleEl.textContent || '',
|
|
1614
|
+
authoritative: authoritativeTitle || '',
|
|
1615
|
+
})
|
|
1616
|
+
if (correction) titleEl.textContent = correction
|
|
1617
|
+
titleInitializedRows.add(row)
|
|
1618
|
+
}
|
|
1558
1619
|
}
|
|
1559
1620
|
// Read DSH's REAL session status from the row's own StateDot and recolor
|
|
1560
1621
|
// it with the user's scheme (we also hide DSH's dot so only ours shows).
|
|
@@ -1582,15 +1643,29 @@ function installSidebarStatusDots() {
|
|
|
1582
1643
|
})
|
|
1583
1644
|
}
|
|
1584
1645
|
dsmRepaintDots = paint
|
|
1585
|
-
|
|
1646
|
+
// MutationObserver 增量驱动为主(childList + 属性 + 文本),彻底替代高频率
|
|
1647
|
+
// 全表定时扫描;保留一个低频兜底 tick 处理无 DOM 变化的状态迁移,且后台
|
|
1648
|
+
// 标签页完全不跑。回收站/墓碑集合的刷新也挂在这个兜底上(每 8 拍 ≈ 32s)。
|
|
1586
1649
|
let raf = 0
|
|
1587
1650
|
const schedulePaint = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; paint() }) }
|
|
1651
|
+
const FALLBACK_TICK_MS = 4000
|
|
1652
|
+
const tick = () => {
|
|
1653
|
+
if (typeof document !== 'undefined' && document.hidden) return
|
|
1654
|
+
if (++dsmTrashTick % 8 === 0) dsmLoadTrashIds()
|
|
1655
|
+
paint()
|
|
1656
|
+
}
|
|
1588
1657
|
tick()
|
|
1589
1658
|
dsmLoadTrashIds()
|
|
1590
1659
|
paint()
|
|
1591
1660
|
const obs = new MutationObserver(schedulePaint)
|
|
1592
|
-
obs.observe(document.body, {
|
|
1593
|
-
|
|
1661
|
+
obs.observe(document.body, {
|
|
1662
|
+
childList: true,
|
|
1663
|
+
subtree: true,
|
|
1664
|
+
attributes: true,
|
|
1665
|
+
attributeFilter: ['data-state', 'aria-selected', 'class', 'style'],
|
|
1666
|
+
characterData: true,
|
|
1667
|
+
})
|
|
1668
|
+
setInterval(tick, FALLBACK_TICK_MS)
|
|
1594
1669
|
}
|
|
1595
1670
|
|
|
1596
1671
|
// Cross-workspace drag/drop for the native DSH sidebar. DSH does not expose a
|
|
@@ -1603,31 +1678,18 @@ function installSidebarWorkspaceDrag() {
|
|
|
1603
1678
|
let dragging = null
|
|
1604
1679
|
let moving = false
|
|
1605
1680
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
const out = []
|
|
1612
|
-
let fiber = findFiber(el)
|
|
1613
|
-
let guard = 0
|
|
1614
|
-
while (fiber && guard++ < 300) {
|
|
1615
|
-
const props = fiber.memoizedProps
|
|
1616
|
-
if (props && props.node && typeof props.node === 'object') out.push(props.node)
|
|
1617
|
-
// DSH's SessionNodeItem receives `node`, while ProjectRowItem (the
|
|
1618
|
-
// workspace heading) receives `group`. Without the latter every session
|
|
1619
|
-
// can start dragging but no workspace can ever become a valid drop zone.
|
|
1620
|
-
if (props && props.group && typeof props.group === 'object') out.push(props.group)
|
|
1621
|
-
fiber = fiber.return
|
|
1622
|
-
}
|
|
1623
|
-
return out
|
|
1681
|
+
// 识别统一走 sidebarAdapter(可版本化、可整体降级);adapter 停用后拖拽
|
|
1682
|
+
// 行为整体退出,不残留在官方侧栏上。
|
|
1683
|
+
const sessionForRow = (row) => {
|
|
1684
|
+
const { node, group } = sidebarAdapter.recognize(row)
|
|
1685
|
+
return sessionForNodes(group ? [node, group].filter(Boolean) : [node], sessions)
|
|
1624
1686
|
}
|
|
1625
|
-
const sessionForRow = (row) => sessionForNodes(fiberNodes(row), sessions)
|
|
1626
1687
|
const workspaceForRow = (row) => {
|
|
1627
1688
|
// A session row's fiber chain also contains its parent workspace node.
|
|
1628
1689
|
// Reject it explicitly so only the visible workspace header is a drop zone.
|
|
1629
1690
|
if (sessionForRow(row)) return null
|
|
1630
|
-
|
|
1691
|
+
const { group } = sidebarAdapter.recognize(row)
|
|
1692
|
+
return group ? workspaceForNodes([group]) : null
|
|
1631
1693
|
}
|
|
1632
1694
|
const eventRow = (event) => {
|
|
1633
1695
|
for (const item of event.composedPath ? event.composedPath() : []) {
|
|
@@ -1647,6 +1709,7 @@ function installSidebarWorkspaceDrag() {
|
|
|
1647
1709
|
document.querySelectorAll('.dsm-drag-source,.dsm-drop-target').forEach((el) => el.classList.remove('dsm-drag-source', 'dsm-drop-target'))
|
|
1648
1710
|
}
|
|
1649
1711
|
const decorateRows = () => {
|
|
1712
|
+
if (sidebarAdapter.disabled) return
|
|
1650
1713
|
document.querySelectorAll('[role="treeitem"]').forEach((row) => {
|
|
1651
1714
|
const session = sessionForRow(row)
|
|
1652
1715
|
const ws = workspaceForRow(row)
|
|
@@ -1679,7 +1742,8 @@ function installSidebarWorkspaceDrag() {
|
|
|
1679
1742
|
}, true)
|
|
1680
1743
|
|
|
1681
1744
|
document.addEventListener('dragstart', (event) => {
|
|
1682
|
-
|
|
1745
|
+
if (sidebarAdapter.disabled) return
|
|
1746
|
+
const moveCapability = dsmActionCapability('relocateSession') || dsmActionCapability('move')
|
|
1683
1747
|
if (!moveCapability || !moveCapability.available) return
|
|
1684
1748
|
const row = eventRow(event)
|
|
1685
1749
|
const item = row && sessionForRow(row)
|
package/src/client/logic.js
CHANGED
|
@@ -7,17 +7,29 @@
|
|
|
7
7
|
// 状态点语义:manual 未读最高优先;done 在当前查看的行上不亮绿
|
|
8
8
|
// (读过即视为已读)。返回逻辑态名,颜色映射留在 UI 层。
|
|
9
9
|
// 历史回归:DSH 曾把 running 报为 ongoing(9766476),上游枚举变化要盯这里。
|
|
10
|
+
// issue #4:上游还存在 waiting / needs-attention 等同义词,这里收敛到同一
|
|
11
|
+
// 逻辑态,颜色映射仍由 UI 层统一处理(issue 保持 open 直到上游确认)。
|
|
10
12
|
export function dotStateFor({ manualUnread = false, dataState = '', isActive = false } = {}) {
|
|
11
13
|
if (manualUnread) return 'manual'
|
|
12
14
|
switch (dataState) {
|
|
15
|
+
case 'ongoing':
|
|
13
16
|
case 'running': return 'running'
|
|
14
|
-
case 'warning':
|
|
17
|
+
case 'warning':
|
|
18
|
+
case 'waiting':
|
|
19
|
+
case 'needs-attention': return 'feedback'
|
|
15
20
|
case 'error': return 'error'
|
|
16
21
|
case 'done': return isActive ? null : 'done'
|
|
17
22
|
default: return null
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
// 权威标题只修正新出现 DOM 行的冷态标题。行一旦完成首次 paint,后续文本
|
|
27
|
+
// 变化属于 DSH Core 的实时更新(例如重命名),插件不得再用轮询快照覆盖。
|
|
28
|
+
export function authoritativeTitleForFirstPaint({ firstPaint = false, rendered = '', authoritative = '' } = {}) {
|
|
29
|
+
if (!firstPaint || !authoritative || rendered === authoritative) return null
|
|
30
|
+
return authoritative
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
// 拖拽迁移前置校验:同工作区拦截(workspacePath 相等即拒绝)。
|
|
22
34
|
// 无 workspacePath 的会话(如侧栏 live 行尚未同步)放行,由 host 最终裁决。
|
|
23
35
|
export function canDropOnWorkspace(item, target) {
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
// Capability matrix for the two DSH persistence generations. Every user-visible
|
|
2
|
+
// action gets its own availability flag plus a Chinese reason, so the UI can
|
|
3
|
+
// disable buttons honestly and the host routes can refuse with a stable error.
|
|
4
|
+
//
|
|
5
|
+
// Guiding rule (revised 2026-09-06 by explicit user decision): the official
|
|
6
|
+
// SessionHandle-era contract (dsh-v0.1.3-alpha.1) exposes NO delete and NO
|
|
7
|
+
// relocation — but legacy implementations were never pure-public either (they
|
|
8
|
+
// used the official `locate` to find the path, then acted on the filesystem
|
|
9
|
+
// directly). The user chose to keep that semi-official approach in the handle
|
|
10
|
+
// era: private-path operations ARE allowed, but only through the guarded
|
|
11
|
+
// derivation in src/handle-era-paths.js (backend root instance field → session
|
|
12
|
+
// directory structure → id ownership) plus handle-era-ops.js (writer probe,
|
|
13
|
+
// backup + rollback). Any derivation failure degrades to "unavailable".
|
|
14
|
+
//
|
|
15
|
+
// Action vocabulary (canonical):
|
|
16
|
+
// readInspection read/list/stat the stored log (read-only)
|
|
17
|
+
// archive archive/unarchive via workspaceRegistry (a marking op)
|
|
18
|
+
// softTrash move a session into the plugin recycle bin (log stays put)
|
|
19
|
+
// restoreIndexedSession restore a trashed session after verifying the
|
|
20
|
+
// underlying stored session still exists
|
|
21
|
+
// physicalPurge irreversibly delete the stored log (guarded fs rm)
|
|
22
|
+
// relocateSession move a session across workspaces (changes header cwd)
|
|
23
|
+
//
|
|
24
|
+
// Legacy aliases (read / trash / restoreTrash / purge / move) are kept so the
|
|
25
|
+
// client and older routes keep working during the transition.
|
|
26
|
+
|
|
1
27
|
function action(available, reason = null) {
|
|
2
28
|
return { available: !!available, reason: available ? null : reason }
|
|
3
29
|
}
|
|
@@ -7,20 +33,50 @@ export function detectCapabilities({ persistence, workspaceRegistry }) {
|
|
|
7
33
|
const legacyRead = !!(persistence && typeof persistence.readFrom === 'function')
|
|
8
34
|
const legacyLocate = !!(persistence && (typeof persistence.locate === 'function'
|
|
9
35
|
|| (persistence.backend && typeof persistence.backend.locate === 'function')))
|
|
36
|
+
// handle 时代:root 必须直接读自后端实例字段(session-persistence-jsonl 的
|
|
37
|
+
// `root`),拿不到整体降级——绝不猜路径。
|
|
38
|
+
const handleEraRoot = !!(handleApi && persistence
|
|
39
|
+
&& typeof persistence.root === 'string' && persistence.root.length > 0)
|
|
40
|
+
const canVerifyExistence = !!(handleApi || legacyRead
|
|
41
|
+
|| (persistence && typeof persistence.stat === 'function'))
|
|
42
|
+
const readOk = legacyRead || handleApi
|
|
10
43
|
const workspaceInternals = !!(workspaceRegistry
|
|
11
44
|
&& workspaceRegistry.headers && workspaceRegistry.sessionPaths
|
|
12
45
|
&& typeof workspaceRegistry.replaceHeaderIndex === 'function')
|
|
13
46
|
|
|
47
|
+
const matrix = {
|
|
48
|
+
readInspection: action(readOk, '当前 DSH 未提供可识别的会话读取接口'),
|
|
49
|
+
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === 'function'), '当前 DSH 未提供归档接口'),
|
|
50
|
+
softTrash: action(readOk, '当前 DSH 无法读取会话,不能安全移入回收站'),
|
|
51
|
+
// 恢复不再无条件宣称可用:必须能校验底层会话仍存在(stat 或 list),
|
|
52
|
+
// 否则恢复只会制造一条指向已消失日志的僵尸条目。
|
|
53
|
+
restoreIndexedSession: action(canVerifyExistence, '当前 DSH 无法校验底层会话是否存在,不能安全恢复'),
|
|
54
|
+
physicalPurge: action(
|
|
55
|
+
(!handleApi && legacyLocate) || (handleApi && handleEraRoot && canVerifyExistence),
|
|
56
|
+
handleApi && !handleEraRoot
|
|
57
|
+
? '无法从当前 DSH 后端确认会话存储根目录,已停止物理删除以保护数据安全'
|
|
58
|
+
: '当前 DSH 版本尚未提供经过验证的安全永久删除能力;移入回收站不会释放磁盘空间',
|
|
59
|
+
),
|
|
60
|
+
relocateSession: action(
|
|
61
|
+
(!handleApi && legacyRead && legacyLocate && workspaceInternals)
|
|
62
|
+
|| (handleApi && handleEraRoot && readOk && workspaceInternals),
|
|
63
|
+
handleApi && !workspaceInternals
|
|
64
|
+
? '当前 DSH 未提供工作区注册表内部结构,跨工作区移动后无法即时刷新分组'
|
|
65
|
+
: handleApi && !handleEraRoot
|
|
66
|
+
? '无法从当前 DSH 后端确认会话存储根目录,已停止移动以保护数据安全'
|
|
67
|
+
: '当前 DSH 版本尚未提供经过验证的跨工作区迁移能力',
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
// Legacy aliases for existing client/routes/tests.
|
|
71
|
+
matrix.read = matrix.readInspection
|
|
72
|
+
matrix.trash = matrix.softTrash
|
|
73
|
+
matrix.restoreTrash = matrix.restoreIndexedSession
|
|
74
|
+
matrix.purge = matrix.physicalPurge
|
|
75
|
+
matrix.move = matrix.relocateSession
|
|
76
|
+
|
|
14
77
|
return {
|
|
15
78
|
persistence: handleApi ? 'session-handle' : 'legacy',
|
|
16
|
-
actions:
|
|
17
|
-
read: action(legacyRead || handleApi, '当前 DSH 未提供可识别的会话读取接口'),
|
|
18
|
-
archive: action(!!(workspaceRegistry && typeof workspaceRegistry.archiveSession === 'function'), '当前 DSH 未提供归档接口'),
|
|
19
|
-
trash: action(legacyRead || handleApi, '当前 DSH 无法读取会话,不能安全移入回收站'),
|
|
20
|
-
restoreTrash: action(true),
|
|
21
|
-
purge: action(!handleApi && legacyLocate, '当前 DSH 版本尚未提供经过验证的安全永久删除能力'),
|
|
22
|
-
move: action(!handleApi && legacyRead && legacyLocate && workspaceInternals, '当前 DSH 版本尚未提供经过验证的跨工作区迁移能力'),
|
|
23
|
-
},
|
|
79
|
+
actions: matrix,
|
|
24
80
|
}
|
|
25
81
|
}
|
|
26
82
|
|