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 +2 -1
- package/assets/screenshots/preview-01.png +0 -0
- package/assets/screenshots/preview-02.png +0 -0
- package/assets/screenshots/preview-03.png +0 -0
- package/assets/screenshots/preview-04.png +0 -0
- package/assets/screenshots/preview-05.png +0 -0
- package/assets/screenshots/preview-06.png +0 -0
- package/assets/screenshots/preview-07.png +0 -0
- package/assets/screenshots/preview-08.png +0 -0
- package/docs/ARCHITECTURE.en.md +2 -0
- package/docs/ARCHITECTURE.md +1 -1
- package/lib/client.js +217 -172
- package/lib/index.js +18 -0
- package/lib/persistence-compat.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
[](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,
|
|
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
|
package/docs/ARCHITECTURE.en.md
CHANGED
|
@@ -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.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
|
2819
|
-
const
|
|
2820
|
-
const
|
|
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-
|
|
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: (
|
|
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
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
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.
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
4533
|
-
|
|
4575
|
+
children: t("bulk.delete")
|
|
4576
|
+
}),
|
|
4534
4577
|
(0, jsx.jsx)("button", {
|
|
4535
4578
|
type: "button",
|
|
4536
|
-
className: "dac-bulk-btn",
|
|
4537
|
-
|
|
4538
|
-
|
|
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: () =>
|
|
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
|
|
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.
|
|
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",
|