dsh-sessions-manager 3.3.0 → 3.4.0
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 +21 -21
- package/README.md +21 -21
- package/assets/screenshot-session-autoarch.png +0 -0
- package/assets/screenshot-session-details.png +0 -0
- package/assets/screenshot-session-settings.png +0 -0
- package/assets/screenshot-session-starred.png +0 -0
- package/assets/screenshot-session-storage.png +0 -0
- package/assets/screenshot-session-trash.png +0 -0
- package/lib/client.js +161 -4
- package/lib/client.js.map +2 -2
- package/lib/index.js +305 -25
- package/lib/index.js.map +4 -4
- package/package.json +1 -1
- package/src/auto-archive.js +168 -0
- package/src/client/index.jsx +177 -5
- package/src/index.js +161 -5
- package/src/storage-stats.js +94 -0
- package/assets/screenshot-session-settingsmenu.png +0 -0
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.
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Durable "auto-archive" settings (schema v4) + the pure candidate rule.
|
|
2
|
+
//
|
|
3
|
+
// Auto-archive hides conversations that have been idle for N days. It is OFF
|
|
4
|
+
// by default: archiving rewrites durable workspace state, so the plugin must
|
|
5
|
+
// never touch a conversation the user has not asked it to.
|
|
6
|
+
//
|
|
7
|
+
// Deliberately mirrors the star index (src/star-index.js): version field,
|
|
8
|
+
// defensive coercion of whatever is on disk, atomic write (tmp + rename) and a
|
|
9
|
+
// single chained mutation queue. The candidate rule lives here as a pure
|
|
10
|
+
// function so it can be tested without a DSH host.
|
|
11
|
+
import { mkdir, rename, writeFile } from 'node:fs/promises'
|
|
12
|
+
import { readFileSync } from 'node:fs'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
|
|
16
|
+
// v4 keeps clear of the recycle bin's v1/v2 and the star index's v3, so a
|
|
17
|
+
// copied or mixed-up file can never be silently accepted as another store.
|
|
18
|
+
export const AUTO_ARCHIVE_SCHEMA_VERSION = 4
|
|
19
|
+
|
|
20
|
+
// Allowed idle windows. 0 = disabled. Deliberately coarse: a free-form number
|
|
21
|
+
// would let a typo schedule archiving "tomorrow" for every conversation.
|
|
22
|
+
export const INACTIVE_DAY_OPTIONS = Object.freeze([0, 30, 60, 90])
|
|
23
|
+
|
|
24
|
+
const DAY_MS = 86400000
|
|
25
|
+
// Re-run at most once per day: the sweep is triggered by panel reads, and a
|
|
26
|
+
// user flipping settings back and forth must not archive in a loop.
|
|
27
|
+
export const RUN_INTERVAL_MS = DAY_MS
|
|
28
|
+
|
|
29
|
+
const DEFAULT_DIR = join(homedir(), '.dsh', 'sessions-manager')
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Coerce anything on disk (or nothing at all) into a valid v4 store.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeAutoArchiveStore(raw) {
|
|
35
|
+
const source = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
|
36
|
+
const settings = source.settings && typeof source.settings === 'object' ? source.settings : {}
|
|
37
|
+
const inactiveDays = INACTIVE_DAY_OPTIONS.includes(settings.inactiveDays) ? settings.inactiveDays : 0
|
|
38
|
+
return {
|
|
39
|
+
schemaVersion: AUTO_ARCHIVE_SCHEMA_VERSION,
|
|
40
|
+
settings: {
|
|
41
|
+
inactiveDays,
|
|
42
|
+
// Starred sessions are an explicit "keep" mark, so they are skipped
|
|
43
|
+
// unless the user opts out.
|
|
44
|
+
skipStarred: settings.skipStarred !== false,
|
|
45
|
+
},
|
|
46
|
+
lastRunAt: Number.isFinite(source.lastRunAt) ? source.lastRunAt : null,
|
|
47
|
+
lastArchivedCount: Number.isInteger(source.lastArchivedCount) && source.lastArchivedCount >= 0 ? source.lastArchivedCount : 0,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Which sessions should be auto-archived right now? Pure — no I/O, no host.
|
|
53
|
+
*
|
|
54
|
+
* The rule is intentionally conservative: anything we cannot prove is idle
|
|
55
|
+
* (unknown last-activity, already archived, starred, currently open) is left
|
|
56
|
+
* alone. A wrong archive is a visible regression; a missed one is invisible.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array<{sessionId: string, archived?: boolean, starred?: boolean,
|
|
59
|
+
* updatedAt?: number|null}>} items
|
|
60
|
+
* @param {object} options
|
|
61
|
+
* @param {number} options.inactiveDays - Idle window in days (0 disables).
|
|
62
|
+
* @param {number} [options.now] - Reference timestamp (tests inject it).
|
|
63
|
+
* @param {boolean} [options.skipStarred=true] - Keep starred sessions.
|
|
64
|
+
* @param {string|null} [options.activeSessionId] - Never archive the open one.
|
|
65
|
+
* @returns {string[]} Session ids to archive.
|
|
66
|
+
*/
|
|
67
|
+
export function pickInactiveCandidates(items, options = {}) {
|
|
68
|
+
const days = options.inactiveDays
|
|
69
|
+
if (!INACTIVE_DAY_OPTIONS.includes(days) || days === 0) return []
|
|
70
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now()
|
|
71
|
+
const cutoff = now - days * DAY_MS
|
|
72
|
+
const skipStarred = options.skipStarred !== false
|
|
73
|
+
const activeId = options.activeSessionId != null ? String(options.activeSessionId) : null
|
|
74
|
+
const list = Array.isArray(items) ? items : []
|
|
75
|
+
|
|
76
|
+
const out = []
|
|
77
|
+
const seen = new Set()
|
|
78
|
+
for (const item of list) {
|
|
79
|
+
if (!item || item.sessionId == null) continue
|
|
80
|
+
const id = String(item.sessionId)
|
|
81
|
+
if (seen.has(id)) continue
|
|
82
|
+
if (item.archived) continue
|
|
83
|
+
if (skipStarred && item.starred) continue
|
|
84
|
+
if (activeId !== null && id === activeId) continue
|
|
85
|
+
const updatedAt = Number(item.updatedAt)
|
|
86
|
+
// No usable timestamp → cannot prove it is idle → leave it alone.
|
|
87
|
+
if (!Number.isFinite(updatedAt) || updatedAt <= 0) continue
|
|
88
|
+
if (updatedAt < cutoff) { seen.add(id); out.push(id) }
|
|
89
|
+
}
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Open the auto-archive settings store.
|
|
95
|
+
* @param {object} [options]
|
|
96
|
+
* @param {string} [options.dir] - Directory holding the index (tests inject a temp dir).
|
|
97
|
+
* @param {string} [options.indexPath] - Full index path, overriding `dir`.
|
|
98
|
+
*/
|
|
99
|
+
export function createAutoArchiveStore(options = {}) {
|
|
100
|
+
const dir = options.dir || process.env.DSH_SESSIONS_MANAGER_AUTO_ARCHIVE_DIR || DEFAULT_DIR
|
|
101
|
+
const indexPath = options.indexPath || join(dir, 'auto-archive.json')
|
|
102
|
+
let mutation = Promise.resolve()
|
|
103
|
+
|
|
104
|
+
async function read() {
|
|
105
|
+
try {
|
|
106
|
+
return normalizeAutoArchiveStore(JSON.parse(readFileSync(indexPath, 'utf8')))
|
|
107
|
+
} catch {
|
|
108
|
+
return normalizeAutoArchiveStore(null)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function write(store) {
|
|
113
|
+
await mkdir(dir, { recursive: true })
|
|
114
|
+
const tmp = join(dir, `.auto-archive-${process.pid}-${Date.now()}.tmp`)
|
|
115
|
+
await writeFile(tmp, JSON.stringify(normalizeAutoArchiveStore(store), null, 2), { encoding: 'utf8', mode: 0o600 })
|
|
116
|
+
await rename(tmp, indexPath)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mutate(mutator) {
|
|
120
|
+
const operation = mutation.then(async () => {
|
|
121
|
+
const store = await read()
|
|
122
|
+
const result = await mutator(store)
|
|
123
|
+
await write(store)
|
|
124
|
+
return result
|
|
125
|
+
})
|
|
126
|
+
mutation = operation.catch(() => {})
|
|
127
|
+
return operation
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merge a partial settings patch.
|
|
132
|
+
* @param {{inactiveDays?: number, skipStarred?: boolean}} patch
|
|
133
|
+
* @returns {Promise<object>} The store's settings after the change.
|
|
134
|
+
*/
|
|
135
|
+
function update(patch = {}) {
|
|
136
|
+
return mutate((store) => {
|
|
137
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'inactiveDays')) {
|
|
138
|
+
const days = Number(patch.inactiveDays)
|
|
139
|
+
if (!INACTIVE_DAY_OPTIONS.includes(days)) {
|
|
140
|
+
const error = new Error(`inactiveDays 仅支持 ${INACTIVE_DAY_OPTIONS.join('、')}`)
|
|
141
|
+
error.status = 400
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
store.settings.inactiveDays = days
|
|
145
|
+
}
|
|
146
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'skipStarred')) {
|
|
147
|
+
store.settings.skipStarred = !!patch.skipStarred
|
|
148
|
+
}
|
|
149
|
+
return store.settings
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Record that a sweep ran, so the once-a-day throttle can skip the next one. */
|
|
154
|
+
function recordRun(count, at = Date.now()) {
|
|
155
|
+
return mutate((store) => {
|
|
156
|
+
store.lastRunAt = at
|
|
157
|
+
store.lastArchivedCount = Number.isInteger(count) && count >= 0 ? count : 0
|
|
158
|
+
return store
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** True when a sweep already ran within RUN_INTERVAL_MS. */
|
|
163
|
+
function isFresh(store, now = Date.now()) {
|
|
164
|
+
return Number.isFinite(store && store.lastRunAt) && (now - store.lastRunAt) < RUN_INTERVAL_MS
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { read, write, mutate, update, recordRun, isFresh, indexPath, dir }
|
|
168
|
+
}
|
package/src/client/index.jsx
CHANGED
|
@@ -220,6 +220,8 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
220
220
|
const [sessions, setSessions] = useState(null)
|
|
221
221
|
const [workspaces, setWorkspaces] = useState([])
|
|
222
222
|
const initialPrefs = useRef(loadPanelPrefs()).current
|
|
223
|
+
// 注意:'storage' 已从可持久化取值中移除(它不再是视图),旧版残留的
|
|
224
|
+
// filter='storage' 会自动回落到 'all',避免落到一个已不存在的界面。
|
|
223
225
|
const [filter, setFilter] = useState(() => ['all', 'active', 'archived', 'starred', 'trash'].includes(initialPrefs.filter) ? initialPrefs.filter : 'all')
|
|
224
226
|
const [query, setQuery] = useState('')
|
|
225
227
|
const [workspaceFilter, setWorkspaceFilter] = useState(() => initialPrefs.workspaceFilter || 'all')
|
|
@@ -245,9 +247,21 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
245
247
|
const [detailsLoading, setDetailsLoading] = useState(null)
|
|
246
248
|
const [mdBusy, setMdBusy] = useState(null)
|
|
247
249
|
const [zipOk, setZipOk] = useState(true)
|
|
250
|
+
const [storage, setStorage] = useState(null)
|
|
251
|
+
const [storageBusy, setStorageBusy] = useState(false)
|
|
252
|
+
// 存储统计要 stat 每条会话日志,开销不小:面板默认收起、按需加载,
|
|
253
|
+
// 且展开状态刻意不持久化——否则每次打开设置面板都会触发一次全量扫描。
|
|
254
|
+
const [storageOpen, setStorageOpen] = useState(false)
|
|
255
|
+
const [storageError, setStorageError] = useState(null)
|
|
256
|
+
const [aa, setAa] = useState({ settings: { inactiveDays: 0, skipStarred: true }, lastRunAt: null, lastArchivedCount: 0 })
|
|
257
|
+
const [aaOpen, setAaOpen] = useState(false)
|
|
258
|
+
const [aaBusy, setAaBusy] = useState(false)
|
|
248
259
|
const zipChecked = useRef(false)
|
|
249
260
|
const [openMenu, setOpenMenu] = useState(null)
|
|
250
261
|
const timer = useRef(null)
|
|
262
|
+
// 存储面板关着时会话集合若发生变化,标脏;下次展开时再刷新,
|
|
263
|
+
// 既不会显示过期数字,也避免每次 refresh 都白扫一遍全量日志。
|
|
264
|
+
const storageDirty = useRef(false)
|
|
251
265
|
const menuRef = useRef(null)
|
|
252
266
|
const dialogRef = useRef(null)
|
|
253
267
|
|
|
@@ -275,6 +289,9 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
275
289
|
setConfirmBatch(false)
|
|
276
290
|
if (!targetWs && works.items && works.items.length) setTargetWs(works.items[0].workspaceId)
|
|
277
291
|
loadTrash()
|
|
292
|
+
// 会话集合变了:面板开着就同步刷新;关着则只标脏,等展开时再刷。
|
|
293
|
+
if (storageOpen) loadStorage()
|
|
294
|
+
else storageDirty.current = true
|
|
278
295
|
})
|
|
279
296
|
.catch((e) => setError(String((e && e.message) || e)))
|
|
280
297
|
}
|
|
@@ -289,6 +306,13 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
289
306
|
try { localStorage.setItem(PANEL_PREFS_KEY, JSON.stringify({ filter, workspaceFilter, sortBy })) } catch (e) {}
|
|
290
307
|
}, [filter, workspaceFilter, sortBy])
|
|
291
308
|
|
|
309
|
+
// 自动归档设置随面板加载一次(host 侧读取即触发每日检查)。
|
|
310
|
+
// 存储统计刻意不在这里预取:它要 stat 每条日志,只在用户展开面板时才算。
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
loadAutoArchive()
|
|
313
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
314
|
+
}, [])
|
|
315
|
+
|
|
292
316
|
// 收藏切换:乐观更新 + 失败回滚(star 是高频轻操作,不等网络往返)。
|
|
293
317
|
const toggleStar = async (it) => {
|
|
294
318
|
const next = !it.starred
|
|
@@ -389,6 +413,8 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
389
413
|
})
|
|
390
414
|
}, [sessions, filter, query, workspaceFilter, sortBy])
|
|
391
415
|
const selIds = Object.keys(selected).filter((k) => selected[k])
|
|
416
|
+
// 「回收站」是独立视图,不共用会话列表。
|
|
417
|
+
const showSessionList = filter !== 'trash'
|
|
392
418
|
|
|
393
419
|
const toggle = (id) => setSelected((s) => ({ ...s, [id]: !s[id] }))
|
|
394
420
|
const clearSel = () => setSelected({})
|
|
@@ -445,6 +471,47 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
445
471
|
.catch((e) => { setTrashBusy(null); setError(String((e && e.message) || e)) })
|
|
446
472
|
}
|
|
447
473
|
|
|
474
|
+
// 存储占用分析:只读聚合(按工作区排行 + 最大的会话)。按需调用,不在面板加载时预取。
|
|
475
|
+
const loadStorage = () => {
|
|
476
|
+
setStorageBusy(true)
|
|
477
|
+
setStorageError(null)
|
|
478
|
+
postJSON('/archived-sessions/storage', { topN: 10 })
|
|
479
|
+
.then((r) => { setStorageBusy(false); setStorage(r); storageDirty.current = false })
|
|
480
|
+
// 错误留在面板内自行重试,不冒泡成整个设置面板的错误条。
|
|
481
|
+
.catch((e) => { setStorageBusy(false); setStorageError(String((e && e.message) || e)) })
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 自动归档设置。一次纯读取会顺带让 host 跑一遍每日检查(host 侧按天节流)。
|
|
485
|
+
const applyAa = (r) => setAa({ settings: r.settings || { inactiveDays: 0, skipStarred: true }, lastRunAt: r.lastRunAt ?? null, lastArchivedCount: r.lastArchivedCount || 0 })
|
|
486
|
+
|
|
487
|
+
const loadAutoArchive = () => {
|
|
488
|
+
postJSON('/archived-sessions/auto-archive/settings', {})
|
|
489
|
+
.then(applyAa)
|
|
490
|
+
.catch(() => {})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const updateAutoArchive = (patch) => {
|
|
494
|
+
if (aaBusy) return
|
|
495
|
+
setAaBusy(true)
|
|
496
|
+
postJSON('/archived-sessions/auto-archive/settings', patch)
|
|
497
|
+
.then((r) => { setAaBusy(false); applyAa(r); showToast('已更新自动归档策略') })
|
|
498
|
+
.catch((e) => { setAaBusy(false); setError(String((e && e.message) || e)) })
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const runAutoArchive = () => {
|
|
502
|
+
if (aaBusy) return
|
|
503
|
+
setAaBusy(true)
|
|
504
|
+
postJSON('/archived-sessions/auto-archive/run', {})
|
|
505
|
+
.then((r) => {
|
|
506
|
+
setAaBusy(false)
|
|
507
|
+
applyAa(r)
|
|
508
|
+
const n = r.archived || 0
|
|
509
|
+
if (r.skipped === 'disabled') showToast('自动归档未启用')
|
|
510
|
+
else { showToast(`已自动归档 ${n} 个会话`); if (n > 0) refresh() }
|
|
511
|
+
})
|
|
512
|
+
.catch((e) => { setAaBusy(false); setError(String((e && e.message) || e)) })
|
|
513
|
+
}
|
|
514
|
+
|
|
448
515
|
const restoreTrash = (sid) => {
|
|
449
516
|
if (trashBusy) return
|
|
450
517
|
setTrashBusy(sid)
|
|
@@ -632,7 +699,97 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
632
699
|
<button type="button" role="tab" aria-selected={filter === 'trash'} className={'sess-fbtn' + (filter === 'trash' ? ' sess-fbtn-on' : '')} onClick={() => { setFilter('trash'); clearSel(); setConfirmBatch(false) }}>回收站 ({trash.length})</button>
|
|
633
700
|
</div>
|
|
634
701
|
|
|
635
|
-
{
|
|
702
|
+
{/* 维护栏是面板级工具,与当前查看哪一组会话无关,故所有视图都显示。 */}
|
|
703
|
+
<div className="maint-bar">
|
|
704
|
+
<button type="button" className={'archv-btn' + (aa.settings.inactiveDays ? ' archv-go' : '')} aria-expanded={aaOpen} onClick={() => setAaOpen(!aaOpen)}>
|
|
705
|
+
自动归档{aa.settings.inactiveDays ? `:${aa.settings.inactiveDays} 天未活跃` : ':未启用'}
|
|
706
|
+
</button>
|
|
707
|
+
{aa.lastRunAt ? <span className="maint-note">上次检查 {fmtDate(aa.lastRunAt)},归档 {aa.lastArchivedCount} 个</span> : <span className="maint-note">尚未检查</span>}
|
|
708
|
+
<button type="button" className="archv-btn maint-bar-right" aria-expanded={storageOpen} onClick={() => { const next = !storageOpen; setStorageOpen(next); if (next && (!storage || storageDirty.current) && !storageBusy) loadStorage() }}>
|
|
709
|
+
存储占用{storage ? ` · ${fmtBytes(storage.totalBytes) || '0 B'}` : ''}
|
|
710
|
+
</button>
|
|
711
|
+
</div>
|
|
712
|
+
{aaOpen && (
|
|
713
|
+
<div className="mv-sheet" aria-label="自动归档设置">
|
|
714
|
+
<div className="mv-sheet-head">
|
|
715
|
+
<h3 className="mv-sheet-title">自动归档</h3>
|
|
716
|
+
<button type="button" className="mv-sheet-close" aria-label="关闭" onClick={() => setAaOpen(false)}>×</button>
|
|
717
|
+
</div>
|
|
718
|
+
<div className="mv-field">
|
|
719
|
+
<label className="mv-field-label" htmlFor="dsm-aa-days">将多久未活跃的会话自动归档</label>
|
|
720
|
+
<select id="dsm-aa-days" value={aa.settings.inactiveDays} disabled={aaBusy} onChange={(e) => updateAutoArchive({ inactiveDays: Number(e.target.value) })}>
|
|
721
|
+
<option value="0">不自动归档</option>
|
|
722
|
+
<option value="30">30 天未活跃</option>
|
|
723
|
+
<option value="60">60 天未活跃</option>
|
|
724
|
+
<option value="90">90 天未活跃</option>
|
|
725
|
+
</select>
|
|
726
|
+
</div>
|
|
727
|
+
<label className="aa-check">
|
|
728
|
+
<input type="checkbox" checked={aa.settings.skipStarred !== false} disabled={aaBusy} onChange={(e) => updateAutoArchive({ skipStarred: e.target.checked })} />
|
|
729
|
+
跳过已收藏的会话
|
|
730
|
+
</label>
|
|
731
|
+
<div className="mv-foot">
|
|
732
|
+
<button type="button" className="archv-btn" disabled={aaBusy || !aa.settings.inactiveDays} onClick={runAutoArchive}>{aaBusy ? '检查中…' : '立即检查'}</button>
|
|
733
|
+
</div>
|
|
734
|
+
<div className="dtl-note">
|
|
735
|
+
自动归档只是把会话收进「已归档」,不删除任何数据,随时可恢复。当前正在使用的会话永远不会被自动归档。检查在打开本面板时触发,每天最多一次。
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
)}
|
|
739
|
+
{storageOpen && (
|
|
740
|
+
<div className="mv-sheet" aria-label="存储占用">
|
|
741
|
+
<div className="mv-sheet-head">
|
|
742
|
+
<h3 className="mv-sheet-title">存储占用</h3>
|
|
743
|
+
<div className="mv-sheet-actions">
|
|
744
|
+
<button type="button" className="archv-btn" disabled={storageBusy} onClick={loadStorage}>{storageBusy ? '统计中…' : '重新统计'}</button>
|
|
745
|
+
<button type="button" className="mv-sheet-close" aria-label="关闭" onClick={() => setStorageOpen(false)}>×</button>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
{storageError ? (
|
|
749
|
+
<div className="archv-err" role="alert">
|
|
750
|
+
<span>{storageError}</span>
|
|
751
|
+
<button type="button" className="archv-errretry" onClick={loadStorage}>重试</button>
|
|
752
|
+
</div>
|
|
753
|
+
) : !storage ? (
|
|
754
|
+
<div className="archv-empty">统计中…</div>
|
|
755
|
+
) : storage.sessionCount === 0 ? (
|
|
756
|
+
<div className="archv-empty">暂无会话,没有可统计的存储占用。</div>
|
|
757
|
+
) : (
|
|
758
|
+
<>
|
|
759
|
+
<div className="dsm-storage-sum">
|
|
760
|
+
共 {fmtBytes(storage.totalBytes) || '0 B'} · {storage.sessionCount} 个会话{storage.unknownSessions ? ` · ${storage.unknownSessions} 个大小未知` : ''}
|
|
761
|
+
</div>
|
|
762
|
+
<div className="dsm-storage-list">
|
|
763
|
+
{storage.workspaces.map((w) => (
|
|
764
|
+
<div className="dsm-storage-row" key={w.key}>
|
|
765
|
+
<span className="dsm-storage-name" title={w.path || '未分组'}>{w.title || (w.path ? pathName(w.path) : '未分组')}</span>
|
|
766
|
+
<span className="dsm-storage-bar" aria-hidden="true"><span className="dsm-storage-fill" style={{ width: `${Math.round((w.share || 0) * 100)}%` }} /></span>
|
|
767
|
+
<span className="dsm-storage-size">{fmtBytes(w.bytes) || '—'}</span>
|
|
768
|
+
<span className="dsm-storage-count">{w.sessions} 个</span>
|
|
769
|
+
</div>
|
|
770
|
+
))}
|
|
771
|
+
</div>
|
|
772
|
+
{storage.top.length > 0 && (
|
|
773
|
+
<div className="dtl-sec">
|
|
774
|
+
<div className="dtl-sec-t">占用最大的会话</div>
|
|
775
|
+
<div className="dsm-storage-list">
|
|
776
|
+
{storage.top.map((s) => (
|
|
777
|
+
<div className="dsm-storage-row" key={s.sessionId}>
|
|
778
|
+
<span className="dsm-storage-name" title={s.sessionId}>{s.title || s.sessionId}</span>
|
|
779
|
+
<span className="dsm-storage-size">{fmtBytes(s.sizeBytes) || '—'}</span>
|
|
780
|
+
<span className="dsm-storage-count">{s.workspaceTitle || (s.workspacePath ? pathName(s.workspacePath) : '未分组')}</span>
|
|
781
|
+
</div>
|
|
782
|
+
))}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
)}
|
|
786
|
+
<div className="dtl-note">统计的是会话日志文件的磁盘占用(压缩后的实际大小),只读,不修改任何数据。</div>
|
|
787
|
+
</>
|
|
788
|
+
)}
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
|
|
792
|
+
{showSessionList && (
|
|
636
793
|
<>
|
|
637
794
|
<div className="sess-tools" aria-label="查找和整理会话">
|
|
638
795
|
<div className="sess-field">
|
|
@@ -657,7 +814,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
657
814
|
</>
|
|
658
815
|
)}
|
|
659
816
|
|
|
660
|
-
{
|
|
817
|
+
{showSessionList && list.length > 0 && (
|
|
661
818
|
<div className="sess-batch">
|
|
662
819
|
<span className="sess-btntext">{selIds.length ? `已选 ${selIds.length} 项` : (filter === 'archived' ? `共 ${archivedList.length} 个归档会话` : filter === 'starred' ? `共 ${starredList.length} 个收藏会话` : `共 ${sessions.length} 个会话(活动 ${activeList.length} / 已归档 ${archivedList.length})`)}</span>
|
|
663
820
|
<button type="button" className="archv-btn" disabled={list.length === 0} onClick={selectAll}>全选</button>
|
|
@@ -678,9 +835,9 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
678
835
|
</div>
|
|
679
836
|
)}
|
|
680
837
|
|
|
681
|
-
{
|
|
838
|
+
{showSessionList && list.length === 0 ? (
|
|
682
839
|
<div className="archv-empty">{query || workspaceFilter !== 'all' ? '没有匹配的会话。请调整搜索词或工作区筛选。' : filter === 'archived' ? '目前没有归档会话。在“全部”里选中会话点“归档”即可收纳进来。' : filter === 'active' ? '目前没有活动会话。' : filter === 'starred' ? '还没有收藏的会话。点击会话左侧的星标即可收藏。' : '暂无可管理的会话。'}</div>
|
|
683
|
-
) :
|
|
840
|
+
) : showSessionList ? (
|
|
684
841
|
<div className="archv-list" role="list">
|
|
685
842
|
{list.map((it) => {
|
|
686
843
|
const date = fmtDate(it.createdAt)
|
|
@@ -703,7 +860,7 @@ function SessionPanel({ workspacesSvc }) {
|
|
|
703
860
|
title={it.starred ? '取消收藏' : '收藏'}
|
|
704
861
|
onClick={(e) => { e.stopPropagation(); toggleStar(it) }}
|
|
705
862
|
>
|
|
706
|
-
<svg viewBox="0 0 24 24" width="
|
|
863
|
+
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M12 2.5l2.9 5.9 6.6.9-4.8 4.6 1.2 6.5-5.9-3.1-5.9 3.1 1.2-6.5L2.5 9.3l6.6-.9z" /></svg>
|
|
707
864
|
</button>
|
|
708
865
|
<div className="archv-body">
|
|
709
866
|
<div className="archv-main">
|
|
@@ -960,6 +1117,21 @@ const SIDEBAR_AUG_CSS = `
|
|
|
960
1117
|
.dsm-trash-date{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}
|
|
961
1118
|
.dsm-trash-actions{display:flex;gap:6px;flex:none}.dsm-trash-actions .archv-btn{min-width:72px}
|
|
962
1119
|
.dsm-trash-empty{font-size:12px;color:var(--dsw-alias-label-tertiary);padding:6px 2px}
|
|
1120
|
+
.dsm-storage-sum{font-size:12px;color:var(--dsw-alias-label-secondary);margin-bottom:10px}
|
|
1121
|
+
.dsm-storage-list{display:flex;flex-direction:column;gap:6px}
|
|
1122
|
+
.dsm-storage-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-fill-elevated)}
|
|
1123
|
+
.dsm-storage-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12.5px;color:var(--dsw-alias-label-primary)}
|
|
1124
|
+
.dsm-storage-bar{flex:0 0 96px;height:6px;border-radius:999px;background:var(--dsw-alias-fill-subtle);overflow:hidden}
|
|
1125
|
+
.dsm-storage-fill{display:block;height:100%;border-radius:999px;background:var(--dsw-alias-state-business-primary)}
|
|
1126
|
+
.dsm-storage-size{font-size:12px;color:var(--dsw-alias-label-secondary);flex:none;white-space:nowrap}
|
|
1127
|
+
.dsm-storage-count{font-size:11px;color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap;max-width:32%;overflow:hidden;text-overflow:ellipsis}
|
|
1128
|
+
.maint-bar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:0 0 8px}
|
|
1129
|
+
.maint-bar-right{margin-left:auto}
|
|
1130
|
+
.maint-note{font-size:11px;color:var(--dsw-alias-label-tertiary)}
|
|
1131
|
+
.mv-sheet-actions{display:flex;align-items:center;gap:6px}
|
|
1132
|
+
.aa-check{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
1133
|
+
.aa-check input{width:15px;height:15px;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer;flex:none}
|
|
1134
|
+
@media (max-width:640px){.dsm-storage-bar{display:none}.dsm-storage-count{max-width:40%}}
|
|
963
1135
|
`
|
|
964
1136
|
|
|
965
1137
|
// Shared status-dot state so the ⋯-menu "标记未读" action can toggle the
|