@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 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.
@@ -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
- throw new TypeError("target.scope must be all, ungrouped, or workspace with a non-empty workspaceId");
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
- }, ArchivedSessionsSection));
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 ever opening alpha.2's unsafe
126
- * per-record source while a v3 whole-file cache is present. Imports are
127
- * add-only: a newer compatibility-domain checkpoint always wins, and a
128
- * partially completed import resumes on the next startup.
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
- * (服务名 `sessionProjectionCache`,同域、同 fail-soft 写路径),
147
- * 为归档管理的 `deleteSession` 增加三处守护:
146
+ * `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
147
+ * `sessionProjectionCache` 与 fail-soft 写路径,但使用独立、路径安全的
148
+ * `session_projcache_archive_manager` 兼容域,为归档管理的
149
+ * `deleteSession` 增加三处守护:
148
150
  *
149
- * - `delete(id)` - 永久移除一个会话的缓存投影行
150
- * (`session_projcache` 域上的 `table.delete`),写入前先登记墓碑。
151
+ * - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
152
+ * 写入前先登记墓碑。
151
153
  * - `whenIdle()` - 全部在途公开写入落定后 resolve。
152
154
  * - 墓碑挡住已删除会话的写入:迟到的写入要么被直接拦截,要么在
153
155
  * 落定后补删自己的残留行,不会复活已删除的缓存条目。守护覆盖
154
- * `write()`(dispose 写后路径)与 `putSoft()`(`coldSnapshot` 的
155
- * 冷读写回路径),两条落盘路径都纳入 `whenIdle` 跟踪。
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
- if (this.deletedSessionIds.has(id)) return;
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
- throw new TypeError("target.scope must be all, ungrouped, or workspace with a non-empty workspaceId");
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}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",