@wangxiaocuo/dsh-session-trash 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +102 -0
- package/README.en.md +10 -8
- package/README.md +10 -8
- package/client.js +164 -46
- package/index.js +2 -2
- package/lib/cleanup.js +8 -8
- package/lib/eligibility.js +5 -5
- package/lib/errors.js +64 -4
- package/lib/fsops.js +16 -9
- package/lib/http.js +22 -14
- package/lib/inventory.js +16 -16
- package/lib/members.js +6 -6
- package/lib/state.js +57 -12
- package/lib/store.js +18 -15
- package/lib/trash-service.js +60 -59
- package/package.json +12 -4
package/client.js
CHANGED
|
@@ -32,6 +32,19 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
* Preflight refusals the user can act on themselves. Those keep a visible
|
|
33
33
|
* menu entry that explains the refusal instead of hiding it.
|
|
34
34
|
*/
|
|
35
|
+
/** Host `lastErrorCode` values mapped to owner-facing messages. */
|
|
36
|
+
const LAST_ERROR_KEYS = {
|
|
37
|
+
TRASH_COPY_MISSING: 'lastErrorTrashCopyMissing',
|
|
38
|
+
TRASH_COPY_MISSING_MEMBERS: 'lastErrorTrashCopyMissingMembers',
|
|
39
|
+
TRASH_OBJECT_MISSING: 'lastErrorTrashObjectMissing',
|
|
40
|
+
PURGE_FAILED: 'lastErrorPurgeFailed',
|
|
41
|
+
RESTORE_INVISIBLE: 'lastErrorRestoreInvisible',
|
|
42
|
+
MOVE_UNSETTLED: 'lastErrorMoveUnsettled',
|
|
43
|
+
SOURCE_CONFLICT: 'lastErrorSourceConflict',
|
|
44
|
+
INDEX_WRITE_FAILED: 'lastErrorIndexWriteFailed',
|
|
45
|
+
IO_ERROR: 'lastErrorIo',
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
const MOVE_BLOCKED_KEYS = {
|
|
36
49
|
SESSION_LIVE: { label: 'moveBlockedLive', toast: 'blockedLiveToast' },
|
|
37
50
|
SESSION_ACTIVE: { label: 'moveBlockedActive', toast: 'blockedActiveToast' },
|
|
@@ -77,7 +90,8 @@ window.__ModuleLoader__.load({
|
|
|
77
90
|
retry: '重试',
|
|
78
91
|
cleanupExpired: '清理已过期',
|
|
79
92
|
cleaning: '正在清理…',
|
|
80
|
-
|
|
93
|
+
statsCountOne: '{count} 个会话',
|
|
94
|
+
statsCountOther: '{count} 个会话',
|
|
81
95
|
statsSize: '占用 {size}',
|
|
82
96
|
searchLabel: '搜索回收站',
|
|
83
97
|
searchPlaceholder: '搜索标题、Session ID 或原始工作区',
|
|
@@ -127,7 +141,8 @@ window.__ModuleLoader__.load({
|
|
|
127
141
|
badgeMigration: '需要迁移',
|
|
128
142
|
badgeExpiring: '即将到期',
|
|
129
143
|
badgeExpired: '已过期',
|
|
130
|
-
|
|
144
|
+
badgeSubagentsOne: '含 {count} 个子会话',
|
|
145
|
+
badgeSubagentsOther: '含 {count} 个子会话',
|
|
131
146
|
stateTrashing: '正在移入…',
|
|
132
147
|
stateRestoring: '正在恢复…',
|
|
133
148
|
statePurging: '正在删除…',
|
|
@@ -135,7 +150,7 @@ window.__ModuleLoader__.load({
|
|
|
135
150
|
actionRestoreFor: '恢复「{title}」',
|
|
136
151
|
actionMoreFor: '「{title}」的更多操作',
|
|
137
152
|
actionRestoreOrphan: '恢复为孤儿会话',
|
|
138
|
-
actionReveal: '
|
|
153
|
+
actionReveal: '在文件管理器中显示',
|
|
139
154
|
actionRevealDisabled: '原工作区已不存在',
|
|
140
155
|
actionCopyId: '复制 Session ID',
|
|
141
156
|
actionForget: '从索引移除',
|
|
@@ -152,23 +167,37 @@ window.__ModuleLoader__.load({
|
|
|
152
167
|
purgeAllTitle: '永久删除回收站中的全部 {count} 个会话?',
|
|
153
168
|
purgeAllBody: '此操作不可撤销。附件、图片和搜索索引等衍生物不会被删除。',
|
|
154
169
|
purgeAllConfirm: '全部永久删除',
|
|
155
|
-
|
|
170
|
+
purgeAllDoneOne: '已永久删除 {count} 个会话',
|
|
171
|
+
purgeAllDoneOther: '已永久删除 {count} 个会话',
|
|
156
172
|
purgeAllNone: '回收站中没有可删除的会话',
|
|
157
|
-
purgeAllPartial: '
|
|
173
|
+
purgeAllPartial: '批量删除完成:成功 {removed} 个,失败 {failed} 个。',
|
|
158
174
|
forgetTitle: '从回收站列表移除这条缺失记录?',
|
|
159
175
|
forgetBody: '不会删除任何文件;仅移除索引中的这条记录。',
|
|
160
176
|
forgetConfirm: '从索引移除',
|
|
161
|
-
|
|
177
|
+
cleanupDoneOne: '已清理 {count} 个过期会话',
|
|
178
|
+
cleanupDoneOther: '已清理 {count} 个过期会话',
|
|
162
179
|
cleanupNone: '没有需要清理的过期会话',
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
cleanupFailedBannerOne: '上次自动清理有 {count} 个会话未能删除。',
|
|
181
|
+
cleanupFailedBannerOther: '上次自动清理有 {count} 个会话未能删除。',
|
|
182
|
+
cleanupErrorBanner: '上次自动清理失败,请稍后在本页重试。',
|
|
183
|
+
cleanupFailed: '清理失败:还有 {failed} 个过期会话没有删除。',
|
|
184
|
+
cleanupPartial: '已清理 {purged} 个过期会话,{failed} 个没有删除。',
|
|
165
185
|
cleanupRetryAt: '将在 {time} 自动重试。',
|
|
166
186
|
cleanupRetryNow: '立即重试',
|
|
167
187
|
copySuccess: '已复制 Session ID',
|
|
168
188
|
copyFailed: '无法复制 Session ID,请手动复制',
|
|
169
|
-
revealSuccess: '
|
|
189
|
+
revealSuccess: '已在文件管理器中显示原工作区',
|
|
170
190
|
revealFailed: '无法打开原工作区',
|
|
171
191
|
lastErrorLabel: '上次错误:{message}',
|
|
192
|
+
lastErrorTrashCopyMissing: '回收站里的会话副本缺失。',
|
|
193
|
+
lastErrorTrashCopyMissingMembers: '回收站里的部分子会话副本缺失。',
|
|
194
|
+
lastErrorTrashObjectMissing: '回收站对象目录缺失。',
|
|
195
|
+
lastErrorPurgeFailed: '会话日志删除失败。',
|
|
196
|
+
lastErrorRestoreInvisible: '恢复后会话存储没有识别到该会话。',
|
|
197
|
+
lastErrorMoveUnsettled: '会话已移动,但会话存储仍报告它存在。',
|
|
198
|
+
lastErrorSourceConflict: '原会话目录与回收站副本同时存在,两者都已保留。',
|
|
199
|
+
lastErrorIndexWriteFailed: '回收站索引写入失败。',
|
|
200
|
+
lastErrorIo: '文件操作失败。',
|
|
172
201
|
footnote: '回收站目录:{path}',
|
|
173
202
|
move: '移入回收站',
|
|
174
203
|
moveBlockedLive: '移入回收站(会话已打开)',
|
|
@@ -177,7 +206,8 @@ window.__ModuleLoader__.load({
|
|
|
177
206
|
undo: '撤销',
|
|
178
207
|
undoFailed: '撤销失败',
|
|
179
208
|
trashSuccess: '已将「{title}」移入回收站',
|
|
180
|
-
|
|
209
|
+
trashSuccessTreeOne: '「{title}」及其 {count} 个子会话已移入回收站',
|
|
210
|
+
trashSuccessTreeOther: '「{title}」及其 {count} 个子会话已移入回收站',
|
|
181
211
|
blockedLiveToast: '该会话已在本机打开。DSH 目前不会释放已打开的会话,请重启 DSH 后再移入回收站。',
|
|
182
212
|
blockedActiveToast: '该会话还有正在运行的任务或子代理,等它停下来后再移入回收站。',
|
|
183
213
|
blockedDescendantsToast: '该会话包含可见的子会话,只能使用 DSH 自带的「归档」整棵会话树。',
|
|
@@ -202,7 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
202
232
|
errorIndexCorrupt: '回收站索引损坏,已进入保护模式。',
|
|
203
233
|
errorIndexOk: '回收站索引可正常读取,无需重建。',
|
|
204
234
|
errorForgetNotAllowed: '只有文件缺失的记录可以从索引移除。',
|
|
205
|
-
errorRevealUnavailable: '
|
|
235
|
+
errorRevealUnavailable: '当前环境无法打开文件管理器。',
|
|
206
236
|
errorRevealFailed: '无法打开原工作区。',
|
|
207
237
|
errorIo: '文件操作失败,请稍后重试。',
|
|
208
238
|
}
|
|
@@ -215,7 +245,8 @@ window.__ModuleLoader__.load({
|
|
|
215
245
|
retry: 'Retry',
|
|
216
246
|
cleanupExpired: 'Delete expired',
|
|
217
247
|
cleaning: 'Cleaning…',
|
|
218
|
-
|
|
248
|
+
statsCountOne: '{count} session',
|
|
249
|
+
statsCountOther: '{count} sessions',
|
|
219
250
|
statsSize: '{size} used',
|
|
220
251
|
searchLabel: 'Search the trash',
|
|
221
252
|
searchPlaceholder: 'Search title, Session ID, or original workspace',
|
|
@@ -265,7 +296,8 @@ window.__ModuleLoader__.load({
|
|
|
265
296
|
badgeMigration: 'Migration required',
|
|
266
297
|
badgeExpiring: 'Expiring soon',
|
|
267
298
|
badgeExpired: 'Expired',
|
|
268
|
-
|
|
299
|
+
badgeSubagentsOne: '{count} sub-session',
|
|
300
|
+
badgeSubagentsOther: '{count} sub-sessions',
|
|
269
301
|
stateTrashing: 'Moving…',
|
|
270
302
|
stateRestoring: 'Restoring…',
|
|
271
303
|
statePurging: 'Deleting…',
|
|
@@ -273,7 +305,7 @@ window.__ModuleLoader__.load({
|
|
|
273
305
|
actionRestoreFor: 'Restore “{title}”',
|
|
274
306
|
actionMoreFor: 'More actions for “{title}”',
|
|
275
307
|
actionRestoreOrphan: 'Restore as orphan',
|
|
276
|
-
actionReveal: 'Show in
|
|
308
|
+
actionReveal: 'Show in file manager',
|
|
277
309
|
actionRevealDisabled: 'The original workspace no longer exists',
|
|
278
310
|
actionCopyId: 'Copy Session ID',
|
|
279
311
|
actionForget: 'Remove from index',
|
|
@@ -290,23 +322,37 @@ window.__ModuleLoader__.load({
|
|
|
290
322
|
purgeAllTitle: 'Permanently delete all {count} sessions in the trash?',
|
|
291
323
|
purgeAllBody: 'This cannot be undone. Attachments, images, and search index rows are not deleted.',
|
|
292
324
|
purgeAllConfirm: 'Delete all permanently',
|
|
293
|
-
|
|
325
|
+
purgeAllDoneOne: 'Permanently deleted {count} session',
|
|
326
|
+
purgeAllDoneOther: 'Permanently deleted {count} sessions',
|
|
294
327
|
purgeAllNone: 'No sessions to delete',
|
|
295
|
-
purgeAllPartial: '
|
|
328
|
+
purgeAllPartial: 'Bulk delete finished: {removed} removed, {failed} failed.',
|
|
296
329
|
forgetTitle: 'Remove this missing record from the trash list?',
|
|
297
330
|
forgetBody: 'No file is deleted; only this index record is removed.',
|
|
298
331
|
forgetConfirm: 'Remove from index',
|
|
299
|
-
|
|
332
|
+
cleanupDoneOne: 'Deleted {count} expired session',
|
|
333
|
+
cleanupDoneOther: 'Deleted {count} expired sessions',
|
|
300
334
|
cleanupNone: 'No expired sessions to delete',
|
|
301
|
-
|
|
302
|
-
|
|
335
|
+
cleanupFailedBannerOne: 'The last automatic cleanup could not delete {count} expired session.',
|
|
336
|
+
cleanupFailedBannerOther: 'The last automatic cleanup could not delete {count} expired sessions.',
|
|
337
|
+
cleanupErrorBanner: 'The last automatic cleanup failed. Retry from this page later.',
|
|
338
|
+
cleanupFailed: 'Cleanup could not delete {failed} of the expired sessions.',
|
|
339
|
+
cleanupPartial: 'Cleanup deleted {purged}; {failed} could not be deleted.',
|
|
303
340
|
cleanupRetryAt: 'Next retry: {time}.',
|
|
304
341
|
cleanupRetryNow: 'Retry now',
|
|
305
342
|
copySuccess: 'Session ID copied',
|
|
306
343
|
copyFailed: 'Could not copy the Session ID. Copy it manually.',
|
|
307
|
-
revealSuccess: 'Revealed the original workspace in
|
|
344
|
+
revealSuccess: 'Revealed the original workspace in the file manager',
|
|
308
345
|
revealFailed: 'Could not open the original workspace',
|
|
309
346
|
lastErrorLabel: 'Last error: {message}',
|
|
347
|
+
lastErrorTrashCopyMissing: 'The stored copy of this session is missing from the trash.',
|
|
348
|
+
lastErrorTrashCopyMissingMembers: 'Some sub-session copies are missing from the trash.',
|
|
349
|
+
lastErrorTrashObjectMissing: 'The trash object directory is missing.',
|
|
350
|
+
lastErrorPurgeFailed: 'The session log could not be deleted.',
|
|
351
|
+
lastErrorRestoreInvisible: 'Session storage did not report the restored session.',
|
|
352
|
+
lastErrorMoveUnsettled: 'The session was moved, but session storage still reports it.',
|
|
353
|
+
lastErrorSourceConflict: 'The original session directory and the trash copy both exist; both were kept.',
|
|
354
|
+
lastErrorIndexWriteFailed: 'The trash index could not be written.',
|
|
355
|
+
lastErrorIo: 'A file operation failed.',
|
|
310
356
|
footnote: 'Trash directory: {path}',
|
|
311
357
|
move: 'Move to trash',
|
|
312
358
|
moveBlockedLive: 'Move to trash (session is open)',
|
|
@@ -315,7 +361,8 @@ window.__ModuleLoader__.load({
|
|
|
315
361
|
undo: 'Undo',
|
|
316
362
|
undoFailed: 'Could not undo',
|
|
317
363
|
trashSuccess: 'Moved “{title}” to the trash',
|
|
318
|
-
|
|
364
|
+
trashSuccessTreeOne: '“{title}” and its single sub-session were moved to the trash',
|
|
365
|
+
trashSuccessTreeOther: '“{title}” and its {count} sub-sessions were moved to the trash',
|
|
319
366
|
blockedLiveToast: 'This session is open in DSH. DSH does not release opened sessions yet; restart DSH, then move it to the trash.',
|
|
320
367
|
blockedActiveToast: 'This session still has running work; wait for it to stop, then move it to the trash.',
|
|
321
368
|
blockedDescendantsToast: 'This session has visible child sessions; use the built-in Archive action for the whole tree instead.',
|
|
@@ -340,7 +387,7 @@ window.__ModuleLoader__.load({
|
|
|
340
387
|
errorIndexCorrupt: 'The trash index is damaged, so protection mode is on.',
|
|
341
388
|
errorIndexOk: 'The trash index is readable and does not need a rebuild.',
|
|
342
389
|
errorForgetNotAllowed: 'Only records whose log is missing can be removed from the index.',
|
|
343
|
-
errorRevealUnavailable: 'This environment cannot open
|
|
390
|
+
errorRevealUnavailable: 'This environment cannot open a file manager.',
|
|
344
391
|
errorRevealFailed: 'Could not open the original workspace.',
|
|
345
392
|
errorIo: 'A file operation failed. Try again.',
|
|
346
393
|
}
|
|
@@ -359,11 +406,26 @@ window.__ModuleLoader__.load({
|
|
|
359
406
|
}
|
|
360
407
|
|
|
361
408
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
409
|
+
* Pick the grammatical singular/plural locale form for a count.
|
|
410
|
+
*
|
|
411
|
+
* @param {(key: string) => string} t bound translator.
|
|
412
|
+
* @param {string} key base locale key (without the One/Other suffix).
|
|
413
|
+
* @param {number} count item count.
|
|
414
|
+
* @param {Record<string, unknown>} [values] extra placeholders.
|
|
415
|
+
* @returns {string} the formatted text.
|
|
416
|
+
*/
|
|
417
|
+
function countText(t, key, count, values = {}) {
|
|
418
|
+
const template = t(`${key}${count === 1 ? 'One' : 'Other'}`)
|
|
419
|
+
return format(template, { ...values, count })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* @param {number | null | undefined} bytes raw byte count.
|
|
424
|
+
* @returns {string} a compact human-readable size, or an em dash when unknown.
|
|
364
425
|
*/
|
|
365
426
|
function formatBytes(bytes) {
|
|
366
|
-
if (typeof bytes !== 'number' || !Number.isFinite(bytes)
|
|
427
|
+
if (typeof bytes !== 'number' || !Number.isFinite(bytes)) return '—'
|
|
428
|
+
if (bytes <= 0) return '0 B'
|
|
367
429
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
368
430
|
let value = bytes
|
|
369
431
|
let unit = 0
|
|
@@ -827,8 +889,14 @@ window.__ModuleLoader__.load({
|
|
|
827
889
|
if (wrapRef.current !== null && !wrapRef.current.contains(event.target)) setOpen(false)
|
|
828
890
|
}
|
|
829
891
|
const onKeyDown = (event) => {
|
|
892
|
+
if (event.key === 'Tab') {
|
|
893
|
+
// Let focus leave the menu naturally; just stop leaving it open.
|
|
894
|
+
setOpen(false)
|
|
895
|
+
return
|
|
896
|
+
}
|
|
830
897
|
if (event.key !== 'Escape') return
|
|
831
898
|
event.preventDefault()
|
|
899
|
+
event.stopPropagation()
|
|
832
900
|
setOpen(false)
|
|
833
901
|
if (buttonRef.current !== null) buttonRef.current.focus()
|
|
834
902
|
}
|
|
@@ -930,9 +998,11 @@ window.__ModuleLoader__.load({
|
|
|
930
998
|
},
|
|
931
999
|
items.map((item, index) =>
|
|
932
1000
|
h(
|
|
933
|
-
|
|
1001
|
+
React.Fragment,
|
|
934
1002
|
{ key: item.key },
|
|
935
|
-
item.separatorBefore === true && index > 0
|
|
1003
|
+
item.separatorBefore === true && index > 0
|
|
1004
|
+
? h('div', { className: 'dsh-trash__menuSeparator', role: 'separator' })
|
|
1005
|
+
: null,
|
|
936
1006
|
h(
|
|
937
1007
|
'button',
|
|
938
1008
|
{
|
|
@@ -972,7 +1042,7 @@ window.__ModuleLoader__.load({
|
|
|
972
1042
|
* }} props dialog content and handlers.
|
|
973
1043
|
* @returns {import('react').ReactElement} the dialog overlay.
|
|
974
1044
|
*/
|
|
975
|
-
function ConfirmDialog({ kind, entry, count, t, onCancel, onConfirm }) {
|
|
1045
|
+
function ConfirmDialog({ kind, entry, count, t, onCancel, onConfirm, fallbackRef }) {
|
|
976
1046
|
const dialogRef = React.useRef(null)
|
|
977
1047
|
const cancelRef = React.useRef(null)
|
|
978
1048
|
const titleId = `dsh-trash-dialog-title-${kind}`
|
|
@@ -1006,7 +1076,12 @@ window.__ModuleLoader__.load({
|
|
|
1006
1076
|
document.addEventListener('keydown', onKeyDown, true)
|
|
1007
1077
|
return () => {
|
|
1008
1078
|
document.removeEventListener('keydown', onKeyDown, true)
|
|
1009
|
-
if (previous !== null && typeof previous.focus === 'function'
|
|
1079
|
+
if (previous !== null && typeof previous.focus === 'function' && previous.isConnected !== false) {
|
|
1080
|
+
previous.focus()
|
|
1081
|
+
return
|
|
1082
|
+
}
|
|
1083
|
+
const fallback = fallbackRef === null || fallbackRef === undefined ? null : fallbackRef.current
|
|
1084
|
+
if (fallback !== null && typeof fallback.focus === 'function') fallback.focus()
|
|
1010
1085
|
}
|
|
1011
1086
|
}, [onCancel])
|
|
1012
1087
|
|
|
@@ -1090,6 +1165,7 @@ window.__ModuleLoader__.load({
|
|
|
1090
1165
|
const [rebuilding, setRebuilding] = React.useState(false)
|
|
1091
1166
|
const [purgingAll, setPurgingAll] = React.useState(false)
|
|
1092
1167
|
const [announcement, setAnnouncement] = React.useState('')
|
|
1168
|
+
const [clock, setClock] = React.useState(() => Date.now())
|
|
1093
1169
|
const [query, setQuery] = React.useState('')
|
|
1094
1170
|
const [filter, setFilter] = React.useState('all')
|
|
1095
1171
|
const [page, setPage] = React.useState(1)
|
|
@@ -1149,6 +1225,19 @@ window.__ModuleLoader__.load({
|
|
|
1149
1225
|
})
|
|
1150
1226
|
}
|
|
1151
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* @param {string} sessionId row whose owner-facing error is resolved.
|
|
1230
|
+
* @returns {void}
|
|
1231
|
+
*/
|
|
1232
|
+
function clearRowError(sessionId) {
|
|
1233
|
+
setRowErrors((previous) => {
|
|
1234
|
+
if (!(sessionId in previous)) return previous
|
|
1235
|
+
const next = { ...previous }
|
|
1236
|
+
delete next[sessionId]
|
|
1237
|
+
return next
|
|
1238
|
+
})
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1152
1241
|
/**
|
|
1153
1242
|
* Open an abort scope for one write. It is cancelled by a hard timeout
|
|
1154
1243
|
* and by the unmount cleanup, so a stuck request can never wedge the
|
|
@@ -1283,6 +1372,15 @@ window.__ModuleLoader__.load({
|
|
|
1283
1372
|
}
|
|
1284
1373
|
}, [load])
|
|
1285
1374
|
|
|
1375
|
+
// Keep relative expiry text honest on a page left open for a long time.
|
|
1376
|
+
React.useEffect(() => {
|
|
1377
|
+
if (typeof window === 'undefined' || typeof window.setInterval !== 'function') return undefined
|
|
1378
|
+
const handle = window.setInterval(() => setClock(Date.now()), 60_000)
|
|
1379
|
+
return () => {
|
|
1380
|
+
if (typeof window.clearInterval === 'function') window.clearInterval(handle)
|
|
1381
|
+
}
|
|
1382
|
+
}, [])
|
|
1383
|
+
|
|
1286
1384
|
const data = view.data
|
|
1287
1385
|
const entries = data !== null && Array.isArray(data.entries) ? [...data.entries] : []
|
|
1288
1386
|
entries.sort((left, right) => (right.trashedAt ?? 0) - (left.trashedAt ?? 0))
|
|
@@ -1323,7 +1421,7 @@ window.__ModuleLoader__.load({
|
|
|
1323
1421
|
const corrupt = data !== null && data.index !== undefined && data.index.status === 'corrupt'
|
|
1324
1422
|
const recovered = data !== null && data.index !== undefined && data.index.status === 'recovered'
|
|
1325
1423
|
const unindexed = data !== null && data.index !== undefined && typeof data.index.unindexed === 'number' ? data.index.unindexed : 0
|
|
1326
|
-
const now =
|
|
1424
|
+
const now = clock
|
|
1327
1425
|
const normalizedQuery = query.trim().toLowerCase()
|
|
1328
1426
|
|
|
1329
1427
|
/**
|
|
@@ -1356,6 +1454,9 @@ window.__ModuleLoader__.load({
|
|
|
1356
1454
|
const filteredEntries = entries.filter((entry) => matchesQuery(entry) && matchesFilter(entry))
|
|
1357
1455
|
const pageCount = Math.max(1, Math.ceil(filteredEntries.length / PAGE_SIZE))
|
|
1358
1456
|
const currentPage = Math.min(Math.max(1, page), pageCount)
|
|
1457
|
+
React.useEffect(() => {
|
|
1458
|
+
if (page > pageCount) setPage(pageCount)
|
|
1459
|
+
}, [page, pageCount])
|
|
1359
1460
|
const pageStart = (currentPage - 1) * PAGE_SIZE
|
|
1360
1461
|
const visibleEntries = filteredEntries.slice(pageStart, pageStart + PAGE_SIZE)
|
|
1361
1462
|
const filtersActive = normalizedQuery !== '' || filter !== 'all'
|
|
@@ -1444,6 +1545,7 @@ window.__ModuleLoader__.load({
|
|
|
1444
1545
|
session !== null && session !== undefined && typeof session.title === 'string' && session.title !== ''
|
|
1445
1546
|
? session.title
|
|
1446
1547
|
: entry.title
|
|
1548
|
+
clearRowError(entry.sessionId)
|
|
1447
1549
|
const message = format(t(orphan ? 'restoreOrphanSuccess' : 'restoreSuccess'), { title })
|
|
1448
1550
|
bumpRevision()
|
|
1449
1551
|
ctx.contextMenus.notify(message, true)
|
|
@@ -1472,6 +1574,7 @@ window.__ModuleLoader__.load({
|
|
|
1472
1574
|
fail(entry, describeActionFailure(outcome.status, outcome.payload, t))
|
|
1473
1575
|
return
|
|
1474
1576
|
}
|
|
1577
|
+
clearRowError(entry.sessionId)
|
|
1475
1578
|
const message = format(t('purgeSuccess'), { title: entry.title })
|
|
1476
1579
|
bumpRevision()
|
|
1477
1580
|
ctx.contextMenus.notify(message, true)
|
|
@@ -1500,6 +1603,7 @@ window.__ModuleLoader__.load({
|
|
|
1500
1603
|
fail(entry, describeActionFailure(outcome.status, outcome.payload, t))
|
|
1501
1604
|
return
|
|
1502
1605
|
}
|
|
1606
|
+
clearRowError(entry.sessionId)
|
|
1503
1607
|
bumpRevision()
|
|
1504
1608
|
ctx.contextMenus.notify(t('forgetSuccess'), true)
|
|
1505
1609
|
announce(t('forgetSuccess'))
|
|
@@ -1527,6 +1631,7 @@ window.__ModuleLoader__.load({
|
|
|
1527
1631
|
fail(entry, describeActionFailure(outcome.status, outcome.payload, t))
|
|
1528
1632
|
return
|
|
1529
1633
|
}
|
|
1634
|
+
clearRowError(entry.sessionId)
|
|
1530
1635
|
ctx.contextMenus.notify(t('revealSuccess'), true)
|
|
1531
1636
|
announce(t('revealSuccess'))
|
|
1532
1637
|
} finally {
|
|
@@ -1563,13 +1668,15 @@ window.__ModuleLoader__.load({
|
|
|
1563
1668
|
ctx.contextMenus.notify(describeActionFailure(outcome.status, outcome.payload, t), false)
|
|
1564
1669
|
return
|
|
1565
1670
|
}
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1671
|
+
const payload = outcome.payload
|
|
1672
|
+
const purged = payload !== null && payload !== undefined && typeof payload.purged === 'number' ? payload.purged : 0
|
|
1673
|
+
const failed = payload !== null && payload !== undefined && typeof payload.failed === 'number' ? payload.failed : 0
|
|
1674
|
+
let message
|
|
1675
|
+
if (failed > 0 && purged > 0) message = format(t('cleanupPartial'), { purged, failed })
|
|
1676
|
+
else if (failed > 0) message = format(t('cleanupFailed'), { failed })
|
|
1677
|
+
else message = purged > 0 ? countText(t, 'cleanupDone', purged) : t('cleanupNone')
|
|
1571
1678
|
bumpRevision()
|
|
1572
|
-
ctx.contextMenus.notify(message,
|
|
1679
|
+
ctx.contextMenus.notify(message, failed === 0)
|
|
1573
1680
|
announce(message)
|
|
1574
1681
|
} finally {
|
|
1575
1682
|
endWrite()
|
|
@@ -1606,7 +1713,7 @@ window.__ModuleLoader__.load({
|
|
|
1606
1713
|
failed > 0
|
|
1607
1714
|
? format(t('purgeAllPartial'), { removed, failed })
|
|
1608
1715
|
: removed > 0
|
|
1609
|
-
?
|
|
1716
|
+
? countText(t, 'purgeAllDone', removed)
|
|
1610
1717
|
: t('purgeAllNone')
|
|
1611
1718
|
bumpRevision()
|
|
1612
1719
|
ctx.contextMenus.notify(message, failed === 0)
|
|
@@ -1741,7 +1848,7 @@ window.__ModuleLoader__.load({
|
|
|
1741
1848
|
if (subagentCount > 0) {
|
|
1742
1849
|
badges.push({
|
|
1743
1850
|
key: 'subagents',
|
|
1744
|
-
label:
|
|
1851
|
+
label: countText(t, 'badgeSubagents', subagentCount),
|
|
1745
1852
|
tone: 'info',
|
|
1746
1853
|
})
|
|
1747
1854
|
}
|
|
@@ -1827,9 +1934,7 @@ window.__ModuleLoader__.load({
|
|
|
1827
1934
|
const retryText =
|
|
1828
1935
|
cleanupRetryAt !== null ? format(t('cleanupRetryAt'), { time: formatDate(cleanupRetryAt, locale, true) }) : ''
|
|
1829
1936
|
const cleanupMessage =
|
|
1830
|
-
cleanupFailed > 0
|
|
1831
|
-
? format(t('cleanupFailedBanner'), { count: cleanupFailed })
|
|
1832
|
-
: format(t('cleanupErrorBanner'), { message: cleanupError })
|
|
1937
|
+
cleanupFailed > 0 ? countText(t, 'cleanupFailedBanner', cleanupFailed) : t('cleanupErrorBanner')
|
|
1833
1938
|
banners.push(
|
|
1834
1939
|
h(
|
|
1835
1940
|
'div',
|
|
@@ -1837,7 +1942,7 @@ window.__ModuleLoader__.load({
|
|
|
1837
1942
|
h(TrashGlyph, { size: 16 }),
|
|
1838
1943
|
h(
|
|
1839
1944
|
'span',
|
|
1840
|
-
{ className: 'dsh-trash__bannerText' },
|
|
1945
|
+
{ className: 'dsh-trash__bannerText', title: cleanupError ?? undefined },
|
|
1841
1946
|
cleanupMessage,
|
|
1842
1947
|
retryText === '' ? null : ` ${retryText}`,
|
|
1843
1948
|
),
|
|
@@ -1882,7 +1987,7 @@ window.__ModuleLoader__.load({
|
|
|
1882
1987
|
h(
|
|
1883
1988
|
'span',
|
|
1884
1989
|
{ className: 'dsh-trash__stats' },
|
|
1885
|
-
|
|
1990
|
+
countText(t, 'statsCount', entries.length),
|
|
1886
1991
|
h('span', { className: 'dsh-trash__dot', 'aria-hidden': true }, '·'),
|
|
1887
1992
|
format(t('statsSize'), { size: formatBytes(totalBytes) }),
|
|
1888
1993
|
h('span', { className: 'dsh-trash__dot', 'aria-hidden': true }, '·'),
|
|
@@ -2047,7 +2152,14 @@ window.__ModuleLoader__.load({
|
|
|
2047
2152
|
{ className: 'dsh-trash__list', 'aria-label': t('listLabel') },
|
|
2048
2153
|
visibleEntries.map((entry) => {
|
|
2049
2154
|
const rowBusy = pendingRows.has(entry.sessionId)
|
|
2050
|
-
const
|
|
2155
|
+
const rowError = rowErrors[entry.sessionId]
|
|
2156
|
+
const lastErrorCode = typeof entry.lastErrorCode === 'string' ? entry.lastErrorCode : null
|
|
2157
|
+
const lastErrorText =
|
|
2158
|
+
typeof entry.lastError === 'string' && entry.lastError !== ''
|
|
2159
|
+
? t(LAST_ERROR_KEYS[lastErrorCode] ?? 'lastErrorIo')
|
|
2160
|
+
: null
|
|
2161
|
+
const error = rowError ?? lastErrorText
|
|
2162
|
+
const errorTitle = rowError === undefined && typeof entry.lastError === 'string' ? entry.lastError : undefined
|
|
2051
2163
|
const title = entry.title !== undefined && entry.title !== '' ? entry.title : entry.sessionId
|
|
2052
2164
|
return h(
|
|
2053
2165
|
'li',
|
|
@@ -2087,7 +2199,11 @@ window.__ModuleLoader__.load({
|
|
|
2087
2199
|
h('span', null, formatBytes(entry.sizeBytes)),
|
|
2088
2200
|
),
|
|
2089
2201
|
error !== null && error !== undefined && error !== ''
|
|
2090
|
-
? h(
|
|
2202
|
+
? h(
|
|
2203
|
+
'div',
|
|
2204
|
+
{ className: 'dsh-trash__rowError', role: 'status', title: errorTitle },
|
|
2205
|
+
format(t('lastErrorLabel'), { message: error }),
|
|
2206
|
+
)
|
|
2091
2207
|
: null,
|
|
2092
2208
|
),
|
|
2093
2209
|
h(
|
|
@@ -2166,6 +2282,7 @@ window.__ModuleLoader__.load({
|
|
|
2166
2282
|
className: 'dsh-trash__panel',
|
|
2167
2283
|
key: 'panel',
|
|
2168
2284
|
ref: panelRef,
|
|
2285
|
+
tabIndex: -1,
|
|
2169
2286
|
'aria-busy': readsBusy ? 'true' : 'false',
|
|
2170
2287
|
},
|
|
2171
2288
|
panelBody,
|
|
@@ -2186,6 +2303,7 @@ window.__ModuleLoader__.load({
|
|
|
2186
2303
|
count: dialog.count,
|
|
2187
2304
|
t,
|
|
2188
2305
|
onCancel: closeDialog,
|
|
2306
|
+
fallbackRef: panelRef,
|
|
2189
2307
|
onConfirm: () => {
|
|
2190
2308
|
const current = dialog
|
|
2191
2309
|
setDialog(null)
|
|
@@ -2252,7 +2370,7 @@ window.__ModuleLoader__.load({
|
|
|
2252
2370
|
: 0
|
|
2253
2371
|
const message =
|
|
2254
2372
|
subagentCount > 0
|
|
2255
|
-
?
|
|
2373
|
+
? countText(t, 'trashSuccessTree', subagentCount, { title })
|
|
2256
2374
|
: format(t('trashSuccess'), { title })
|
|
2257
2375
|
ctx.contextMenus.notify(message, true, {
|
|
2258
2376
|
label: t('undo'),
|
package/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function createLogger(ctx) {
|
|
|
34
34
|
const prefix = 'dsh-session-trash'
|
|
35
35
|
let sink = null
|
|
36
36
|
try {
|
|
37
|
-
const factory = ctx.get('logger')
|
|
37
|
+
const factory = /** @type {any} */ (ctx.get('logger'))
|
|
38
38
|
if (typeof factory === 'function') {
|
|
39
39
|
const instance = factory(prefix)
|
|
40
40
|
if (instance !== null && instance !== undefined && typeof instance.info === 'function') sink = instance
|
|
@@ -65,7 +65,7 @@ function createLogger(ctx) {
|
|
|
65
65
|
* recovery, and the retention sweep.
|
|
66
66
|
*
|
|
67
67
|
* @param {import('@deepseek-ai/cordis').Context} ctx plugin context.
|
|
68
|
-
* @param {
|
|
68
|
+
* @param {any} config validated plugin configuration.
|
|
69
69
|
* @returns {void}
|
|
70
70
|
*/
|
|
71
71
|
export function apply(ctx, config) {
|
package/lib/cleanup.js
CHANGED
|
@@ -14,7 +14,7 @@ export const CLEANUP_RETRY_MAX_MS = 60 * 60 * 1000
|
|
|
14
14
|
const STOPPED_RESULT = Object.freeze({ purged: 0, failed: 0, settled: 0 })
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} error arbitrary failure.
|
|
18
18
|
* @returns {string} a concise message.
|
|
19
19
|
*/
|
|
20
20
|
function messageOf(error) {
|
|
@@ -22,7 +22,7 @@ function messageOf(error) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {any} error arbitrary failure.
|
|
26
26
|
* @returns {string} a bounded diagnostic message for `/state`.
|
|
27
27
|
*/
|
|
28
28
|
function errorText(error) {
|
|
@@ -60,18 +60,18 @@ function resolveTimer(ctx) {
|
|
|
60
60
|
*
|
|
61
61
|
* @param {{
|
|
62
62
|
* ctx: import('@deepseek-ai/cordis').Context,
|
|
63
|
-
* trashService: { sweep: (options: { allowPurge: boolean, now: number, reason: string }) => Promise<
|
|
63
|
+
* trashService: { sweep: (options: { allowPurge: boolean, now: number, reason: string }) => Promise<any> },
|
|
64
64
|
* config: { sweepIntervalHours: number, enableAutoCleanup: boolean },
|
|
65
65
|
* logger: { info: Function, warn: Function, error: Function, debug: Function },
|
|
66
66
|
* }} deps host services and the configured cadence.
|
|
67
67
|
* @returns {{
|
|
68
68
|
* start: () => void,
|
|
69
69
|
* stop: () => void,
|
|
70
|
-
* runNow: (options?: { manual?: boolean, reason?: string }) => Promise<
|
|
70
|
+
* runNow: (options?: { manual?: boolean, reason?: string }) => Promise<any>,
|
|
71
71
|
* maybeSweepOnPageOpen: () => void,
|
|
72
72
|
* nextSweepAt: () => number | null,
|
|
73
73
|
* lastSweepAt: () => number,
|
|
74
|
-
* diagnostics: () =>
|
|
74
|
+
* diagnostics: () => any,
|
|
75
75
|
* }} the cleanup service.
|
|
76
76
|
*/
|
|
77
77
|
export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
@@ -91,7 +91,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
91
91
|
let disposeRetry = null
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* @param {
|
|
94
|
+
* @param {any} error sweep failure.
|
|
95
95
|
* @returns {void}
|
|
96
96
|
*/
|
|
97
97
|
function report(error) {
|
|
@@ -148,7 +148,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
148
148
|
* because an automatic sweep may have been started with `allowPurge: false`.
|
|
149
149
|
*
|
|
150
150
|
* @param {{ manual?: boolean, reason?: string }} [options] manual trigger and diagnostic label.
|
|
151
|
-
* @returns {Promise<
|
|
151
|
+
* @returns {Promise<any>} the sweep summary.
|
|
152
152
|
*/
|
|
153
153
|
async function runNow(options = {}) {
|
|
154
154
|
if (stopped) return { ...STOPPED_RESULT }
|
|
@@ -324,7 +324,7 @@ export function createCleanupService({ ctx, trashService, config, logger }) {
|
|
|
324
324
|
/**
|
|
325
325
|
* Read-only cleanup diagnostics for `/state` and the settings page.
|
|
326
326
|
*
|
|
327
|
-
* @returns {
|
|
327
|
+
* @returns {any} a JSON-safe diagnostic snapshot.
|
|
328
328
|
*/
|
|
329
329
|
diagnostics() {
|
|
330
330
|
return {
|
package/lib/eligibility.js
CHANGED
|
@@ -7,14 +7,14 @@ export const PREFLIGHT_TTL_MS = 2000
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {{ sessionId: string, header: object }} DescendantRef
|
|
10
|
-
* @typedef {{ sessionId: string, header:
|
|
10
|
+
* @typedef {{ sessionId: string, header: any, location: { path: string }, formatStatus?: string }} PlannedMember
|
|
11
11
|
* @typedef {{ eligible: true, members: PlannedMember[] } | { eligible: false, code: string, message: string, details?: object }} PreflightResult
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {string} code refusal code.
|
|
16
16
|
* @param {string} message owner-facing explanation.
|
|
17
|
-
* @param {
|
|
17
|
+
* @param {any} [details] optional structured context.
|
|
18
18
|
* @returns {{ eligible: false, code: string, message: string, details?: object }} a refusal.
|
|
19
19
|
*/
|
|
20
20
|
function refuse(code, message, details) {
|
|
@@ -24,7 +24,7 @@ function refuse(code, message, details) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Collect every stored session whose lineage descends from `sessionId`.
|
|
26
26
|
*
|
|
27
|
-
* @param {
|
|
27
|
+
* @param {any} persistence persistence service.
|
|
28
28
|
* @param {string} sessionId root session.
|
|
29
29
|
* @returns {Promise<DescendantRef[]>} descendant identity and stored header.
|
|
30
30
|
*/
|
|
@@ -59,7 +59,7 @@ async function descendantsFromListing(persistence, sessionId) {
|
|
|
59
59
|
* A grandchild whose direct parent is no longer stored still proves that the
|
|
60
60
|
* root session owns a lineage the trash must not split.
|
|
61
61
|
*
|
|
62
|
-
* @param {readonly
|
|
62
|
+
* @param {readonly any[]} nodes descendant nodes from the trace.
|
|
63
63
|
* @returns {DescendantRef[]} every descendant session identity and header.
|
|
64
64
|
*/
|
|
65
65
|
function collectTraceDescendants(nodes) {
|
|
@@ -123,7 +123,7 @@ export function createEligibility({ ctx, store, logger }) {
|
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* @param {string} sessionId descendant session to validate.
|
|
126
|
-
* @returns {Promise<
|
|
126
|
+
* @returns {Promise<any>} either `{ member }` with the planned member or `{ refusal }`.
|
|
127
127
|
*/
|
|
128
128
|
async function planDescendant(sessionId) {
|
|
129
129
|
let snapshot
|