dsh-archived-chats 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ [![DSH Insights health](https://dsh-insights.com/badge/Ultronen/dsh-archived-chats.svg)](https://dsh-insights.com/p/Ultronen/dsh-archived-chats/)
1
2
  <p align="center">
2
3
  <img src="assets/brand/session-archive-banner.png" alt="Session Archive: local-first archived-chat center for DeepSeek Harness" width="100%">
3
4
  </p>
@@ -58,7 +59,7 @@ dsh plugin --profile web update dsh-archived-chats
58
59
  | **Read-only preview** | Native conversation layout for Markdown, reasoning, tool activity, JSON, code, and available stored images, with responsive turn navigation. |
59
60
  | **Local History** | Validated versions captured after archive, read-only snapshot preview, confirmed deletion, clear-history, and **Restore as copy** without overwriting the source. |
60
61
  | **Backup and restore** | JSON + Markdown ZIP export with preview-first, conflict-safe import. Existing session IDs are never overwritten. |
61
- | **Recoverable deletion** | Snapshot-protected Recycle Bin with immediate Undo, two-level restore, and separately confirmed permanent deletion. |
62
+ | **Recoverable deletion** | Snapshot-protected Recycle Bin with immediate Undo, two-level restore, separately confirmed permanent deletion, and an optional direct permanent-delete action for users who do not need recovery. |
62
63
  | **Storage and relationships** | Separate storage accounting, preview-first retention policies, and read-only Origins & Branches for forks and subagent trees. |
63
64
 
64
65
  ## Safety by design
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -128,6 +128,8 @@ Protection manifests use `dsh-archived-chats/snapshot` v1 and session payloads u
128
128
 
129
129
  Move ordering is: validate archive ownership → dispose or park a live session → capture and verify snapshot → recheck ownership → atomically commit `trashed` → invalidate caches. Ordinary move never removes the persistence artifact.
130
130
 
131
+ Archived sessions also expose an explicit direct permanent-delete action. It uses the same session-scoped physical-delete checks and crash-recovery bracket as Recycle Bin purge, but intentionally skips snapshot creation and recycle storage after a separate irreversible confirmation. Ordinary delete remains recoverable by default.
132
+
131
133
  Restore first rejects an existing-ID conflict. With an intact original it restores archive visibility and removes only the recycle record, without rewriting persistence; the snapshot remains history. With a missing original it completes validation and attachment-identity republishing before writing through public `create` / `append` / `saveImage` capabilities. A failure rolls back the new artifact and retains trash.
132
134
 
133
135
  Permanent purge persists `purge-pending` before physical writes, then removes every snapshot for that source, then the original session, and finally the recycle record. The session delete is deliberately last: a failure before it leaves the original intact and the record completable, rather than a `purge-pending` record whose session is already gone and which can therefore neither restore nor complete. The snapshot sweep attributes each published snapshot by manifest identity so a snapshot that fails validation is still removed when it belongs to this session, and an unrelated unverifiable snapshot is skipped instead of aborting the sweep — corruption elsewhere in the store must never make a purge impossible. The recycle record also names its own snapshot id, covering one damaged past attribution. Workspace, metadata, snapshot, or physical-delete failures retain `purge-pending`; snapshot deletion is rescanned before success can be returned. Physical deletion additionally requires the located artifact to sit in a directory named for the session itself, so a backend layout that shares one parent between sessions can never have that parent removed. Startup recovery retries only `purge-pending`, never plain `trashed`. Legacy `pending-deletions.json` is strict read-only migration input: each still-archived ID becomes recoverable trash and is never boot-deleted merely because of the old marker.
@@ -160,7 +160,7 @@ client.js 注册 order 30 的 `settings.section` 与 `shell.overlay`,并使用
160
160
  - 历史响应不包含工作区/快照/附件路径、原始事件、备注或确认 token;日志只记 ID 和稳定代码。
161
161
  - 工作区归档响应只公开安全的工作区 ID/标题、符合条件数量、已确认预览中的会话 ID/标题/时间和稳定的逐项结果;工作区路径、事件正文、备注、附件路径与确认凭据不会进入日志或渲染结果。
162
162
  - 导入限制 ZIP 大小、条目数量、路径格式、版本和 JSON 结构,拒绝遍历、重复和原型污染字段。
163
- - 普通删除从不调用物理清除;仅已提交回收记录可进入 purge
163
+ - 普通删除从不调用物理清除;仅已提交回收记录可进入 purge。归档列表还提供单独确认的直接永久删除操作,跳过快照和回收站,但复用同一套会话目录边界、崩溃恢复和失败保护;普通删除仍默认可恢复。
164
164
  - 快照和回收文件使用 `0600`,目录使用 `0700`;快照文件在 sync 前以可写句柄重新打开,发布顺序为临时写入、sync、原子 rename,以保持 Windows、macOS 和 Linux 的持久化语义一致。
165
165
  - 物理 purge 删除快照副本,但不承诺立即清理 Harness 全局附件库中仍被其他会话引用的字节。
166
166
  - 未知宿主能力必须降级或返回明确错误,不得猜测内部对象结构。
package/lib/client.js CHANGED
@@ -297,7 +297,7 @@ window.__ModuleLoader__.load({
297
297
  "retention.preview": "预览清理",
298
298
  "retention.apply": "应用所选清理",
299
299
  "retention.empty": "当前策略没有清理候选",
300
- "retention.saved": "策略已保存,不会自动执行清理",
300
+ "retention.saved": "策略已保存",
301
301
  "retention.note": "保留数量按每个原会话分别计算;保存策略不会删除任何数据,必须再预览并应用清理。",
302
302
  "retention.snapshotCandidate": "历史快照清理",
303
303
  "retention.trashCandidate": "永久删除回收站会话",
@@ -345,8 +345,7 @@ window.__ModuleLoader__.load({
345
345
  "lineage.diagnostic.cycle": "检测到循环",
346
346
  "lineage.diagnostic.delegation-depth-mismatch": "委派深度不一致",
347
347
  "action.import": "导入备份",
348
- "action.export": "导出备份",
349
- "action.more": "更多",
348
+ "action.export": "全部导出",
350
349
  "delete.all": "全部移至回收站",
351
350
  "import.action": "导入备份",
352
351
  "import.title": "导入归档备份",
@@ -366,7 +365,8 @@ window.__ModuleLoader__.load({
366
365
  "import.done": "备份恢复完成",
367
366
  "export.all": "全部导出",
368
367
  "export.selected": "导出选中项",
369
- "export.row": "导出备份",
368
+ "export.row": "导出本条",
369
+ "menu.row": "更多聊天操作",
370
370
  "export.started": "已开始下载备份",
371
371
  "search.placeholder": "搜索标题、标签、备注和聊天内容",
372
372
  "search.loading": "正在搜索聊天内容…",
@@ -406,11 +406,10 @@ window.__ModuleLoader__.load({
406
406
  "sort.newest": "最新优先",
407
407
  "sort.oldest": "最早优先",
408
408
  "sort.title": "按标题",
409
- "selection.visible": "选择当前结果",
409
+ "selection.projectVisible": "全选当前显示的聊天",
410
410
  "selection.group": "选择此项目",
411
411
  "selection.start": "批量选择",
412
412
  "selection.done": "完成",
413
- "selection.clear": "清除",
414
413
  "bulk.unarchive": "取消归档",
415
414
  "bulk.delete": "移至回收站",
416
415
  "group.noProject": "未分组",
@@ -418,6 +417,7 @@ window.__ModuleLoader__.load({
418
417
  "chat.unarchive": "取消归档",
419
418
  "menu.unarchiveAll": "全部取消归档",
420
419
  "menu.deleteAll": "全部移至回收站",
420
+ "menu.deletePermanently": "全部永久删除",
421
421
  "group.collapse": "折叠",
422
422
  "group.expand": "展开",
423
423
  "confirm.deleteOne.title": "移至回收站?",
@@ -425,6 +425,9 @@ window.__ModuleLoader__.load({
425
425
  "confirm.deleteAll.title": "全部移至回收站?",
426
426
  "confirm.deleteGroup.title": "将该项目下的已归档聊天移至回收站?",
427
427
  "confirm.deleteSelected.title": "将选中的已归档聊天移至回收站?",
428
+ "confirm.permanent.title": "直接永久删除已归档聊天?",
429
+ "confirm.permanent.body": "这些聊天不会进入回收站,删除后无法恢复。",
430
+ "confirm.permanent.confirm": "永久删除",
428
431
  "confirm.cancel": "取消",
429
432
  "confirm.delete": "移至回收站",
430
433
  "trash.restore": "恢复",
@@ -597,7 +600,7 @@ window.__ModuleLoader__.load({
597
600
  "retention.preview": "Preview cleanup",
598
601
  "retention.apply": "Apply selected cleanup",
599
602
  "retention.empty": "The current policy has no cleanup candidates",
600
- "retention.saved": "Policy saved; no cleanup ran automatically",
603
+ "retention.saved": "Policy saved",
601
604
  "retention.note": "The retention count applies separately to each original chat. Saving never deletes data; preview and apply cleanup separately.",
602
605
  "retention.snapshotCandidate": "Historical snapshot cleanup",
603
606
  "retention.trashCandidate": "Permanently delete recycled chat",
@@ -645,8 +648,7 @@ window.__ModuleLoader__.load({
645
648
  "lineage.diagnostic.cycle": "Cycle detected",
646
649
  "lineage.diagnostic.delegation-depth-mismatch": "Delegation depth mismatch",
647
650
  "action.import": "Import backup",
648
- "action.export": "Export backup",
649
- "action.more": "More",
651
+ "action.export": "Export all",
650
652
  "delete.all": "Move all to Recycle Bin",
651
653
  "import.action": "Import backup",
652
654
  "import.title": "Import archived backup",
@@ -666,7 +668,8 @@ window.__ModuleLoader__.load({
666
668
  "import.done": "Backup restore completed",
667
669
  "export.all": "Export all",
668
670
  "export.selected": "Export selected",
669
- "export.row": "Export backup",
671
+ "export.row": "Export this chat",
672
+ "menu.row": "More chat actions",
670
673
  "export.started": "Backup download started",
671
674
  "search.placeholder": "Search titles, tags, notes, and conversation content",
672
675
  "search.loading": "Searching conversation content…",
@@ -706,11 +709,10 @@ window.__ModuleLoader__.load({
706
709
  "sort.newest": "Newest first",
707
710
  "sort.oldest": "Oldest first",
708
711
  "sort.title": "Title",
709
- "selection.visible": "Select visible chats",
712
+ "selection.projectVisible": "Select all visible chats",
710
713
  "selection.group": "Select this project",
711
714
  "selection.start": "Select multiple",
712
715
  "selection.done": "Done",
713
- "selection.clear": "Clear",
714
716
  "bulk.unarchive": "Unarchive",
715
717
  "bulk.delete": "Move to Recycle Bin",
716
718
  "group.noProject": "Ungrouped",
@@ -718,6 +720,7 @@ window.__ModuleLoader__.load({
718
720
  "chat.unarchive": "Unarchive",
719
721
  "menu.unarchiveAll": "Unarchive all",
720
722
  "menu.deleteAll": "Move all to Recycle Bin",
723
+ "menu.deletePermanently": "Delete all permanently",
721
724
  "group.collapse": "Collapse",
722
725
  "group.expand": "Expand",
723
726
  "confirm.deleteOne.title": "Move to Recycle Bin?",
@@ -725,6 +728,9 @@ window.__ModuleLoader__.load({
725
728
  "confirm.deleteAll.title": "Move all to Recycle Bin?",
726
729
  "confirm.deleteGroup.title": "Move this project's archived chats to Recycle Bin?",
727
730
  "confirm.deleteSelected.title": "Move selected archived chats to Recycle Bin?",
731
+ "confirm.permanent.title": "Permanently delete archived chats directly?",
732
+ "confirm.permanent.body": "These chats will skip the Recycle Bin and cannot be recovered after deletion.",
733
+ "confirm.permanent.confirm": "Delete permanently",
728
734
  "confirm.cancel": "Cancel",
729
735
  "confirm.delete": "Move to Recycle Bin",
730
736
  "trash.restore": "Restore",
@@ -1211,6 +1217,14 @@ window.__ModuleLoader__.load({
1211
1217
  .dac-menu-item.dac-danger{color:var(--dsw-alias-state-error-primary)}
1212
1218
  .dac-menu-item.dac-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}
1213
1219
  .dac-list{display:flex;flex-direction:column;gap:8px}
1220
+ .dac-group>.dac-list{gap:0}
1221
+ .dac-group>.dac-list>.dac-row{border:0;border-bottom:1px solid var(--dsw-alias-border-l2);border-radius:0}
1222
+ .dac-row-more{position:relative;flex:none}
1223
+ .dac-row-more>summary{list-style:none}
1224
+ .dac-row-more>summary::-webkit-details-marker{display:none}
1225
+ .dac-row-more[open]{z-index:41}
1226
+ .dac-row-menu{top:34px;min-width:180px;max-width:calc(100vw - 48px)}
1227
+ .dac-row-menu .dac-menu-item{display:flex;align-items:center;gap:8px}
1214
1228
  .dac-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:12px 16px;transition:border-color .15s,background .15s}
1215
1229
  .dac-row:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
1216
1230
  .dac-row.dac-selected{border-color:color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 45%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 6%,var(--dsw-alias-bg-layer-1))}
@@ -2806,7 +2820,48 @@ window.__ModuleLoader__.load({
2806
2820
  ] }, group.key);
2807
2821
  }
2808
2822
 
2809
- function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, onExport, onPreview, contentHits, busy, selectionMode, selected, onToggleSelected, stats, metadataStatus, onEditMetadata }) {
2823
+ function ChatActionMenu({ t, title, children }) {
2824
+ const menuRef = _react.useRef(null);
2825
+ _react.useEffect(() => {
2826
+ const closeOutside = (event) => {
2827
+ const menu = menuRef.current;
2828
+ if (menu?.open && !menu.contains(event.target)) menu.open = false;
2829
+ };
2830
+ document.addEventListener("pointerdown", closeOutside);
2831
+ document.addEventListener("focusin", closeOutside);
2832
+ return () => {
2833
+ document.removeEventListener("pointerdown", closeOutside);
2834
+ document.removeEventListener("focusin", closeOutside);
2835
+ };
2836
+ }, []);
2837
+ return (0, jsx.jsxs)("details", {
2838
+ ref: menuRef,
2839
+ className: "dac-row-more",
2840
+ onKeyDown: (event) => {
2841
+ if (event.key === "Escape" && menuRef.current?.open) {
2842
+ event.preventDefault();
2843
+ event.stopPropagation();
2844
+ menuRef.current.open = false;
2845
+ menuRef.current.querySelector("summary")?.focus();
2846
+ }
2847
+ },
2848
+ children: [
2849
+ (0, jsx.jsx)("summary", { className: "dac-iconbtn", "aria-label": `${t("menu.row")}: ${title}`, title: t("menu.row"), children: (0, jsx.jsx)(IconDots, {}) }),
2850
+ (0, jsx.jsx)("div", {
2851
+ className: "dac-menu dac-row-menu",
2852
+ onClickCapture: (event) => {
2853
+ if (event.target.closest("button:not(:disabled)") && menuRef.current) {
2854
+ menuRef.current.open = false;
2855
+ menuRef.current.querySelector("summary")?.focus();
2856
+ }
2857
+ },
2858
+ children
2859
+ })
2860
+ ]
2861
+ });
2862
+ }
2863
+
2864
+ function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, onPermanentDelete, onExport, onPreview, contentHits, busy, selectionMode, selected, onToggleSelected, stats, metadataStatus, onEditMetadata }) {
2810
2865
  const wrapRef = _react.useRef(null);
2811
2866
  _react.useEffect(() => {
2812
2867
  if (!menuOpen) return void 0;
@@ -2815,9 +2870,10 @@ window.__ModuleLoader__.load({
2815
2870
  return () => document.removeEventListener("mousedown", onDown);
2816
2871
  }, [menuOpen, onToggleMenu]);
2817
2872
  const ids = group.selectionIds;
2818
- const selectedInGroup = ids.filter((id) => selected.has(id)).length;
2819
- const allSelected = ids.length > 0 && selectedInGroup === ids.length;
2820
- const someSelected = selectedInGroup > 0 && !allSelected;
2873
+ const visibleIds = group.items.map((session) => session.id);
2874
+ const selectedCount = visibleIds.filter((id) => selected.has(id)).length;
2875
+ const allSelected = visibleIds.length > 0 && selectedCount === visibleIds.length;
2876
+ const partiallySelected = selectedCount > 0 && !allSelected;
2821
2877
  const groupBusy = ids.some((id) => busy[id] === true);
2822
2878
  return (0, jsx.jsxs)("div", {
2823
2879
  className: "dac-group",
@@ -2829,13 +2885,6 @@ window.__ModuleLoader__.load({
2829
2885
  (0, jsx.jsxs)("div", {
2830
2886
  className: "dac-group-left",
2831
2887
  children: [
2832
- selectionMode && (0, jsx.jsx)(SelectionCheckbox, {
2833
- checked: allSelected,
2834
- indeterminate: someSelected,
2835
- disabled: groupBusy,
2836
- ariaLabel: selectProjectLabel(t, group.title),
2837
- onChange: (checked) => onToggleSelected(ids, checked)
2838
- }),
2839
2888
  (0, jsx.jsxs)("button", {
2840
2889
  type: "button",
2841
2890
  className: "dac-group-toggle",
@@ -2853,6 +2902,16 @@ window.__ModuleLoader__.load({
2853
2902
  (0, jsx.jsxs)("div", {
2854
2903
  className: "dac-group-side",
2855
2904
  children: [
2905
+ visibleIds.length > 0 && (0, jsx.jsx)(SelectionCheckbox, {
2906
+ checked: allSelected,
2907
+ indeterminate: partiallySelected,
2908
+ disabled: visibleIds.length === 0 || visibleIds.some((id) => busy[id] === true),
2909
+ ariaLabel: `${t("selection.projectVisible")}${isZh(t) ? ":" : ": "}${group.title}`,
2910
+ onChange: (checked) => {
2911
+ onToggleSelected(visibleIds, checked);
2912
+ if (checked && collapsed) onToggleCollapsed(group.key);
2913
+ }
2914
+ }),
2856
2915
  (0, jsx.jsx)("span", { className: "dac-count", children: chatsCount(t, group.items.length) }),
2857
2916
  (0, jsx.jsx)("button", {
2858
2917
  type: "button",
@@ -2871,6 +2930,7 @@ window.__ModuleLoader__.load({
2871
2930
  type: "button",
2872
2931
  className: "dac-menu-item",
2873
2932
  role: "menuitem",
2933
+ disabled: groupBusy,
2874
2934
  onClick: () => { onToggleMenu(null); onUnarchive(ids); },
2875
2935
  children: t("menu.unarchiveAll")
2876
2936
  }),
@@ -2878,8 +2938,17 @@ window.__ModuleLoader__.load({
2878
2938
  type: "button",
2879
2939
  className: "dac-menu-item dac-danger",
2880
2940
  role: "menuitem",
2941
+ disabled: groupBusy,
2881
2942
  onClick: () => { onToggleMenu(null); onDelete(ids, group.title); },
2882
2943
  children: t("menu.deleteAll")
2944
+ }),
2945
+ (0, jsx.jsx)("button", {
2946
+ type: "button",
2947
+ className: "dac-menu-item dac-danger",
2948
+ role: "menuitem",
2949
+ disabled: groupBusy,
2950
+ onClick: () => { onToggleMenu(null); onPermanentDelete(ids); },
2951
+ children: t("menu.deletePermanently")
2883
2952
  })
2884
2953
  ]
2885
2954
  })
@@ -2931,37 +3000,38 @@ window.__ModuleLoader__.load({
2931
3000
  onClick: () => onPreview(session),
2932
3001
  children: (0, jsx.jsx)(IconEye, {})
2933
3002
  }),
3003
+ (0, jsx.jsx)("button", {
3004
+ type: "button",
3005
+ className: "dac-iconbtn dac-danger",
3006
+ "aria-label": t("confirm.permanent.confirm"),
3007
+ title: t("confirm.permanent.confirm"),
3008
+ disabled: busy[session.id] === true,
3009
+ onClick: () => onPermanentDelete([session.id]),
3010
+ children: (0, jsx.jsx)(IconTrash, {})
3011
+ }),
3012
+ (0, jsx.jsx)("button", {
3013
+ type: "button", className: "dac-unarchive",
3014
+ disabled: busy[session.id] === true,
3015
+ onClick: () => onUnarchive([session.id]), children: t("chat.unarchive")
3016
+ }),
3017
+ (0, jsx.jsxs)(ChatActionMenu, { t, title: session.title ?? t("chat.untitled"), children: [
2934
3018
  (0, jsx.jsx)("button", {
2935
3019
  type: "button",
2936
- className: "dac-iconbtn",
3020
+ className: "dac-menu-item",
2937
3021
  "aria-label": t("tag.edit"),
2938
3022
  disabled: busy[session.id] === true || metadataStatus === "unavailable",
2939
3023
  onClick: () => onEditMetadata(session),
2940
- children: (0, jsx.jsx)(IconEdit, {})
2941
- }),
2942
- (0, jsx.jsx)("button", {
2943
- type: "button",
2944
- className: "dac-iconbtn",
2945
- "aria-label": t("export.row"),
2946
- disabled: busy[session.id] === true,
2947
- onClick: () => onExport([session.id]),
2948
- children: (0, jsx.jsx)(IconDownload, {})
2949
- }),
2950
- (0, jsx.jsx)("button", {
2951
- type: "button",
2952
- className: "dac-iconbtn dac-danger",
2953
- "aria-label": t("menu.deleteAll"),
2954
- disabled: busy[session.id] === true,
2955
- onClick: () => onDelete([session.id], null),
2956
- children: (0, jsx.jsx)(IconTrash, {})
3024
+ children: t("tag.edit")
2957
3025
  }),
2958
- (0, jsx.jsx)("button", {
2959
- type: "button",
2960
- className: "dac-unarchive",
2961
- disabled: busy[session.id] === true,
2962
- onClick: () => onUnarchive([session.id]),
2963
- children: t("chat.unarchive")
2964
- })
3026
+ (0, jsx.jsx)("button", {
3027
+ type: "button",
3028
+ className: "dac-menu-item",
3029
+ "aria-label": t("export.row"),
3030
+ disabled: busy[session.id] === true,
3031
+ onClick: () => onExport([session.id]),
3032
+ children: t("export.row")
3033
+ }),
3034
+ ] })
2965
3035
  ]
2966
3036
  })
2967
3037
  ]
@@ -3089,6 +3159,7 @@ window.__ModuleLoader__.load({
3089
3159
  const [selected, setSelected] = _react.useState(() => new Set());
3090
3160
  const [busy, setBusy] = _react.useState(false);
3091
3161
  const [message, setMessage] = _react.useState(null);
3162
+ const [messageKind, setMessageKind] = _react.useState("error");
3092
3163
  const [detailsMode, setDetailsMode] = _react.useState(null);
3093
3164
  const previewReturnFocusRef = _react.useRef(null);
3094
3165
  const detailsReturnFocusRef = _react.useRef(null);
@@ -3103,15 +3174,20 @@ window.__ModuleLoader__.load({
3103
3174
  } catch (error) { if (!signal?.aborted) setState({ status: "error", value: null, error }); }
3104
3175
  }, []);
3105
3176
  _react.useEffect(() => { const controller = new AbortController(); void load(controller.signal); return () => controller.abort(); }, [load]);
3177
+ _react.useEffect(() => {
3178
+ if (message === null || messageKind !== "ok") return undefined;
3179
+ const timer = setTimeout(() => setMessage(null), 3000);
3180
+ return () => clearTimeout(timer);
3181
+ }, [message, messageKind]);
3106
3182
  const setNumber = (key, value, nullable) => setDraft((current) => ({ ...current, [key]: nullable && value === "" ? null : Number(value) }));
3107
3183
  const save = async () => {
3108
- setBusy(true); setMessage(null);
3109
- try { const result = await saveRetentionPolicy(draft); setDraft({ ...(result.policy ?? draft) }); setMessage(t("retention.saved")); await load(); }
3184
+ setBusy(true); setMessage(null); setMessageKind("error");
3185
+ try { const result = await saveRetentionPolicy(draft); setDraft({ ...(result.policy ?? draft) }); setMessageKind("ok"); setMessage(t("retention.saved")); await load(); }
3110
3186
  catch (error) { setMessage(String(error.message ?? error)); }
3111
3187
  finally { setBusy(false); }
3112
3188
  };
3113
3189
  const openPreview = async () => {
3114
- setBusy(true); setMessage(null);
3190
+ setBusy(true); setMessage(null); setMessageKind("error");
3115
3191
  try { previewReturnFocusRef.current = document.activeElement; const value = await previewRetention(); setPreview(value); setSelected(defaultRetentionSelection(value.candidates)); }
3116
3192
  catch (error) { setMessage(String(error.message ?? error)); }
3117
3193
  finally { setBusy(false); }
@@ -3121,7 +3197,7 @@ window.__ModuleLoader__.load({
3121
3197
  setDetailsMode(mode);
3122
3198
  };
3123
3199
  const applySelected = async () => {
3124
- setBusy(true);
3200
+ setBusy(true); setMessage(null); setMessageKind("error");
3125
3201
  try { const result = await applyRetention(preview.token, preview.nonce, [...selected]); const applied = result?.applied ?? []; const failed = result?.failed ?? []; setMessage(`${t("retention.result")}: ${applied.length} / ${failed.length}${failed.length > 0 ? ` · ${failed.map((item) => `${item.key}: ${item.reason}`).join(" · ")}` : ""}`); setPreview(null); setSelected(new Set()); await load(); }
3126
3202
  catch (error) { const body = error.body ?? {}; const applied = body.applied ?? []; const failed = body.failed ?? []; setMessage(failed.length > 0 ? `${t("retention.result")}: ${applied.length} / ${failed.length} · ${failed.map((item) => `${item.key}: ${item.reason}`).join(" · ")}` : String(error.message ?? error)); setPreview(null); setSelected(new Set()); }
3127
3203
  finally { setBusy(false); }
@@ -3143,7 +3219,7 @@ window.__ModuleLoader__.load({
3143
3219
  (0, jsx.jsx)("strong", { children: display }),
3144
3220
  mode && (0, jsx.jsx)("button", { type: "button", className: "dac-insights-open", style: { alignSelf: "center" }, "aria-label": t(actionLabel), disabled: count === 0, onClick: () => openDetails(mode), children: t("insights.details") })
3145
3221
  ] }, label)) }),
3146
- message && (0, jsx.jsx)("div", { className: "dac-notice", role: "status", children: message }),
3222
+ message && (0, jsx.jsx)("div", { className: messageKind === "ok" ? "dac-toast" : "dac-notice", role: "status", children: message }),
3147
3223
  draft && (0, jsx.jsxs)("div", { className: "dac-policy", children: [
3148
3224
  (0, jsx.jsxs)("label", { children: [(0, jsx.jsx)("span", { children: t("retention.history") }), (0, jsx.jsx)("input", { type: "number", min: 0, max: 20, value: draft.historicalSnapshotsPerSession, onChange: (event) => setNumber("historicalSnapshotsPerSession", event.target.value, false) })] }),
3149
3225
  (0, jsx.jsxs)("label", { children: [(0, jsx.jsx)("span", { children: t("retention.snapshotAge") }), (0, jsx.jsx)("input", { type: "number", min: 1, max: 3650, placeholder: t("retention.disabled"), value: draft.historicalSnapshotMaxAgeDays ?? "", onChange: (event) => setNumber("historicalSnapshotMaxAgeDays", event.target.value, true) })] }),
@@ -3649,8 +3725,7 @@ window.__ModuleLoader__.load({
3649
3725
  const [metaBusy, setMetaBusy] = _react.useState(false);
3650
3726
  const [importPreview, setImportPreview] = _react.useState(null);
3651
3727
  const [importBusy, setImportBusy] = _react.useState(false);
3652
- const [selectionMode, setSelectionMode] = _react.useState(false);
3653
- const [actionMenu, setActionMenu] = _react.useState(null);
3728
+ const selectionMode = selected.size > 0;
3654
3729
  const [contentSearch, setContentSearch] = _react.useState({ status: "idle", query: "", hits: [], skipped: [] });
3655
3730
  const [preview, setPreview] = _react.useState(null);
3656
3731
  const [previewBusy, setPreviewBusy] = _react.useState(false);
@@ -3659,8 +3734,6 @@ window.__ModuleLoader__.load({
3659
3734
  const [workspaceArchiveRefresh, setWorkspaceArchiveRefresh] = _react.useState(0);
3660
3735
  const pageRef = _react.useRef(null);
3661
3736
  const pageHeadingRef = _react.useRef(null);
3662
- const actionMenuRef = _react.useRef(null);
3663
- const moreActionRef = _react.useRef(null);
3664
3737
  const deleteReturnFocusRef = _react.useRef(null);
3665
3738
  const trashReturnFocusRef = _react.useRef(null);
3666
3739
  const metaReturnFocusRef = _react.useRef(null);
@@ -3706,25 +3779,6 @@ window.__ModuleLoader__.load({
3706
3779
  previewRequestRef.current.controller?.abort();
3707
3780
  }, []);
3708
3781
 
3709
- _react.useEffect(() => {
3710
- if (actionMenu === null) return undefined;
3711
- const onDown = (event) => {
3712
- if (actionMenuRef.current && !actionMenuRef.current.contains(event.target)) setActionMenu(null);
3713
- };
3714
- document.addEventListener("mousedown", onDown);
3715
- return () => {
3716
- document.removeEventListener("mousedown", onDown);
3717
- };
3718
- }, [actionMenu]);
3719
-
3720
- const closeActionMenuFromKeyboard = (event) => {
3721
- if (event.key !== "Escape" || actionMenu === null) return;
3722
- event.preventDefault();
3723
- event.stopPropagation();
3724
- setActionMenu(null);
3725
- moreActionRef.current?.focus?.();
3726
- };
3727
-
3728
3782
  _react.useEffect(() => {
3729
3783
  if (sessions === null || loadError !== null) return undefined;
3730
3784
  return markArchiveDialog(pageRef.current);
@@ -3859,9 +3913,8 @@ window.__ModuleLoader__.load({
3859
3913
  const toggleSelected = (ids, checked) => {
3860
3914
  setSelected((current) => setVisibleSelection(current, ids, checked));
3861
3915
  };
3862
- const finishSelectionMode = () => {
3916
+ const clearArchiveSelection = () => {
3863
3917
  setSelected(new Set());
3864
- setSelectionMode(false);
3865
3918
  };
3866
3919
 
3867
3920
  const markBusy = (ids, value) => {
@@ -3982,7 +4035,7 @@ window.__ModuleLoader__.load({
3982
4035
  });
3983
4036
  };
3984
4037
 
3985
- const unarchive = async (ids, finishSelectionAfter = false) => {
4038
+ const unarchive = async (ids) => {
3986
4039
  if (ids.length === 0) return;
3987
4040
  markBusy(ids, true);
3988
4041
  setNotice(null);
@@ -3990,7 +4043,6 @@ window.__ModuleLoader__.load({
3990
4043
  await post("/unarchive-all", { sessionIds: ids });
3991
4044
  setSessions((prev) => (prev ?? []).filter((s) => !ids.includes(s.id)));
3992
4045
  setSelected((current) => setVisibleSelection(current, ids, false));
3993
- if (finishSelectionAfter) finishSelectionMode();
3994
4046
  } catch (error) {
3995
4047
  setNotice({ kind: "error", text: String(error.message ?? error) });
3996
4048
  } finally {
@@ -3998,7 +4050,7 @@ window.__ModuleLoader__.load({
3998
4050
  }
3999
4051
  };
4000
4052
 
4001
- const runDelete = async (ids, finishSelectionAfter = false) => {
4053
+ const runDelete = async (ids) => {
4002
4054
  if (ids.length === 0) return;
4003
4055
  markBusy(ids, true);
4004
4056
  setNotice(null);
@@ -4015,7 +4067,6 @@ window.__ModuleLoader__.load({
4015
4067
  }
4016
4068
  if (failed.length > 0) setNotice({ kind: "error", text: failureText(t, failed) });
4017
4069
  else if (trashed.length > 0) setNotice({ kind: "ok", text: t("trash.moved"), action: "undo-trash", ids: trashed });
4018
- if (finishSelectionAfter && failed.length === 0 && trashed.length === ids.length) finishSelectionMode();
4019
4070
  } catch (error) {
4020
4071
  const trashed = uniqueSessionIds(error.body?.trashed);
4021
4072
  const failed = error.body?.failed ?? [];
@@ -4037,6 +4088,37 @@ window.__ModuleLoader__.load({
4037
4088
  }
4038
4089
  };
4039
4090
 
4091
+ const runPermanentDelete = async (ids) => {
4092
+ if (ids.length === 0) return;
4093
+ markBusy(ids, true);
4094
+ setNotice(null);
4095
+ try {
4096
+ const result = await post("/delete-all", { sessionIds: ids, permanent: true });
4097
+ const deleted = uniqueSessionIds(result.deleted);
4098
+ const failed = result.failed ?? [];
4099
+ if (deleted.length > 0) {
4100
+ setSessions((prev) => (prev ?? []).filter((s) => !deleted.includes(s.id)));
4101
+ setSelected((current) => setVisibleSelection(current, deleted, false));
4102
+ setTrash((current) => ({ ...current, status: "idle" }));
4103
+ refreshSidebar?.();
4104
+ }
4105
+ if (failed.length > 0) setNotice({ kind: "error", text: failureText(t, failed) });
4106
+ else if (deleted.length > 0) setNotice({ kind: "ok", text: t("trash.purged") });
4107
+ } catch (error) {
4108
+ const deleted = uniqueSessionIds(error.body?.deleted);
4109
+ const failed = error.body?.failed ?? [];
4110
+ if (deleted.length > 0) {
4111
+ setSessions((prev) => (prev ?? []).filter((s) => !deleted.includes(s.id)));
4112
+ setSelected((current) => setVisibleSelection(current, deleted, false));
4113
+ refreshSidebar?.();
4114
+ }
4115
+ setNotice({ kind: "error", text: failed.length > 0 ? failureText(t, failed) : String(error.message ?? error) });
4116
+ } finally {
4117
+ markBusy(ids, false);
4118
+ setConfirm(null);
4119
+ }
4120
+ };
4121
+
4040
4122
  const undoTrashMove = async (ids) => {
4041
4123
  const requested = uniqueSessionIds(ids);
4042
4124
  if (requested.length === 0) return;
@@ -4063,7 +4145,7 @@ window.__ModuleLoader__.load({
4063
4145
  if (ids.length === 0) return;
4064
4146
  deleteReturnFocusRef.current = document.activeElement;
4065
4147
  if (selectedScope) {
4066
- setConfirm({ title: t("confirm.deleteSelected.title"), body: deleteSelectedBody(t, ids.length), ids, finishSelectionAfter: true });
4148
+ setConfirm({ title: t("confirm.deleteSelected.title"), body: deleteSelectedBody(t, ids.length), ids });
4067
4149
  } else if (ids.length === 1 && groupName === null) {
4068
4150
  setConfirm({ title: t("confirm.deleteOne.title"), body: t("confirm.deleteOne.body"), ids });
4069
4151
  } else if (groupName !== null && groupName !== void 0) {
@@ -4073,6 +4155,17 @@ window.__ModuleLoader__.load({
4073
4155
  }
4074
4156
  };
4075
4157
 
4158
+ const askPermanentDelete = (ids) => {
4159
+ if (ids.length === 0) return;
4160
+ deleteReturnFocusRef.current = document.activeElement;
4161
+ setConfirm({
4162
+ title: t("confirm.permanent.title"),
4163
+ body: `${t("confirm.permanent.body")} ${isZh(t) ? `共 ${ids.length} 个聊天。` : `${ids.length} chats selected.`}`,
4164
+ ids,
4165
+ permanent: true,
4166
+ });
4167
+ };
4168
+
4076
4169
  const openMetadataEditor = (session) => {
4077
4170
  metaReturnFocusRef.current = document.activeElement;
4078
4171
  setMetadataEdit(session);
@@ -4312,13 +4405,8 @@ window.__ModuleLoader__.load({
4312
4405
  }
4313
4406
 
4314
4407
  const allIds = (sessions ?? []).map((s) => s.id);
4315
- const visibleIds = groups.flatMap((group) => group.items.map((session) => session.id));
4316
4408
  const selectedIds = allIds.filter((id) => selected.has(id));
4317
- const selectedVisibleCount = visibleIds.filter((id) => selected.has(id)).length;
4318
- const allVisibleSelected = visibleIds.length > 0 && selectedVisibleCount === visibleIds.length;
4319
- const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected;
4320
4409
  const selectedBusy = selectedIds.some((id) => busy[id] === true);
4321
- const visibleBusy = visibleIds.some((id) => busy[id] === true);
4322
4410
  const allBusy = allIds.some((id) => busy[id] === true);
4323
4411
  const filtering = query.trim() !== "" || projectFilter !== "all" || typeFilter !== "all" || tagFilter !== "";
4324
4412
  const trashGroups = groupTrashSessions(trash.sessions);
@@ -4337,51 +4425,28 @@ window.__ModuleLoader__.load({
4337
4425
  className: "dac-head",
4338
4426
  children: [
4339
4427
  (0, jsx.jsx)("h2", { ref: pageHeadingRef, tabIndex: -1, className: "dac-title", children: t("page.title") }),
4340
- pageMode === "archived" && (0, jsx.jsxs)("div", {
4341
- ref: actionMenuRef,
4342
- className: "dac-head-actions",
4343
- onKeyDown: closeActionMenuFromKeyboard,
4344
- children: [
4345
- (0, jsx.jsx)("div", { className: "dac-action-wrap", children:
4346
- (0, jsx.jsxs)("button", {
4347
- type: "button",
4348
- className: "dac-action-trigger",
4349
- onClick: openWorkspaceArchive,
4350
- children: [(0, jsx.jsx)(IconArchive, { size: 15 }), (0, jsx.jsx)("span", { children: t("workspaceArchive.settingsAction") })]
4351
- })
4352
- }),
4353
- (0, jsx.jsx)("div", { className: "dac-action-wrap", children:
4354
- (0, jsx.jsxs)("button", {
4355
- type: "button",
4356
- className: "dac-action-trigger",
4357
- disabled: importBusy,
4358
- onClick: () => importInputRef.current?.click?.(),
4359
- children: [(0, jsx.jsx)(IconUpload, {}), (0, jsx.jsx)("span", { children: t("action.import") })]
4360
- })
4428
+ pageMode === "archived" && (0, jsx.jsxs)("div", {
4429
+ className: "dac-head-actions",
4430
+ children: [
4431
+ (0, jsx.jsxs)("button", {
4432
+ type: "button",
4433
+ className: "dac-action-trigger",
4434
+ onClick: openWorkspaceArchive,
4435
+ children: [(0, jsx.jsx)(IconArchive, { size: 15 }), (0, jsx.jsx)("span", { children: t("workspaceArchive.settingsAction") })]
4361
4436
  }),
4362
- (0, jsx.jsx)("div", { className: "dac-action-wrap", children:
4363
- (0, jsx.jsxs)("button", {
4364
- type: "button",
4365
- className: "dac-action-trigger",
4366
- disabled: allIds.length === 0 || allBusy,
4367
- onClick: () => { const didExport = exportSessions(selectedIds.length > 0 ? selectedIds : allIds); if (didExport && selectedIds.length > 0) finishSelectionMode(); },
4368
- children: [(0, jsx.jsx)(IconDownload, {}), (0, jsx.jsx)("span", { children: t("action.export") })]
4369
- })
4370
- }),
4371
- (0, jsx.jsxs)("div", { className: "dac-action-wrap", children: [
4372
- (0, jsx.jsxs)("button", {
4373
- ref: moreActionRef,
4374
- type: "button",
4375
- className: "dac-action-trigger",
4376
- disabled: allIds.length === 0,
4377
- "aria-controls": "dac-more-actions",
4378
- "aria-expanded": actionMenu === "more",
4379
- onClick: () => setActionMenu((current) => current === "more" ? null : "more"),
4380
- children: [(0, jsx.jsx)(IconDots, {}), (0, jsx.jsx)("span", { children: t("action.more") })]
4381
- }),
4382
- actionMenu === "more" && (0, jsx.jsx)("div", { id: "dac-more-actions", className: "dac-action-menu", children: (0, jsx.jsx)("button", { type: "button", className: "dac-action-menu-item dac-danger", onClick: () => { setActionMenu(null); askDelete(allIds, void 0); }, children: t("delete.all") }) })
4383
- ] })
4384
- ]
4437
+ (0, jsx.jsxs)("button", {
4438
+ type: "button",
4439
+ className: "dac-action-trigger",
4440
+ disabled: importBusy,
4441
+ onClick: () => importInputRef.current?.click?.(),
4442
+ children: [(0, jsx.jsx)(IconUpload, {}), (0, jsx.jsx)("span", { children: t("action.import") })]
4443
+ }),
4444
+ (0, jsx.jsxs)("button", {
4445
+ type: "button",
4446
+ className: "dac-action-trigger",
4447
+ disabled: allIds.length === 0 || allBusy,
4448
+ onClick: () => exportSessions(allIds),
4449
+ children: [(0, jsx.jsx)(IconDownload, {}), (0, jsx.jsx)("span", { children: t("action.export") })]
4385
4450
  }),
4386
4451
  (0, jsx.jsx)("input", {
4387
4452
  ref: importInputRef,
@@ -4389,9 +4454,11 @@ window.__ModuleLoader__.load({
4389
4454
  accept: ".zip,application/zip",
4390
4455
  hidden: true,
4391
4456
  onChange: (event) => { const file = event.target.files?.[0]; void importFile(file); event.target.value = ""; }
4392
- }),
4457
+ })
4393
4458
  ]
4394
- }),
4459
+ })
4460
+ ]
4461
+ }),
4395
4462
  (0, jsx.jsxs)("div", { className: "dac-tabs", role: "tablist", "aria-label": t("page.title"), children: [
4396
4463
  (0, jsx.jsx)("button", { type: "button", role: "tab", className: "dac-tab", "aria-selected": pageMode === "archived", onClick: () => setPageMode("archived"), children: t("tab.archived") }),
4397
4464
  (0, jsx.jsx)("button", { type: "button", role: "tab", className: "dac-tab", "aria-selected": pageMode === "history", onClick: () => setPageMode("history"), children: t("tab.history") }),
@@ -4428,7 +4495,7 @@ window.__ModuleLoader__.load({
4428
4495
  type: "text",
4429
4496
  placeholder: t("search.placeholder"),
4430
4497
  value: query,
4431
- onChange: (e) => setQuery(e.target.value)
4498
+ onChange: (e) => { clearArchiveSelection(); setQuery(e.target.value); }
4432
4499
  }),
4433
4500
  contentSearch.status === "loading" && (0, jsx.jsx)("span", { className: "dac-search-state", role: "status", children: t("search.loading") }),
4434
4501
  contentSearch.status === "error" && (0, jsx.jsx)("span", { className: "dac-search-state dac-error", role: "status", children: t("search.error") })
@@ -4439,7 +4506,7 @@ window.__ModuleLoader__.load({
4439
4506
  children: [
4440
4507
  (0, jsx.jsx)(Select, {
4441
4508
  value: typeFilter,
4442
- onChange: setTypeFilter,
4509
+ onChange: (value) => { clearArchiveSelection(); setTypeFilter(value); },
4443
4510
  ariaLabel: t("filter.allChats"),
4444
4511
  options: [
4445
4512
  { value: "all", label: t("filter.allChats") },
@@ -4449,7 +4516,7 @@ window.__ModuleLoader__.load({
4449
4516
  }),
4450
4517
  (0, jsx.jsx)(Select, {
4451
4518
  value: projectFilter,
4452
- onChange: setProjectFilter,
4519
+ onChange: (value) => { clearArchiveSelection(); setProjectFilter(value); },
4453
4520
  ariaLabel: t("filter.allProjects"),
4454
4521
  options: [
4455
4522
  { value: "all", label: t("filter.allProjects") },
@@ -4458,7 +4525,7 @@ window.__ModuleLoader__.load({
4458
4525
  }),
4459
4526
  (0, jsx.jsx)(Select, {
4460
4527
  value: tagFilter,
4461
- onChange: setTagFilter,
4528
+ onChange: (value) => { clearArchiveSelection(); setTagFilter(value); },
4462
4529
  ariaLabel: t("tag.filter"),
4463
4530
  options: [
4464
4531
  { value: "", label: t("tag.filter") },
@@ -4475,30 +4542,6 @@ window.__ModuleLoader__.load({
4475
4542
  { value: "title", label: t("sort.title") }
4476
4543
  ]
4477
4544
  }),
4478
- (visibleIds.length > 0 || selectionMode) && (0, jsx.jsxs)("div", {
4479
- className: "dac-selection-controls",
4480
- children: [
4481
- selectionMode && visibleIds.length > 0 && (0, jsx.jsxs)("label", {
4482
- className: "dac-selection-toggle",
4483
- children: [
4484
- (0, jsx.jsx)(SelectionCheckbox, {
4485
- checked: allVisibleSelected,
4486
- indeterminate: someVisibleSelected,
4487
- disabled: visibleBusy,
4488
- ariaLabel: t("selection.visible"),
4489
- onChange: (checked) => toggleSelected(visibleIds, checked)
4490
- }),
4491
- (0, jsx.jsx)("span", { children: t("selection.visible") })
4492
- ]
4493
- }),
4494
- (0, jsx.jsx)("button", {
4495
- type: "button",
4496
- className: "dac-selection-mode",
4497
- onClick: selectionMode ? finishSelectionMode : () => setSelectionMode(true),
4498
- children: t(selectionMode ? "selection.done" : "selection.start")
4499
- })
4500
- ]
4501
- })
4502
4545
  ]
4503
4546
  }),
4504
4547
  selectedIds.length > 0 && (0, jsx.jsxs)("div", {
@@ -4514,14 +4557,14 @@ window.__ModuleLoader__.load({
4514
4557
  type: "button",
4515
4558
  className: "dac-bulk-btn",
4516
4559
  disabled: selectedBusy,
4517
- onClick: () => { if (exportSessions(selectedIds)) finishSelectionMode(); },
4560
+ onClick: () => exportSessions(selectedIds),
4518
4561
  children: t("export.selected")
4519
4562
  }),
4520
4563
  (0, jsx.jsx)("button", {
4521
4564
  type: "button",
4522
4565
  className: "dac-bulk-btn",
4523
4566
  disabled: selectedBusy,
4524
- onClick: () => unarchive(selectedIds, true),
4567
+ onClick: () => unarchive(selectedIds),
4525
4568
  children: t("bulk.unarchive")
4526
4569
  }),
4527
4570
  (0, jsx.jsx)("button", {
@@ -4529,14 +4572,15 @@ window.__ModuleLoader__.load({
4529
4572
  className: "dac-bulk-btn dac-danger",
4530
4573
  disabled: selectedBusy,
4531
4574
  onClick: () => askDelete(selectedIds, null, true),
4532
- children: t("bulk.delete")
4533
- }),
4575
+ children: t("bulk.delete")
4576
+ }),
4534
4577
  (0, jsx.jsx)("button", {
4535
4578
  type: "button",
4536
- className: "dac-bulk-btn",
4537
- onClick: () => setSelected(new Set()),
4538
- children: t("selection.clear")
4539
- })
4579
+ className: "dac-bulk-btn dac-danger",
4580
+ disabled: selectedBusy,
4581
+ onClick: () => askPermanentDelete(selectedIds),
4582
+ children: t("confirm.permanent.confirm")
4583
+ }),
4540
4584
  ]
4541
4585
  })
4542
4586
  ]
@@ -4577,6 +4621,7 @@ window.__ModuleLoader__.load({
4577
4621
  onToggleMenu: setMenuFor,
4578
4622
  onUnarchive: unarchive,
4579
4623
  onDelete: askDelete,
4624
+ onPermanentDelete: askPermanentDelete,
4580
4625
  onExport: exportSessions,
4581
4626
  onPreview: openPreview,
4582
4627
  contentHits,
@@ -4683,12 +4728,12 @@ window.__ModuleLoader__.load({
4683
4728
  confirm !== null && (0, jsx.jsx)(ConfirmDialog, {
4684
4729
  title: confirm.title,
4685
4730
  body: confirm.body,
4686
- confirmLabel: t("confirm.delete"),
4731
+ confirmLabel: t(confirm.permanent ? "confirm.permanent.confirm" : "confirm.delete"),
4687
4732
  cancelLabel: t("confirm.cancel"),
4688
4733
  busy: confirm.ids.some((id) => busy[id] === true),
4689
4734
  returnFocus: deleteReturnFocusRef.current,
4690
4735
  fallbackFocusRef: pageHeadingRef,
4691
- onConfirm: () => runDelete(confirm.ids, confirm.finishSelectionAfter === true),
4736
+ onConfirm: () => confirm.permanent ? runPermanentDelete(confirm.ids) : runDelete(confirm.ids),
4692
4737
  onCancel: () => setConfirm(null)
4693
4738
  }),
4694
4739
  trashConfirm !== null && (0, jsx.jsx)(ConfirmDialog, {
package/lib/index.js CHANGED
@@ -1729,6 +1729,24 @@ function registerRoutes(ctx, webServer, registry, persistence, titleCache, metad
1729
1729
  send(res, 400, { error: batch ? 'sessionIds-required' : 'sessionId-required' });
1730
1730
  return;
1731
1731
  }
1732
+ if (body.permanent === true) {
1733
+ const deleted = [];
1734
+ const pending = [];
1735
+ const failed = [];
1736
+ for (const id of [...new Set(ids)]) {
1737
+ try {
1738
+ const outcome = await deleteSession(ctx, registry, persistence, id, titleCache, statsService, metadataStore, pendingStore, lifecycle);
1739
+ if (outcome === 'deleted') deleted.push(id);
1740
+ else if (outcome === 'pending') pending.push(id);
1741
+ else failed.push({ id, reason: 'operation-cancelled' });
1742
+ } catch (error) {
1743
+ failed.push({ id, reason: error?.code ?? 'delete-failed' });
1744
+ }
1745
+ }
1746
+ const result = { deleted, pending, failed };
1747
+ send(res, failed.length > 0 && deleted.length === 0 ? 409 : 200, { ok: failed.length === 0, ...result });
1748
+ return;
1749
+ }
1732
1750
  const result = await recycleService.move(ids);
1733
1751
  send(res, result.failed.length > 0 && result.trashed.length === 0 ? 409 : 200, { ok: result.failed.length === 0, ...result });
1734
1752
  } catch (error) {
@@ -10,7 +10,7 @@ function objectLike(value) {
10
10
 
11
11
  function validatedHeader(value, expectedId) {
12
12
  if (!objectLike(value)
13
- || value.version !== 2
13
+ || (value.version !== 2 && value.version !== 3)
14
14
  || typeof value.id !== 'string'
15
15
  || value.id === ''
16
16
  || (expectedId !== undefined && value.id !== expectedId)
@@ -113,7 +113,8 @@ export function resolvePersistenceCompat(raw) {
113
113
  throw invalid('persistence read handle metadata is invalid');
114
114
  }
115
115
  if (handle.inheritedEventCount > 0) throw inspectionUnsupported();
116
- const events = await handle.read.call(handle, 0, undefined);
116
+ const slice = await handle.read.call(handle, 0, undefined);
117
+ const events = Array.isArray(slice) ? slice : slice?.events;
117
118
  if (!Array.isArray(events)) throw invalid('persistence read response must be an event array');
118
119
  return { meta: header, events };
119
120
  } finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-archived-chats",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "DeepSeek Harness 会话档案:从设置中安全批量归档工作区会话,按工作区浏览和全文搜索归档聊天,原生只读预览对话、工具活动和已存储图片,管理标签备注与 ZIP 备份恢复;保留历史版本并恢复为副本,提供可撤销回收站、空间分账、保留策略和来源与分支。所有数据留在本机。 Session Archive for DeepSeek Harness: safely bulk-archive workspace chats from Settings; browse and full-text search archived chats; natively preview conversations, tool activity, and stored images read-only; manage tags, notes, and ZIP backup restore; retain local History with restore-as-copy, an undoable Recycle Bin, storage accounting, retention policies, and Origins & Branches. All data stays local.",
5
5
  "license": "MIT",
6
6
  "author": "Ultronen",