@michengai/dsh-archive-manager 0.1.20 → 0.1.22
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 +15 -0
- package/CHANGELOG.zh-CN.md +15 -0
- package/lib/client.js +290 -5
- package/lib/projcache.js +17 -12
- package/lib/workspace.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
Published release notes are retained below; new versions are added without removing earlier entries.
|
|
6
6
|
|
|
7
|
+
## 0.1.22 — 2026-09-01
|
|
8
|
+
|
|
9
|
+
- Added arbitrary archived-chat selection in **Settings → Archived**, including per-row checkboxes, select-all for the current filtered results, cross-project selection, batch restore, and confirmed batch permanent deletion.
|
|
10
|
+
- Kept selected operations on the existing authoritative host batch path, so stale records, partial deletion failures, and retry behavior match existing project-wide and all-chat actions.
|
|
11
|
+
- Refreshed the client session projection after restoring chats so restored sessions immediately reappear in their original workspace.
|
|
12
|
+
|
|
13
|
+
Published package: [`@michengai/dsh-archive-manager@0.1.22`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.22).
|
|
14
|
+
|
|
15
|
+
## 0.1.21 — 2026-08-31
|
|
16
|
+
|
|
17
|
+
- Preserved the projection-cache `put()` Promise contract when a tombstone suppresses a deleted Session write, so DSH `v0.1.2-alpha.2` cold-read write-back remains fail-soft instead of throwing a synchronous `TypeError`.
|
|
18
|
+
- Added direct `put(...).catch(...)` regression coverage for alpha.2 while retaining the `putSoft` path used by DSH `v0.1.1-rc.2`, and clarified the separate safe cache domain and migration behavior.
|
|
19
|
+
|
|
20
|
+
Published package: [`@michengai/dsh-archive-manager@0.1.21`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.21).
|
|
21
|
+
|
|
7
22
|
## 0.1.20 — 2026-08-31
|
|
8
23
|
|
|
9
24
|
- Kept IM and other non-filesystem-safe Session ids unchanged while persisting projection checkpoints under fixed-length safe keys, preventing DSH `v0.1.2-alpha.2` from using colons as Windows filenames.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
以下发布说明会持续保留;新增版本时不再删除较早记录。
|
|
6
6
|
|
|
7
|
+
## 0.1.22 — 2026-09-01
|
|
8
|
+
|
|
9
|
+
- 在「设置 → 已归档」新增任意多选:每行复选框、全选当前筛选结果、跨项目保留选择、批量恢复和确认后的批量永久删除。
|
|
10
|
+
- 所选操作复用宿主权威批量路径,陈旧记录清理、部分删除失败和重试行为与项目级及全部操作保持一致。
|
|
11
|
+
- 恢复聊天后刷新客户端会话投影,使其立刻重新出现在原工作区。
|
|
12
|
+
|
|
13
|
+
已发布包:[`@michengai/dsh-archive-manager@0.1.22`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.22)。
|
|
14
|
+
|
|
15
|
+
## 0.1.21 — 2026-08-31
|
|
16
|
+
|
|
17
|
+
- 墓碑拦截已删除会话的投影缓存写入时仍保持 `put()` 的 Promise 契约,使 DSH `v0.1.2-alpha.2` 冷读写回继续按 fail-soft 方式处理,而不会同步抛出 `TypeError`。
|
|
18
|
+
- 补充 alpha.2 直接调用 `put(...).catch(...)` 的回归测试,同时保留 DSH `v0.1.1-rc.2` 使用的 `putSoft` 路径覆盖,并澄清独立安全缓存域与迁移行为。
|
|
19
|
+
|
|
20
|
+
已发布包:[`@michengai/dsh-archive-manager@0.1.21`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.21)。
|
|
21
|
+
|
|
7
22
|
## 0.1.20 — 2026-08-31
|
|
8
23
|
|
|
9
24
|
- 保持 IM 等不适合作文件名的会话 ID 不变,将投影检查点改用固定长度安全键落盘,避免 DSH `v0.1.2-alpha.2` 在 Windows 上把冒号直接用作文件名。
|
package/lib/client.js
CHANGED
|
@@ -49,7 +49,8 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
|
|
50
50
|
if (value.scope === "all" || value.scope === "ungrouped") return value;
|
|
51
51
|
if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
|
|
52
|
-
|
|
52
|
+
if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
|
|
53
|
+
throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
|
|
53
54
|
}
|
|
54
55
|
};
|
|
55
56
|
const unarchivedBatchSchema = {
|
|
@@ -283,6 +284,195 @@ window.__ModuleLoader__.load({
|
|
|
283
284
|
}
|
|
284
285
|
});
|
|
285
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* 归档多选原型。保留既有设置页作为兼容回退,原型直接使用相同宿主
|
|
289
|
+
* 批量接口,因此其恢复、删除和并发安全语义与原有项目级操作一致。
|
|
290
|
+
*/
|
|
291
|
+
function ArchivedSessionsSectionPrototype({ sessionStore, workspaceStore, unarchiveSession, deleteSession, unarchiveSessions, deleteArchivedSessions, archivedSessionMetadata, t }) {
|
|
292
|
+
const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
|
|
293
|
+
const workspaceState = (0, react.useSyncExternalStore)(workspaceStore.subscribe, workspaceStore.getSnapshot);
|
|
294
|
+
const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
|
|
295
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
296
|
+
const [error, setError] = (0, react.useState)(null);
|
|
297
|
+
const [notice, setNotice] = (0, react.useState)(null);
|
|
298
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
299
|
+
const [project, setProject] = (0, react.useState)("all");
|
|
300
|
+
const [sortBy, setSortBy] = (0, react.useState)("updated");
|
|
301
|
+
const [createdAtById, setCreatedAtById] = (0, react.useState)({});
|
|
302
|
+
const [unarchivingSessionIds, setUnarchivingSessionIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
303
|
+
const unarchivingSessionIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
304
|
+
const [selectedSessionIds, setSelectedSessionIds] = (0, react.useState)([]);
|
|
305
|
+
const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
|
|
306
|
+
const sortedGroups = (0, react.useMemo)(() => sortArchivedGroups(groups, sortBy, createdAtById, t), [groups, sortBy, createdAtById, t]);
|
|
307
|
+
(0, react.useEffect)(() => {
|
|
308
|
+
let cancelled = false;
|
|
309
|
+
archivedSessionMetadata().then((result) => {
|
|
310
|
+
if (!cancelled) setCreatedAtById(Object.fromEntries(result.items.map((item) => [item.sessionId, item.createdAt])));
|
|
311
|
+
}).catch(() => {
|
|
312
|
+
if (!cancelled) setCreatedAtById({});
|
|
313
|
+
});
|
|
314
|
+
return () => {
|
|
315
|
+
cancelled = true;
|
|
316
|
+
};
|
|
317
|
+
}, [archivedSessionMetadata, workspaceState.archivedSessionIds]);
|
|
318
|
+
(0, react.useEffect)(() => {
|
|
319
|
+
if (project !== "all" && !groups.some((group) => group.key === project)) setProject("all");
|
|
320
|
+
}, [groups, project]);
|
|
321
|
+
const filteredGroups = (0, react.useMemo)(() => {
|
|
322
|
+
const normalizedQuery = query.trim().toLocaleLowerCase();
|
|
323
|
+
return sortedGroups.filter((group) => project === "all" || project === group.key).map((group) => ({
|
|
324
|
+
...group,
|
|
325
|
+
sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
|
|
326
|
+
})).filter((group) => group.sessions.length > 0);
|
|
327
|
+
}, [sortedGroups, project, query, t]);
|
|
328
|
+
const visibleSessionIds = (0, react.useMemo)(() => archivedSessionIdsInGroups(filteredGroups), [filteredGroups]);
|
|
329
|
+
const selectedSessionIdSet = (0, react.useMemo)(() => new Set(selectedSessionIds), [selectedSessionIds]);
|
|
330
|
+
const selectedVisibleCount = visibleSessionIds.filter((sessionId) => selectedSessionIdSet.has(sessionId)).length;
|
|
331
|
+
const allVisibleSelected = visibleSessionIds.length > 0 && selectedVisibleCount === visibleSessionIds.length;
|
|
332
|
+
const allBatchTarget = { scope: "all" };
|
|
333
|
+
const allBatchSessionIds = (0, react.useMemo)(() => deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, allBatchTarget), [workspaceState.archivedSessionIds, workspaceState.items]);
|
|
334
|
+
(0, react.useEffect)(() => {
|
|
335
|
+
setSelectedSessionIds((current) => pruneArchivedSelection(current, workspaceState.archivedSessionIds));
|
|
336
|
+
}, [workspaceState.archivedSessionIds]);
|
|
337
|
+
const toggleSessionSelection = (sessionId, checked) => {
|
|
338
|
+
setSelectedSessionIds((current) => toggleArchivedSelection(current, [sessionId], checked));
|
|
339
|
+
};
|
|
340
|
+
const toggleVisibleSelection = (checked) => {
|
|
341
|
+
setSelectedSessionIds((current) => toggleArchivedSelection(current, visibleSessionIds, checked));
|
|
342
|
+
};
|
|
343
|
+
const onUnarchive = (sessionId) => {
|
|
344
|
+
if (busy || unarchivingSessionIdsRef.current.has(sessionId)) return;
|
|
345
|
+
unarchivingSessionIdsRef.current.add(sessionId);
|
|
346
|
+
setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
|
|
347
|
+
setError(null);
|
|
348
|
+
setNotice(null);
|
|
349
|
+
unarchiveSession(sessionId).catch((reason) => {
|
|
350
|
+
setError(formatUnarchiveError(reason, t));
|
|
351
|
+
}).finally(() => {
|
|
352
|
+
unarchivingSessionIdsRef.current.delete(sessionId);
|
|
353
|
+
setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
const onBatchUnarchive = async (target) => {
|
|
357
|
+
if (busy) return;
|
|
358
|
+
setBusy(true);
|
|
359
|
+
setError(null);
|
|
360
|
+
setNotice(null);
|
|
361
|
+
try {
|
|
362
|
+
const result = await unarchiveSessions(target);
|
|
363
|
+
setNotice(t("archives.restoreSuccess", { n: result.unarchivedSessionIds.length }));
|
|
364
|
+
return result;
|
|
365
|
+
} catch (reason) {
|
|
366
|
+
setError(t("archives.restoreBatchFailed", { detail: reason instanceof Error ? reason.message : String(reason) }));
|
|
367
|
+
} finally {
|
|
368
|
+
setBusy(false);
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
const onSelectedUnarchive = async () => {
|
|
372
|
+
if (selectedSessionIds.length === 0) return;
|
|
373
|
+
const result = await onBatchUnarchive({ scope: "sessions", sessionIds: selectedSessionIds });
|
|
374
|
+
if (result !== void 0) {
|
|
375
|
+
const restored = new Set(result.unarchivedSessionIds);
|
|
376
|
+
setSelectedSessionIds((current) => current.filter((sessionId) => !restored.has(sessionId)));
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const closeDelete = () => {
|
|
380
|
+
if (!busy) setDeleteTarget(null);
|
|
381
|
+
};
|
|
382
|
+
(0, react.useEffect)(() => {
|
|
383
|
+
if (deleteTarget === null) return;
|
|
384
|
+
const onKeyDown = (event) => {
|
|
385
|
+
if (event.key !== "Escape") return;
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
event.stopPropagation();
|
|
388
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
389
|
+
if (!busy) setDeleteTarget(null);
|
|
390
|
+
};
|
|
391
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
392
|
+
return () => window.removeEventListener("keydown", onKeyDown, true);
|
|
393
|
+
}, [deleteTarget, busy]);
|
|
394
|
+
const confirmDelete = async () => {
|
|
395
|
+
if (busy || deleteTarget === null) return;
|
|
396
|
+
setBusy(true);
|
|
397
|
+
setError(null);
|
|
398
|
+
setNotice(null);
|
|
399
|
+
try {
|
|
400
|
+
if (deleteTarget.kind === "batch") {
|
|
401
|
+
const result = await deleteArchivedSessions(deleteTarget.target);
|
|
402
|
+
const feedback = archivedDeleteFeedback(result, t);
|
|
403
|
+
if (feedback.kind === "error") setError(feedback.message);
|
|
404
|
+
else setNotice(feedback.message);
|
|
405
|
+
if (deleteTarget.target.scope === "sessions") {
|
|
406
|
+
const completed = new Set([...result.deletedSessionIds, ...result.skippedSessionIds]);
|
|
407
|
+
setSelectedSessionIds((current) => current.filter((sessionId) => !completed.has(sessionId)));
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
await deleteSession(deleteTarget.session.id);
|
|
411
|
+
}
|
|
412
|
+
setDeleteTarget(null);
|
|
413
|
+
} catch (reason) {
|
|
414
|
+
setError(formatDeleteError(reason, t));
|
|
415
|
+
} finally {
|
|
416
|
+
setBusy(false);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const batchScope = deleteTarget?.kind === "batch" ? deleteTarget.target.scope : null;
|
|
420
|
+
const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : batchScope === "sessions" ? t("archives.deleteSelectedTitle") : t("deleteSession.title");
|
|
421
|
+
const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : batchScope === "sessions" ? t("archives.deleteSelectedDesc", { n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
|
|
422
|
+
const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : batchScope === "sessions" ? t("archives.deleteSelectedConfirm") : t("deleteSession.title");
|
|
423
|
+
return (0, react_jsx_runtime.jsxs)("section", {
|
|
424
|
+
className: "dsham_settings",
|
|
425
|
+
"aria-label": t("archives.title"),
|
|
426
|
+
children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS + ARCHIVE_SETTINGS_BATCH_CSS + ARCHIVE_SETTINGS_SELECTION_CSS }), (0, react_jsx_runtime.jsxs)("header", {
|
|
427
|
+
className: "dsham_settingsHeader",
|
|
428
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
429
|
+
children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })]
|
|
430
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
431
|
+
className: "dsham_settingsHeaderActions",
|
|
432
|
+
children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsRestoreAll", disabled: busy || allBatchSessionIds.length === 0, onClick: () => onBatchUnarchive(allBatchTarget), children: t("archives.restoreAll") }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || allBatchSessionIds.length === 0, onClick: () => setDeleteTarget({ kind: "batch", target: allBatchTarget, title: t("archives.allProjects"), count: allBatchSessionIds.length }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })]
|
|
433
|
+
})]
|
|
434
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
435
|
+
className: "dsham_settingsToolbar",
|
|
436
|
+
children: [(0, react_jsx_runtime.jsxs)("label", {
|
|
437
|
+
className: "dsham_settingsSearch",
|
|
438
|
+
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })]
|
|
439
|
+
}), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-sort-filter", value: sortBy, options: [{ value: "updated", label: t("archives.sortUpdated") }, { value: "created", label: t("archives.sortCreated") }, { value: "alphabetical", label: t("archives.sortAlphabetical") }], onChange: setSortBy, "aria-label": t("archives.sortBy") }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...sortedGroups.map((group) => ({ value: group.key, label: group.title }))], onChange: setProject, "aria-label": t("archives.projectFilter") })]
|
|
440
|
+
}), groups.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
441
|
+
className: "dsham_settingsSelection",
|
|
442
|
+
children: [(0, react_jsx_runtime.jsxs)("label", {
|
|
443
|
+
className: "dsham_settingsSelectionToggle",
|
|
444
|
+
children: [(0, react_jsx_runtime.jsx)(ArchiveSelectionCheckbox, { checked: allVisibleSelected, indeterminate: selectedVisibleCount > 0 && !allVisibleSelected, disabled: busy || visibleSessionIds.length === 0, label: t("archives.selectAllFiltered"), onChange: (event) => toggleVisibleSelection(event.target.checked) }), t("archives.selectAllFiltered")]
|
|
445
|
+
}), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsSelectionCount", children: t("archives.selectedCount", { n: selectedSessionIds.length }) }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsSelectionAction", disabled: busy || selectedSessionIds.length === 0, onClick: onSelectedUnarchive, children: t("archives.restoreSelected") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsSelectionAction dsham_settingsSelectionDelete", disabled: busy || selectedSessionIds.length === 0, onClick: () => setDeleteTarget({ kind: "batch", target: { scope: "sessions", sessionIds: selectedSessionIds }, count: selectedSessionIds.length }), children: t("archives.deleteSelected") })]
|
|
446
|
+
}), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => {
|
|
447
|
+
const target = archivedBatchTargetForGroup(group.key);
|
|
448
|
+
const count = deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, target).length;
|
|
449
|
+
return (0, react_jsx_runtime.jsxs)("section", {
|
|
450
|
+
className: "dsham_settingsGroup",
|
|
451
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
452
|
+
className: "dsham_settingsGroupHeading",
|
|
453
|
+
children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupMeta", children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: count }) }), (0, react_jsx_runtime.jsx)(ArchivedGroupActions, { group, busy, onRestore: () => onBatchUnarchive(target), onDelete: () => setDeleteTarget({ kind: "batch", target, title: group.title, count }), t })] })]
|
|
454
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
455
|
+
className: "dsham_settingsList",
|
|
456
|
+
children: group.sessions.map((session) => (0, react_jsx_runtime.jsxs)("article", {
|
|
457
|
+
className: "dsham_settingsRow",
|
|
458
|
+
children: [(0, react_jsx_runtime.jsx)(ArchiveSelectionCheckbox, { checked: selectedSessionIdSet.has(session.id), disabled: busy, label: t("archives.selectSession", { name: displayTitle(session, t) }), onChange: (event) => toggleSessionSelection(session.id, event.target.checked) }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsContent", children: [(0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsTitle", children: displayTitle(session, t) }), (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsMeta", children: archiveTimeLabel(session.updatedAt, t) })] }), (0, react_jsx_runtime.jsxs)("div", {
|
|
459
|
+
className: "dsham_settingsActions",
|
|
460
|
+
children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy || unarchivingSessionIds.has(session.id), onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy || unarchivingSessionIds.has(session.id), "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget({ kind: "session", session }), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
|
|
461
|
+
})]
|
|
462
|
+
}, session.id))
|
|
463
|
+
})]
|
|
464
|
+
}, group.key);
|
|
465
|
+
}), error !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsError", role: "alert", children: error }), notice !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsStatus", role: "status", children: notice }), (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
466
|
+
open: deleteTarget !== null,
|
|
467
|
+
onClose: closeDelete,
|
|
468
|
+
closeLabel: t("close"),
|
|
469
|
+
title: deleteDialogTitle,
|
|
470
|
+
...deleteDialogDescription === void 0 ? {} : { description: deleteDialogDescription },
|
|
471
|
+
footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, { variant: "outline", disabled: busy, onClick: closeDelete, children: t("cancel") }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, { variant: "outline", disabled: busy, onClick: confirmDelete, children: deleteConfirmLabel })] }),
|
|
472
|
+
children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.kind === "batch" ? t("archives.deleteBatchPending") : t("deleteSession.pending") })
|
|
473
|
+
})]
|
|
474
|
+
});
|
|
475
|
+
}
|
|
286
476
|
//#endregion
|
|
287
477
|
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
288
478
|
function r(e) {
|
|
@@ -2622,6 +2812,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
2812
|
//#region dsh-archive-manager: settings section
|
|
2623
2813
|
const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{position:relative;min-width:168px;flex:none}.dsham_selectTrigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;line-height:20px;text-align:left}.dsham_selectTrigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_selectTrigger[aria-expanded='true']{border-color:var(--dsw-alias-state-success-primary)}.dsham_selectValue{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsham_selectCaret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dsham_selectMenu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;min-width:100%;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dsham_selectOption{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dsham_selectOption:hover,.dsham_selectOption[data-active='true']{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectOption[aria-selected='true']{color:var(--dsw-alias-state-success-primary)}.dsham_settingsGroup{margin:0 0 20px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
|
|
2624
2814
|
const ARCHIVE_SETTINGS_BATCH_CSS = ".dsham_settingsHeaderActions,.dsham_settingsGroupMeta{display:flex;align-items:center;gap:8px;flex:none}.dsham_settingsRestoreAll{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsRestoreAll:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsRestoreAll:disabled,.dsham_settingsDanger:disabled,.dsham_settingsGroupMenu:disabled{cursor:not-allowed;opacity:.5}.dsham_settingsGroupMenu{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsGroupMenu:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsStatus{margin-top:10px;color:var(--dsw-alias-label-secondary);font-size:12px}@media(max-width:720px){.dsham_settingsHeader{flex-direction:column}.dsham_settingsHeaderActions{align-self:flex-end}}";
|
|
2815
|
+
const ARCHIVE_SETTINGS_SELECTION_CSS = ".dsham_settingsSelection{display:flex;align-items:center;gap:10px;min-height:40px;margin:-8px 0 16px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsSelectionToggle{display:inline-flex;align-items:center;gap:8px;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}.dsham_settingsCheckbox{width:16px;height:16px;margin:0;accent-color:#3b82f6}.dsham_settingsSelectionCount{margin-right:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsSelectionAction{min-height:28px;padding:0 10px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;cursor:pointer;font:inherit;font-size:12px;font-weight:500}.dsham_settingsSelectionAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsSelectionDelete{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.dsham_settingsSelectionAction:disabled{cursor:not-allowed;opacity:.5}@media(max-width:720px){.dsham_settingsSelection{flex-wrap:wrap}.dsham_settingsSelectionCount{margin-right:0}.dsham_settingsSelectionAction{margin-left:auto}.dsham_settingsSelectionAction+.dsham_settingsSelectionAction{margin-left:-2px}}";
|
|
2625
2816
|
const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
|
|
2626
2817
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
|
|
2627
2818
|
const tag = document.createElement("style");
|
|
@@ -2762,6 +2953,22 @@ window.__ModuleLoader__.load({
|
|
|
2762
2953
|
}) : null]
|
|
2763
2954
|
});
|
|
2764
2955
|
}
|
|
2956
|
+
/** 原生 checkbox 保留浏览器的正确键盘语义,并显式同步三态显示。 */
|
|
2957
|
+
function ArchiveSelectionCheckbox({ checked, indeterminate = false, disabled = false, label, onChange }) {
|
|
2958
|
+
const inputRef = (0, react.useRef)(null);
|
|
2959
|
+
(0, react.useEffect)(() => {
|
|
2960
|
+
if (inputRef.current !== null) inputRef.current.indeterminate = indeterminate;
|
|
2961
|
+
}, [indeterminate]);
|
|
2962
|
+
return (0, react_jsx_runtime.jsx)("input", {
|
|
2963
|
+
ref: inputRef,
|
|
2964
|
+
type: "checkbox",
|
|
2965
|
+
className: "dsham_settingsCheckbox",
|
|
2966
|
+
checked,
|
|
2967
|
+
disabled,
|
|
2968
|
+
"aria-label": label,
|
|
2969
|
+
onChange
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2765
2972
|
/** 项目标题右侧的批量恢复/删除菜单,复用宿主菜单组件的键盘和焦点行为。 */
|
|
2766
2973
|
function ArchivedGroupActions({ group, busy, onRestore, onDelete, t }) {
|
|
2767
2974
|
const [open, setOpen] = (0, react.useState)(false);
|
|
@@ -2805,6 +3012,10 @@ window.__ModuleLoader__.load({
|
|
|
2805
3012
|
function deriveArchivedBatchIds(archivedSessionIds, items, target) {
|
|
2806
3013
|
const ids = [...new Set(archivedSessionIds ?? [])];
|
|
2807
3014
|
if (target.scope === "all") return ids;
|
|
3015
|
+
if (target.scope === "sessions") {
|
|
3016
|
+
const selected = new Set(target.sessionIds ?? []);
|
|
3017
|
+
return ids.filter((id) => selected.has(id));
|
|
3018
|
+
}
|
|
2808
3019
|
if (target.scope === "workspace") {
|
|
2809
3020
|
const accounted = new Set(items.find((workspace) => workspace.workspaceId === target.workspaceId)?.sessionIds ?? []);
|
|
2810
3021
|
return ids.filter((id) => accounted.has(id));
|
|
@@ -2812,6 +3023,24 @@ window.__ModuleLoader__.load({
|
|
|
2812
3023
|
const accounted = new Set(items.flatMap((workspace) => workspace.sessionIds));
|
|
2813
3024
|
return ids.filter((id) => !accounted.has(id));
|
|
2814
3025
|
}
|
|
3026
|
+
/** 当前筛选结果中的可选会话;按列表显示顺序去重。 */
|
|
3027
|
+
function archivedSessionIdsInGroups(groups) {
|
|
3028
|
+
return [...new Set(groups.flatMap((group) => group.sessions.map((session) => session.id)))];
|
|
3029
|
+
}
|
|
3030
|
+
/** 只保留仍属于权威归档集合的选中项,避免对已变化的列表执行旧操作。 */
|
|
3031
|
+
function pruneArchivedSelection(selectedSessionIds, archivedSessionIds) {
|
|
3032
|
+
const archived = new Set(archivedSessionIds);
|
|
3033
|
+
return [...new Set(selectedSessionIds)].filter((id) => archived.has(id));
|
|
3034
|
+
}
|
|
3035
|
+
/** 向当前选择中加入或移除一组可见会话,供“全选筛选结果”复用。 */
|
|
3036
|
+
function toggleArchivedSelection(selectedSessionIds, sessionIds, checked) {
|
|
3037
|
+
const selected = new Set(selectedSessionIds);
|
|
3038
|
+
for (const sessionId of sessionIds) {
|
|
3039
|
+
if (checked) selected.add(sessionId);
|
|
3040
|
+
else selected.delete(sessionId);
|
|
3041
|
+
}
|
|
3042
|
+
return [...selected];
|
|
3043
|
+
}
|
|
2815
3044
|
/** 把批量删除结果转换为准确区分“已删除”与“陈旧标记已清理”的反馈。 */
|
|
2816
3045
|
function archivedDeleteFeedback(result, t) {
|
|
2817
3046
|
const deleted = result.deletedSessionIds.length;
|
|
@@ -2898,6 +3127,7 @@ window.__ModuleLoader__.load({
|
|
|
2898
3127
|
const [createdAtById, setCreatedAtById] = (0, react.useState)({});
|
|
2899
3128
|
const [unarchivingSessionIds, setUnarchivingSessionIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
2900
3129
|
const unarchivingSessionIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
3130
|
+
const [selectedSessionIds, setSelectedSessionIds] = (0, react.useState)([]);
|
|
2901
3131
|
const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
|
|
2902
3132
|
const sortedGroups = (0, react.useMemo)(() => sortArchivedGroups(groups, sortBy, createdAtById, t), [groups, sortBy, createdAtById, t]);
|
|
2903
3133
|
(0, react.useEffect)(() => {
|
|
@@ -2926,8 +3156,21 @@ window.__ModuleLoader__.load({
|
|
|
2926
3156
|
sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
|
|
2927
3157
|
})).filter((group) => group.sessions.length > 0);
|
|
2928
3158
|
}, [sortedGroups, project, query, t]);
|
|
3159
|
+
const visibleSessionIds = (0, react.useMemo)(() => archivedSessionIdsInGroups(filteredGroups), [filteredGroups]);
|
|
3160
|
+
const selectedSessionIdSet = (0, react.useMemo)(() => new Set(selectedSessionIds), [selectedSessionIds]);
|
|
3161
|
+
const selectedVisibleCount = visibleSessionIds.filter((sessionId) => selectedSessionIdSet.has(sessionId)).length;
|
|
3162
|
+
const allVisibleSelected = visibleSessionIds.length > 0 && selectedVisibleCount === visibleSessionIds.length;
|
|
2929
3163
|
const allBatchTarget = { scope: "all" };
|
|
2930
3164
|
const allBatchSessionIds = (0, react.useMemo)(() => deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, allBatchTarget), [workspaceState.archivedSessionIds, workspaceState.items]);
|
|
3165
|
+
(0, react.useEffect)(() => {
|
|
3166
|
+
setSelectedSessionIds((current) => pruneArchivedSelection(current, workspaceState.archivedSessionIds));
|
|
3167
|
+
}, [workspaceState.archivedSessionIds]);
|
|
3168
|
+
const toggleSessionSelection = (sessionId, checked) => {
|
|
3169
|
+
setSelectedSessionIds((current) => toggleArchivedSelection(current, [sessionId], checked));
|
|
3170
|
+
};
|
|
3171
|
+
const toggleVisibleSelection = (checked) => {
|
|
3172
|
+
setSelectedSessionIds((current) => toggleArchivedSelection(current, visibleSessionIds, checked));
|
|
3173
|
+
};
|
|
2931
3174
|
const onUnarchive = (sessionId) => {
|
|
2932
3175
|
if (busy || unarchivingSessionIdsRef.current.has(sessionId)) return;
|
|
2933
3176
|
unarchivingSessionIdsRef.current.add(sessionId);
|
|
@@ -2949,12 +3192,21 @@ window.__ModuleLoader__.load({
|
|
|
2949
3192
|
try {
|
|
2950
3193
|
const result = await unarchiveSessions(target);
|
|
2951
3194
|
setNotice(t("archives.restoreSuccess", { n: result.unarchivedSessionIds.length }));
|
|
3195
|
+
return result;
|
|
2952
3196
|
} catch (reason) {
|
|
2953
3197
|
setError(t("archives.restoreBatchFailed", { detail: reason instanceof Error ? reason.message : String(reason) }));
|
|
2954
3198
|
} finally {
|
|
2955
3199
|
setBusy(false);
|
|
2956
3200
|
}
|
|
2957
3201
|
};
|
|
3202
|
+
const onSelectedUnarchive = async () => {
|
|
3203
|
+
if (selectedSessionIds.length === 0) return;
|
|
3204
|
+
const result = await onBatchUnarchive({ scope: "sessions", sessionIds: selectedSessionIds });
|
|
3205
|
+
if (result !== void 0) {
|
|
3206
|
+
const restored = new Set(result.unarchivedSessionIds);
|
|
3207
|
+
setSelectedSessionIds((current) => current.filter((sessionId) => !restored.has(sessionId)));
|
|
3208
|
+
}
|
|
3209
|
+
};
|
|
2958
3210
|
const closeDelete = () => {
|
|
2959
3211
|
if (!busy) setDeleteTarget(null);
|
|
2960
3212
|
};
|
|
@@ -2981,6 +3233,10 @@ window.__ModuleLoader__.load({
|
|
|
2981
3233
|
const feedback = archivedDeleteFeedback(result, t);
|
|
2982
3234
|
if (feedback.kind === "error") setError(feedback.message);
|
|
2983
3235
|
else setNotice(feedback.message);
|
|
3236
|
+
if (deleteTarget.target.scope === "sessions") {
|
|
3237
|
+
const completed = new Set([...result.deletedSessionIds, ...result.skippedSessionIds]);
|
|
3238
|
+
setSelectedSessionIds((current) => current.filter((sessionId) => !completed.has(sessionId)));
|
|
3239
|
+
}
|
|
2984
3240
|
} else {
|
|
2985
3241
|
await deleteSession(deleteTarget.session.id);
|
|
2986
3242
|
}
|
|
@@ -2992,9 +3248,9 @@ window.__ModuleLoader__.load({
|
|
|
2992
3248
|
}
|
|
2993
3249
|
};
|
|
2994
3250
|
const batchScope = deleteTarget?.kind === "batch" ? deleteTarget.target.scope : null;
|
|
2995
|
-
const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : t("deleteSession.title");
|
|
2996
|
-
const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
|
|
2997
|
-
const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : t("deleteSession.title");
|
|
3251
|
+
const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : batchScope === "sessions" ? t("archives.deleteSelectedTitle") : t("deleteSession.title");
|
|
3252
|
+
const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : batchScope === "sessions" ? t("archives.deleteSelectedDesc", { n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
|
|
3253
|
+
const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : batchScope === "sessions" ? t("archives.deleteSelectedConfirm") : t("deleteSession.title");
|
|
2998
3254
|
return (0, react_jsx_runtime.jsxs)("section", {
|
|
2999
3255
|
className: "dsham_settings",
|
|
3000
3256
|
"aria-label": t("archives.title"),
|
|
@@ -3075,6 +3331,11 @@ window.__ModuleLoader__.load({
|
|
|
3075
3331
|
"archives.projectFilter": "按项目筛选",
|
|
3076
3332
|
"archives.allProjects": "所有项目",
|
|
3077
3333
|
"archives.sessionCount": "{n} 个聊天",
|
|
3334
|
+
"archives.selectAllFiltered": "全选当前筛选结果",
|
|
3335
|
+
"archives.selectSession": "选择会话“{name}”",
|
|
3336
|
+
"archives.selectedCount": "已选 {n} 个",
|
|
3337
|
+
"archives.restoreSelected": "恢复所选",
|
|
3338
|
+
"archives.deleteSelected": "删除所选",
|
|
3078
3339
|
"archives.timestamp": "{date},{time}",
|
|
3079
3340
|
"archives.restoreAll": "全部恢复",
|
|
3080
3341
|
"archives.restoreProject": "恢复该项目的全部聊天",
|
|
@@ -3088,6 +3349,9 @@ window.__ModuleLoader__.load({
|
|
|
3088
3349
|
"archives.deleteAll": "全部删除",
|
|
3089
3350
|
"archives.deleteAllTitle": "删除全部已归档聊天",
|
|
3090
3351
|
"archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理(含正在运行的)和记录,此操作不可恢复。",
|
|
3352
|
+
"archives.deleteSelectedTitle": "删除所选已归档聊天",
|
|
3353
|
+
"archives.deleteSelectedDesc": "将永久删除所选的 {n} 个已归档聊天及其子代理(含正在运行的)和记录。其他聊天不会受影响,此操作不可恢复。",
|
|
3354
|
+
"archives.deleteSelectedConfirm": "删除所选聊天",
|
|
3091
3355
|
"archives.deleteAllPending": "正在删除已归档聊天…",
|
|
3092
3356
|
"archives.deleteProjectTitle": "删除“{name}”中的已归档聊天",
|
|
3093
3357
|
"archives.deleteProjectDesc": "将永久删除“{name}”中的 {n} 个已归档聊天及其子代理和记录。项目目录和未归档聊天不会受影响,此操作不可恢复。",
|
|
@@ -3191,6 +3455,11 @@ window.__ModuleLoader__.load({
|
|
|
3191
3455
|
"archives.projectFilter": "Filter by project",
|
|
3192
3456
|
"archives.allProjects": "All projects",
|
|
3193
3457
|
"archives.sessionCount": "{n} chats",
|
|
3458
|
+
"archives.selectAllFiltered": "Select all results",
|
|
3459
|
+
"archives.selectSession": "Select chat {name}",
|
|
3460
|
+
"archives.selectedCount": "{n} selected",
|
|
3461
|
+
"archives.restoreSelected": "Restore selected",
|
|
3462
|
+
"archives.deleteSelected": "Delete selected",
|
|
3194
3463
|
"archives.timestamp": "{date}, {time}",
|
|
3195
3464
|
"archives.restoreAll": "Restore all",
|
|
3196
3465
|
"archives.restoreProject": "Restore all chats in this project",
|
|
@@ -3204,6 +3473,9 @@ window.__ModuleLoader__.load({
|
|
|
3204
3473
|
"archives.deleteAll": "Delete all",
|
|
3205
3474
|
"archives.deleteAllTitle": "Delete all archived chats",
|
|
3206
3475
|
"archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents (including any that are still running), and their records. This cannot be undone.",
|
|
3476
|
+
"archives.deleteSelectedTitle": "Delete selected archived chats",
|
|
3477
|
+
"archives.deleteSelectedDesc": "This permanently deletes the selected {n} archived chats, their child agents (including any that are still running), and their records. Other chats are not affected. This cannot be undone.",
|
|
3478
|
+
"archives.deleteSelectedConfirm": "Delete selected chats",
|
|
3207
3479
|
"archives.deleteAllPending": "Deleting archived chats…",
|
|
3208
3480
|
"archives.deleteProjectTitle": "Delete archived chats in {name}",
|
|
3209
3481
|
"archives.deleteProjectDesc": "This permanently deletes the {n} archived chats in {name}, their child agents, and their records. The project directory and unarchived chats are not affected. This cannot be undone.",
|
|
@@ -3482,11 +3754,20 @@ window.__ModuleLoader__.load({
|
|
|
3482
3754
|
});
|
|
3483
3755
|
const browserFlowSource = flowSource("sidebar.workspaces.directoryFlow");
|
|
3484
3756
|
const pickerFlowSource = flowSource("conversation.hero.workspace.directoryFlow");
|
|
3757
|
+
const refreshSessionList = async () => {
|
|
3758
|
+
if (typeof ctx.sessions.refresh !== "function") return;
|
|
3759
|
+
try {
|
|
3760
|
+
await ctx.sessions.refresh();
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
console.warn("archive-manager: restored archived sessions but session list refresh failed:", error);
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3485
3765
|
const unarchiveSession = async (sessionId) => {
|
|
3486
3766
|
const registry = ctx.get("remote.workspaceRegistry");
|
|
3487
3767
|
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
3488
3768
|
const result = await registry.unarchiveSession(sessionId);
|
|
3489
3769
|
if (!result.ok) throw new Error(result.error.message);
|
|
3770
|
+
await refreshSessionList();
|
|
3490
3771
|
};
|
|
3491
3772
|
const deleteSession = async (sessionId) => {
|
|
3492
3773
|
const registry = ctx.get("remote.workspaceRegistry");
|
|
@@ -3499,6 +3780,7 @@ window.__ModuleLoader__.load({
|
|
|
3499
3780
|
if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
|
|
3500
3781
|
const result = await registry.unarchiveSessions(target);
|
|
3501
3782
|
if (!result.ok) throw new Error(result.error.message);
|
|
3783
|
+
await refreshSessionList();
|
|
3502
3784
|
return result.value;
|
|
3503
3785
|
};
|
|
3504
3786
|
const deleteArchivedSessions = async (target) => {
|
|
@@ -3605,7 +3887,7 @@ window.__ModuleLoader__.load({
|
|
|
3605
3887
|
archivedSessionMetadata,
|
|
3606
3888
|
t: ctx.locale.bind(NS)
|
|
3607
3889
|
})
|
|
3608
|
-
},
|
|
3890
|
+
}, ArchivedSessionsSectionPrototype));
|
|
3609
3891
|
}
|
|
3610
3892
|
//#endregion
|
|
3611
3893
|
/** Pure derivation surface for dsh-archive-manager self-tests (no-op for the runtime). */
|
|
@@ -3620,6 +3902,9 @@ window.__ModuleLoader__.load({
|
|
|
3620
3902
|
deriveArchivedGroups,
|
|
3621
3903
|
sortArchivedGroups,
|
|
3622
3904
|
deriveArchivedBatchIds,
|
|
3905
|
+
archivedSessionIdsInGroups,
|
|
3906
|
+
pruneArchivedSelection,
|
|
3907
|
+
toggleArchivedSelection,
|
|
3623
3908
|
archivedDeleteFeedback,
|
|
3624
3909
|
createWorkspaceViewStore,
|
|
3625
3910
|
bindObservable,
|
package/lib/projcache.js
CHANGED
|
@@ -122,10 +122,11 @@ async function importMissingRecords(ctx, target, records, label) {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Seed the compatibility domain without
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
125
|
+
* Seed the compatibility domain without opening alpha.2's unsafe per-record
|
|
126
|
+
* source while the v3 whole-file cache contains rows. An empty v3 source may
|
|
127
|
+
* fall through to the current source; source-open failures stay fail-soft.
|
|
128
|
+
* Imports are add-only: a newer compatibility-domain checkpoint always wins,
|
|
129
|
+
* and a partially completed import resumes on the next startup.
|
|
129
130
|
*/
|
|
130
131
|
async function importPreviousProjectionCache(ctx, target) {
|
|
131
132
|
const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
|
|
@@ -142,17 +143,19 @@ async function importPreviousProjectionCache(ctx, target) {
|
|
|
142
143
|
/**
|
|
143
144
|
* dsh-archive-manager projcache 半边。
|
|
144
145
|
*
|
|
145
|
-
* `ArchiveProjectionCache` 继承上游 `SessionProjectionCache
|
|
146
|
-
*
|
|
147
|
-
*
|
|
146
|
+
* `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
|
|
147
|
+
* `sessionProjectionCache` 与 fail-soft 写路径,但使用独立、路径安全的
|
|
148
|
+
* `session_projcache_archive_manager` 兼容域,为归档管理的
|
|
149
|
+
* `deleteSession` 增加三处守护:
|
|
148
150
|
*
|
|
149
|
-
* - `delete(id)` -
|
|
150
|
-
*
|
|
151
|
+
* - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
|
|
152
|
+
* 写入前先登记墓碑。
|
|
151
153
|
* - `whenIdle()` - 全部在途公开写入落定后 resolve。
|
|
152
154
|
* - 墓碑挡住已删除会话的写入:迟到的写入要么被直接拦截,要么在
|
|
153
155
|
* 落定后补删自己的残留行,不会复活已删除的缓存条目。守护覆盖
|
|
154
|
-
* `write()`(dispose 写后路径)与 `
|
|
155
|
-
*
|
|
156
|
+
* `write()`(dispose 写后路径)与 `put()`(rc.2 的 `putSoft` 最终调用
|
|
157
|
+
* 它,alpha.2 的 `coldSnapshot` 则直接调用它),两代落盘路径都纳入
|
|
158
|
+
* `whenIdle` 跟踪。
|
|
156
159
|
*
|
|
157
160
|
* 默认导出是 Service 子类(与上游 `@deepseek-ai/dsh-session-projection-cache`
|
|
158
161
|
* 包同形),profile 补丁可直接替换 `session-projection-cache` 服务行,
|
|
@@ -201,7 +204,9 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
|
|
|
201
204
|
* 直接经 `put`;把墓碑与 whenIdle 跟踪放在这里可同时兼容两代实现。
|
|
202
205
|
*/
|
|
203
206
|
put(id, identity, rows) {
|
|
204
|
-
|
|
207
|
+
// alpha.2 coldSnapshot unconditionally chains `.catch()` onto put().
|
|
208
|
+
// A blocked write must therefore preserve the upstream Promise contract.
|
|
209
|
+
if (this.deletedSessionIds.has(id)) return Promise.resolve();
|
|
205
210
|
const task = this.putCore(id, identity, rows);
|
|
206
211
|
this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
|
|
207
212
|
return task;
|
package/lib/workspace.js
CHANGED
|
@@ -84,7 +84,8 @@ const archivedBatchTargetSchema = {
|
|
|
84
84
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
|
|
85
85
|
if (value.scope === "all" || value.scope === "ungrouped") return value;
|
|
86
86
|
if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
|
|
87
|
-
|
|
87
|
+
if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
|
|
88
|
+
throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
91
|
const unarchivedBatchSchema = {
|
|
@@ -400,6 +401,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
400
401
|
const state = this.requireState();
|
|
401
402
|
const archivedSessionIds = [...new Set(state.archivedSessionIds)];
|
|
402
403
|
if (target.scope === "all") return archivedSessionIds;
|
|
404
|
+
if (target.scope === "sessions") {
|
|
405
|
+
const selected = new Set(target.sessionIds);
|
|
406
|
+
return archivedSessionIds.filter((id) => selected.has(id));
|
|
407
|
+
}
|
|
403
408
|
if (target.scope === "workspace") {
|
|
404
409
|
const workspace = this.requireTable().get(target.workspaceId);
|
|
405
410
|
if (workspace === void 0) throw new Error(`unknown workspace "${target.workspaceId}"`);
|