dsh-archived-chats 1.4.0 → 1.4.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/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@
4
4
 
5
5
  Entries describe behavior at each release, not necessarily current behavior. Consult the READMEs and user guides for current usage.
6
6
 
7
+ ## 1.4.2 — 2026-09-22
8
+
9
+ ### 中文
10
+
11
+ - 修复在 DSH `0.1.6-alpha.2` 等新版 Host 上启动即崩溃的问题。导出改用 `fflate`,移除 `zip-stream` 及其 `readable-stream@4` 依赖链:该链会请求带斜杠的内置模块名(`process/`),新版 Host 的模块解析层去掉斜杠后对内建模块调用 `createRequire().resolve.paths()`,得到 `null` 并逐项迭代,从而抛出异常并中止整个 Host 启动。此问题与聊天内容无关,插件在被加载时即触发。
12
+ - 导出的 ZIP 改为同步分片压缩:每个条目按 256 KiB 分片推送,分片之间让出事件循环,避免单次压缩长时间占用主线程;分片大小固定,同一导出计划仍产生字节一致的归档。
13
+ - 新增依赖树回归测试,阻止该依赖链被重新引入;新增下载中途取消的测试,确认中止会销毁流并以调用方错误拒绝完成,不再可能把已取消的导出报告为成功。
14
+
15
+ ### English
16
+
17
+ - Fix the startup crash on DSH `0.1.6-alpha.2` and later Hosts. Export now uses `fflate`, removing `zip-stream` and its `readable-stream@4` chain: that chain requests a builtin module name with a trailing slash (`process/`), and the newer Host's resolution layer strips the slash, calls `createRequire().resolve.paths()` on a builtin, receives `null`, iterates it, and throws — aborting the whole Host boot. The failure is independent of chat content and fires as soon as the plugin loads.
18
+ - ZIP export now compresses synchronously in 256 KiB slices, yielding to the event loop between slices so a single compression never monopolizes the main thread. The slice size is fixed, so one export plan still produces a byte-identical archive.
19
+ - Add a dependency-tree regression test that blocks that chain from returning, and a mid-download cancellation test proving that abort destroys the stream and rejects completion with the caller's error instead of reporting a cancelled export as successful.
20
+
21
+ ## 1.4.1 — 2026-09-21
22
+
23
+ ### 中文
24
+
25
+ - 修复恢复成功提示一直停留的问题:成功提示在 4 秒后收起,悬停或键盘聚焦时暂停计时,切换标签即清除;改善「查看已归档」按钮的明暗主题悬停对比度。
26
+ - 来源范围与回收站自动清理说明改为提示图标,支持鼠标、键盘与触屏;永久删除确认保留完整说明。仅确认有新版时在「关于」标签显示红点。
27
+
28
+ - 英文界面、设置入口与当前文档统一为 Archive Management,对应中文「归档管理」;包名和数据位置不变。
29
+ - 核对中英文 README、用户指南、架构与支持说明,补齐市场安装步骤,明确分叉 ZIP v2 能力及已发布 1.4.0 的英文菜单名称差异。当前 README 不再展示旧名称横幅或已退役流程的历史截图目录。
30
+ - 修正客户端类型说明中「删除会进入回收站」的旧描述:只有工作区菜单能移入回收站,删除需确认且不可恢复。
31
+
32
+ ### English
33
+
34
+ - Fix persistent restore-success notices: dismiss after four seconds, pause on hover or keyboard focus, and clear on tab changes. Improve View Archived button hover contrast in light and dark themes.
35
+ - Move relationship-scope and automatic-cleanup explanations into mouse-, keyboard-, and touch-accessible help controls, keeping full permanent-deletion confirmations. Show an About-tab red dot only for a confirmed available update.
36
+
37
+ - Align the English UI, settings entry, and current documentation on Archive Management, matching 归档管理. The package identifier and data location are unchanged.
38
+ - Audit both READMEs, user guides, architecture, and support notes; add marketplace installation steps, clarify fork ZIP v2 support, and distinguish the English menu label in published 1.4.0. Remove the old-name banner and historical screenshot catalog from current READMEs.
39
+ - Correct the client declaration's outdated claim that Delete moves chats to the Recycle Bin: only workspace actions recycle chats; deletion requires confirmation and is irreversible.
40
+
7
41
  ## 1.4.0 — 2026-09-21
8
42
 
9
43
  ### 中文
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  [![DSH Insights health](https://dsh-insights.com/badge/Ultronen/dsh-archived-chats.svg)](https://dsh-insights.com/p/Ultronen/dsh-archived-chats/)
2
+
2
3
  <p align="center">
3
- <img src="assets/brand/archive-management-banner.png" alt="Session Archive: local-first archived-chat center for DeepSeek Harness" width="100%">
4
+ <img src="assets/brand/archive-management-banner.png" alt="Archive Management: local-first archived-chat center for DeepSeek Harness" width="100%">
4
5
  </p>
5
6
 
6
7
  <div align="center">
7
8
 
8
- <h1>Session Archive</h1>
9
+ <h1>Archive Management</h1>
9
10
 
10
11
  <p><strong>A local-first archived-chat center for DeepSeek Harness</strong></p>
11
12
  <p><code>dsh-archived-chats</code></p>
@@ -27,19 +28,23 @@
27
28
 
28
29
  </div>
29
30
 
30
- Session Archive gives DeepSeek Harness a place to find chats hidden from the main chat area after archiving. It keeps DSH's single-chat archive workflow and adds workspace bulk archiving, search, preview, backup, and a workspace-based Recycle Bin workflow.
31
+ Archive Management gives DeepSeek Harness a place to find chats hidden from the main chat area after archiving. It keeps DSH's single-chat archive workflow and adds workspace bulk archiving, search, preview, backup, and a workspace-based Recycle Bin workflow.
31
32
 
32
- > The English name remains **Session Archive** (formerly **Archived Chats**); the Chinese interface now uses **归档管理**, previously **会话档案**. The rename does not change the package name, install command, or local data location. It does not imply that older snapshots are preserved by later upgrades; see the upgrade warning below.
33
+ > The English name is **Archive Management**, and the Chinese name is **归档管理**. The installation package remains `dsh-archived-chats`.
33
34
  >
34
- > These documents describe dsh-archived-chats 1.4.0. See the [changelog](CHANGELOG.md) for release history.
35
+ > This document follows the `main` branch. This guide targets 1.4.2, whose export writer no longer carries a dependency that prevents startup on newer Hosts. See the [changelog](CHANGELOG.md) for release history, including 1.4.1's English rename and feedback improvements.
35
36
 
36
37
  ## Quick start
37
38
 
39
+ Search for `dsh-archived-chats` in DSH's plugin market, verify the author is **Ultronen**, and choose **Install**. After installation, follow the host's restart instructions. The settings entry is **归档管理** in Chinese; in English it is **Archive Management**.
40
+
41
+ Alternatively, run this command on the computer running DSH:
42
+
38
43
  ```sh
39
44
  dsh plugin --profile web add dsh-archived-chats@latest
40
45
  ```
41
46
 
42
- Restart DSH, then open **Settings → Session Archive**.
47
+ Restart DSH, then open **Settings → Archive Management** (Chinese: **设置 → 归档管理**).
43
48
 
44
49
  Before updating an older installation, read “Upgrading from older releases” below. To update:
45
50
 
@@ -55,11 +60,11 @@ dsh plugin --profile web update dsh-archived-chats
55
60
  | Workspace archiving | A settings-owned workspace chooser supports one or more workspaces and one aggregate confirmation; blank, active, or unreadable chats are skipped. |
56
61
  | Read-only preview | Conversations, reasoning, tool activity, Markdown, JSON, code, and readable stored images without unarchiving. |
57
62
  | Backup | Export one workspace or the entire archive as JSON + Markdown ZIP; preview imports and skip conflicting IDs. |
58
- | Recycle Bin | Move archived chats by workspace, then restore individual chats or a workspace back to Archived. |
63
+ | Recycle Bin | Move archived chats by workspace; restore one chat, a workspace, or the entire Recycle Bin back to Archived. |
59
64
  | Permanent deletion | Delete one archived chat, a workspace's archive, or all archived chats after confirmation. Recycle Bin deletion is separate. |
60
65
  | Storage and relationships | Storage accounting, optional automatic Recycle Bin cleanup, and read-only Origins & Branches. |
61
66
 
62
- The five views are **Archived**, **Recycle Bin**, **Storage & Retention**, **Origins & Branches**, and **About**. History and cleanup-preview interfaces are retired; archiving does not create historical versions.
67
+ The five views are **Archived**, **Recycle Bin**, **Storage & Retention**, **Origins & Branches**, and **About**. Archiving does not create historical versions; Recycle Bin protection snapshots support recovery, not a browsable version-history library.
63
68
 
64
69
  The Archived header has **Bulk archive** and **More**. More contains Import backup, Export all, Unarchive all, then Delete all after a separator. The Recycle Bin header directly shows **Restore all** and **Empty Recycle Bin** without a More menu. Recycle rows retain the preview icon and use compact **Restore** and **Delete** text buttons. Workspace and global restoration separately confirm scope, counts, and the Archived destination, skipping pending deletions. Empty Recycle Bin still requires irreversible-action confirmation. The header keeps consistent dimensions across tabs. Workspace actions and global export, unarchive, and deletion ask you to confirm the complete archive scope and chat count.
65
70
 
@@ -80,7 +85,7 @@ Empty Recycle Bin acts only on the exact recycle-record incarnations shown at co
80
85
 
81
86
  ## Data safety and limits
82
87
 
83
- - **Local only:** plugin state lives under `$DSH_HOME/plugin-data/archived-chats/`; the plugin does not upload or cloud-sync chats.
88
+ - **Local only:** plugin state lives under `$DSH_HOME/plugin-data/archived-chats/`; the plugin does not upload or cloud-sync chats. Update checks request only public npm version metadata, without chat or backup content.
84
89
  - **Recovery before purge:** workspace Recycle Bin moves require a healthy protection snapshot. Direct permanent deletion does not create a recoverable copy.
85
90
  - **No overwrite:** imports skip existing IDs. Recycle recovery prefers the original; snapshot fallback recreates an ordinary session ID only through an explicitly exclusive Host writer and otherwise retains the recovery record.
86
91
  - **Durable deletion:** confirmed deletion tasks clean related snapshots before the original. Partial failures retain non-restorable tasks for retries while running or after restart.
@@ -91,6 +96,10 @@ Successful exports are validated against the same format and budgets as import b
91
96
 
92
97
  ## Upgrading from older releases
93
98
 
99
+ **Names and versions:** Version 1.4.2 changes the export writer only; the menu names below are unchanged. Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
100
+
101
+ The standalone History and cleanup-preview interfaces have been removed; do not use older screenshots to locate them.
102
+
94
103
  **Startup recovery removes old snapshots that are not referenced by current recycle records, including plugin-owned attachment copies. They are not placed in the Recycle Bin. This cleanup is independent of the optional Recycle Bin retention setting.**
95
104
 
96
105
  Referenced protection snapshots are kept. Removing an unreferenced snapshot does not delete its source chat; already-confirmed deletion retries and enabled retention cleanup are separate operations.
@@ -108,33 +117,12 @@ The package declares DSH `>=0.1.0-rc.7`; individual features depend on public Ho
108
117
  | Session-scoped physical location | Required for session-directory accounting and permanent deletion; shared directories are never used as purge targets. |
109
118
  | Public writer | Required for ZIP import and snapshot fallback; restoring an intact original does not rewrite its log. Plain legacy create/append is not enough for safe ZIP restore unless the provider explicitly guarantees exclusive creation. |
110
119
  | Modern handles | Fork titles, preview/search, and Recycle Bin moves are supported. ZIP v2 preserves inherited boundaries; import uses create/append/flush/close and a safe session-scoped rollback location. Ordinary v1 ZIPs remain readable, but ambiguous seeded v1 sources are refused rather than flattened. |
120
+ | Host module resolution | DSH `0.1.6-alpha.2` added a resolution layer that fails on any dependency requesting a builtin name with a trailing slash. Before 1.4.2 this package reached `readable-stream@4` through `zip-stream`, so the plugin aborted Host startup as soon as it loaded. 1.4.2 uses `fflate` for export and no longer carries that chain; update before running on those Host versions. |
111
121
 
112
122
  Recycle Bin preview still depends on the original session; a missing original may prevent preview even when a protection snapshot can restore it. See [compatibility and limits](docs/USER_GUIDE.md#compatibility-and-limits).
113
123
 
114
124
  The declared range remains capability-based. Release automation passed Node.js 18 on Ubuntu and Node.js 24 on Ubuntu, macOS, and Windows; the Node.js 24 matrix required the official Host backend integration (5/5) and package checks. The installed 1.4.0 artifact also passed the official native round trip (5/5) against `@deepseek-ai/dsh-session@0.1.5-rc.2`.
115
125
 
116
- ## Historical screenshots
117
-
118
- The screenshot assets were captured for **v1.3.1**, using synthetic conversations in a Simplified Chinese light-theme environment. They are historical examples, **not current-version acceptance evidence**. In particular, `preview-04.png` and `preview-05.png` show retired snapshot workflows. Current behavior is defined by the guide above.
119
-
120
- <details>
121
- <summary>View the historical screenshot set</summary>
122
-
123
- | Scene | Historical image |
124
- | --- | --- |
125
- | Archive overview | [preview-01](assets/screenshots/preview-01.png) |
126
- | Full-text search | [preview-02](assets/screenshots/preview-02.png) |
127
- | Workspace chooser | [preview-03](assets/screenshots/preview-03.png) |
128
- | Retired snapshot preview | [preview-04](assets/screenshots/preview-04.png) |
129
- | Recycle Bin with retired legacy rows | [preview-05](assets/screenshots/preview-05.png) |
130
- | Empty Recycle Bin confirmation | [preview-06](assets/screenshots/preview-06.png) |
131
- | Automatic cleanup confirmation | [preview-07](assets/screenshots/preview-07.png) |
132
- | Origins and Branches | [preview-08](assets/screenshots/preview-08.png) |
133
-
134
- </details>
135
-
136
- The existing market screenshot manifest is unchanged. Recapture affected scenes against the release build before refreshing marketplace assets.
137
-
138
126
  ## Documentation
139
127
 
140
128
  | Resource | English | 简体中文 |
@@ -147,7 +135,7 @@ See also [Support](SUPPORT.md), [Security](SECURITY.md), [Contributing](CONTRIBU
147
135
 
148
136
  ## Project status
149
137
 
150
- Session Archive is actively maintained. The latest stable npm release receives fixes and security updates; older releases should be upgraded before reporting a problem. Reproducible bug reports and focused pull requests are welcome. Accepted community tasks are marked [`help wanted`](https://github.com/Ultronen/dsh-archived-chats/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22); read the [claim workflow](CONTRIBUTING.md#community-proposals-and-claims) before starting substantial work. Maintenance is performed as availability permits, so no fixed response or release schedule is promised.
138
+ Archive Management is actively maintained. The latest stable npm release receives fixes and security updates; older releases should be upgraded before reporting a problem. Reproducible bug reports and focused pull requests are welcome. Accepted community tasks are marked [`help wanted`](https://github.com/Ultronen/dsh-archived-chats/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22); read the [claim workflow](CONTRIBUTING.md#community-proposals-and-claims) before starting substantial work. Maintenance is performed as availability permits, so no fixed response or release schedule is promised.
151
139
 
152
140
  ## Development
153
141
 
package/README.zh-CN.md CHANGED
@@ -28,12 +28,16 @@
28
28
 
29
29
  归档管理为 DeepSeek Harness 中归档后从主页面会话区隐藏的聊天提供查看和管理入口。保留 DSH 原有单条归档方式,并扩展工作区批量归档、搜索、预览、备份及按工作区移入回收站的流程。
30
30
 
31
- > 中文界面统一使用「归档管理」(曾用名「已归档的聊天」「会话档案」),英文名称仍为 Session Archive。更名没有改变包名、安装命令和本地数据位置,但不代表后续升级会保留旧快照;请阅读下方升级提醒。
31
+ > 中文名称为「归档管理」,英文名称为 Archive Management;安装包名始终为 `dsh-archived-chats`。
32
32
  >
33
- > 本文对应 dsh-archived-chats 1.4.0。版本变更见[更新日志](CHANGELOG.md)。
33
+ > 本文跟随 `main` 分支维护。本文适用于 1.4.2,其导出实现不再携带会导致新版 Host 启动失败的依赖;已安装用户请先升级。版本变更见[更新日志](CHANGELOG.md),其中包含 1.4.1 的英文名称统一与交互提示优化。
34
34
 
35
35
  ## 快速开始
36
36
 
37
+ 在 DSH 插件市场搜索 `dsh-archived-chats`,核对作者为 **Ultronen** 后点击「安装」。安装完成后按宿主提示重启 DSH,打开 **设置 → 归档管理**。
38
+
39
+ 也可以在运行 DSH 的电脑上使用命令行安装:
40
+
37
41
  ```sh
38
42
  dsh plugin --profile web add dsh-archived-chats@latest
39
43
  ```
@@ -54,11 +58,11 @@ dsh plugin --profile web update dsh-archived-chats
54
58
  | 工作区批量归档 | 设置页的工作区选择器支持选择一个或多个工作区,汇总确认后归档;跳过空白、正在使用或无法确认内容的聊天。 |
55
59
  | 只读预览 | 无需取消归档即可查看对话、思考、工具活动、Markdown、JSON、代码及可读取的已存储图片。 |
56
60
  | 备份 | 支持按工作区或全部归档导出 JSON + Markdown ZIP;导入先预览,跳过冲突 ID。 |
57
- | 回收站 | 按工作区移入回收站,再按单条或工作区恢复到已归档。 |
61
+ | 回收站 | 按工作区移入回收站;支持单条、工作区和整个回收站恢复到已归档。 |
58
62
  | 永久删除 | 确认后删除单条归档、工作区归档或全部归档;回收站删除独立操作。 |
59
63
  | 空间与关系 | 空间分账、可选的回收站自动清理,以及只读「来源与分支」。 |
60
64
 
61
- 五个视图为 **已归档**、**回收站**、**空间与策略**、**来源与分支** 和 **关于**。「历史版本」和「预览清理」界面已退役;归档不再生成历史版本。
65
+ 五个视图为 **已归档**、**回收站**、**空间与策略**、**来源与分支** 和 **关于**。归档不会创建历史版本;回收站保护快照仅用于恢复,不是可浏览的历史版本库。
62
66
 
63
67
  已归档页顶部仅保留 **批量归档** 和 **更多**;「更多」依次提供导入备份、全部导出、全部取消归档,分隔线后为全部删除。回收站顶部直接并列显示 **全部恢复** 和 **清空回收站**,不再设更多菜单;聊天行保留预览图标,使用紧凑的 **恢复** 和 **删除** 文字按钮。工作区恢复和全局恢复分别确认范围、数量及已归档去向,跳过正在永久删除的条目;清空仍须确认不可恢复的后果。各 Tab 顶部保持一致尺寸。工作区操作及全局导出、取消归档、删除均先确认完整归档范围和聊天数量。
64
68
 
@@ -90,6 +94,10 @@ dsh plugin --profile web update dsh-archived-chats
90
94
 
91
95
  ## 从旧版升级
92
96
 
97
+ **名称与版本:** 1.4.2 仅更换导出实现,下列菜单名称不变。1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
98
+
99
+ 独立「历史版本」和「预览清理」界面已移除,不应再按旧版截图寻找这些入口。
100
+
93
101
  **启动恢复流程会清理未被当前回收记录引用的旧快照及其插件附件副本,不会将它们放进回收站。这项清理不受回收站自动清理开关控制。**
94
102
 
95
103
  当前回收记录引用的保护快照会保留。清理未引用快照不删除来源聊天本身;已确认删除任务的重试和已启用的到期清理是另外的操作。
@@ -107,33 +115,12 @@ dsh plugin --profile web update dsh-archived-chats
107
115
  | 会话独占物理位置 | 会话目录统计和永久删除必需;不把共享目录当作删除目标。 |
108
116
  | 公开 writer | ZIP 导入和快照回退恢复必需;恢复完好的原会话不重写日志。仅有旧 create/append 不足以安全恢复 ZIP,除非提供者明确保证独占创建。 |
109
117
  | 新版句柄接口 | 分叉支持标题、预览/搜索及移入回收站。ZIP v2 保存继承边界;导入通过 create/append/flush/close 写入,并要求安全的会话独占回滚位置。普通 v1 ZIP 仍可读取,但边界不明的 seeded v1 来源会被拒绝,不会展平。 |
118
+ | Host 模块解析 | DSH `0.1.6-alpha.2` 新增的解析层会在任何依赖请求带斜杠的内置模块名时失败。1.4.2 之前,本插件经 `zip-stream` 引入 `readable-stream@4`,因此插件一被加载就会中止 Host 启动。1.4.2 的导出改用 `fflate`,不再携带该依赖链;请在这些 Host 版本上先升级。 |
110
119
 
111
120
  回收站预览目前仍依赖原会话;原件丢失时,即使保护快照可恢复,也可能无法预览。详见[兼容性与限制](docs/USER_GUIDE.zh-CN.md#兼容性与限制)。
112
121
 
113
122
  声明的版本范围仍以 Host 公开能力为准。发布自动化已通过 Ubuntu 上的 Node.js 18,以及 Ubuntu、macOS 和 Windows 上的 Node.js 24;Node.js 24 矩阵强制运行官方 Host 后端集成(5/5)与打包检查。已安装的 1.4.0 产物也已在 `@deepseek-ai/dsh-session@0.1.5-rc.2` 上通过官方 native 闭环(5/5)。
114
123
 
115
- ## 历史演示截图
116
-
117
- 现有截图拍摄于 **v1.3.1**,使用简体中文浅色环境和合成会话,仅供历史参考,**不代表当前版本验收结果**。其中 `preview-04.png`、`preview-05.png` 展示了已退役的快照流程;当前行为以上述指南为准。
118
-
119
- <details>
120
- <summary>查看历史截图目录</summary>
121
-
122
- | 场景 | 历史图片 |
123
- | --- | --- |
124
- | 归档总览 | [preview-01](assets/screenshots/preview-01.png) |
125
- | 全文搜索 | [preview-02](assets/screenshots/preview-02.png) |
126
- | 工作区选择器 | [preview-03](assets/screenshots/preview-03.png) |
127
- | 已退役的快照预览 | [preview-04](assets/screenshots/preview-04.png) |
128
- | 含已退役旧快照条目的回收站 | [preview-05](assets/screenshots/preview-05.png) |
129
- | 清空回收站确认 | [preview-06](assets/screenshots/preview-06.png) |
130
- | 自动清理确认 | [preview-07](assets/screenshots/preview-07.png) |
131
- | 来源与分支 | [preview-08](assets/screenshots/preview-08.png) |
132
-
133
- </details>
134
-
135
- 现有市场截图清单未改动。更新市场素材前,应基于发布构建重新拍摄受影响场景。
136
-
137
124
  ## 文档
138
125
 
139
126
  | 资料 | English | 简体中文 |
@@ -146,7 +133,7 @@ dsh plugin --profile web update dsh-archived-chats
146
133
 
147
134
  ## 项目状态
148
135
 
149
- Session Archive 目前处于积极维护状态。最新 npm 稳定版会接收缺陷修复与安全更新;报告问题前请先从旧版本升级。欢迎可复现的缺陷报告和目标集中的 Pull Request。已开放认领的社区任务会标记为 [`help wanted`](https://github.com/Ultronen/dsh-archived-chats/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22);开始较大改动前,请先阅读[需求认领流程](CONTRIBUTING.md#社区需求与认领)。项目按维护者的可用时间推进,不承诺固定响应或发布时间。
136
+ 归档管理目前处于积极维护状态。最新 npm 稳定版会接收缺陷修复与安全更新;报告问题前请先从旧版本升级。欢迎可复现的缺陷报告和目标集中的 Pull Request。已开放认领的社区任务会标记为 [`help wanted`](https://github.com/Ultronen/dsh-archived-chats/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22);开始较大改动前,请先阅读[需求认领流程](CONTRIBUTING.md#社区需求与认领)。项目按维护者的可用时间推进,不承诺固定响应或发布时间。
150
137
 
151
138
  ## 开发
152
139
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  English · [中文](ARCHITECTURE.md) · [User guide](USER_GUIDE.md)
4
4
 
5
- This document describes dsh-archived-chats 1.4.0; see the [changelog](../CHANGELOG.md) for release history. Runtime code is the authority for interfaces and behavior; user-facing documentation should agree with it.
5
+ This document follows `main`, covering 1.4.2 behavior, which switches export ZIP generation to `fflate`; see the [changelog](../CHANGELOG.md) for release history. Runtime code is the authority for interfaces and behavior; user-facing documentation should agree with it.
6
6
 
7
7
  ## Product boundary and modules
8
8
 
@@ -86,7 +86,7 @@ Metadata and recycle writes are serialized and published through temporary files
86
86
 
87
87
  ## Workspace bulk archive
88
88
 
89
- The client registers `settings.section` and `shell.overlay`. Its workspace chooser lives in **Settings → Session Archive / 设置 → 归档管理**, without depending on a workspace-menu extension or shared client store.
89
+ The client registers `settings.section` and `shell.overlay`. Its workspace chooser lives in **Settings → Archive Management / 设置 → 归档管理**, without depending on a workspace-menu extension or shared client store.
90
90
 
91
91
  1. List workspaces containing at least one eligible chat.
92
92
  2. Prepare each selected workspace separately. A preview binds at most 2,000 ordered IDs to a five-minute, single-use token/nonce.
@@ -192,7 +192,7 @@ Named native buttons expand branches and native details/summary disclose timesta
192
192
 
193
193
  ## ZIP export, import, and restore
194
194
 
195
- Export contains a manifest and per-chat session.json/transcript.md with sanitized collision-safe paths. It inspects every selected source exactly once and stages the rendered entries before returning a sequential ZIP stream. The shared complete semantic validator and all importer budgets run first, so a successful export is importable by this plugin and a later invalid source cannot turn an already successful response into a partial backup. Modern reads produce v2 manifest/session records with `source.inheritedEventCount`; legacy reads retain v1. Import accepts ordinary records in both versions, requires matching manifest/record versions, and validates the v2 boundary. An ambiguous seeded v1 source is refused rather than flattened. Both formats exclude attachment bytes and automatic descendant recursion.
195
+ Export contains a manifest and per-chat session.json/transcript.md with sanitized collision-safe paths. It inspects every selected source exactly once and stages the rendered entries before returning a sequential ZIP stream. `fflate` compresses those entries synchronously in 256 KiB slices with an event-loop yield between slices, and the writer no longer depends on `zip-stream`, so it cannot pull in the `readable-stream@4` chain that requests a builtin name with a trailing slash. The shared complete semantic validator and all importer budgets run first, so a successful export is importable by this plugin and a later invalid source cannot turn an already successful response into a partial backup. Modern reads produce v2 manifest/session records with `source.inheritedEventCount`; legacy reads retain v1. Import accepts ordinary records in both versions, requires matching manifest/record versions, and validates the v2 boundary. An ambiguous seeded v1 source is refused rather than flattened. Both formats exclude attachment bytes and automatic descendant recursion.
196
196
 
197
197
  The shared limits are 2,000 sessions, 4,001 entries, 4 MiB manifest, 4 MiB per session JSON, 8 MiB per entry/Markdown, and 256 MiB expanded total. Imported compressed input is limited to 512 MiB. Each JSON document is limited to depth 64, 100,000 nodes, and 4 Mi Unicode code points across strings. Import uses bounded decompression and validates declared/actual sizes, paths, version, generator, JSON budgets, and cross-file identities. It reconciles local entries, data descriptors, and the central directory, including sizes and CRCs; truncation, invalid UTF-8, duplicates, encryption, ZIP64, multi-disk archives, and methods other than Store/Deflate are rejected. Existing IDs are disabled; missing workspaces or attachment references produce warnings. Confirmation lasts ten minutes and is single-use; at most eight previews totaling 128 MiB remain in process. Conflict rechecks and confirmed commits run in the lifecycle queue.
198
198
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](ARCHITECTURE.en.md) · 中文 · [用户指南](USER_GUIDE.zh-CN.md)
4
4
 
5
- 本文描述 dsh-archived-chats 1.4.0;版本变更见[更新日志](../CHANGELOG.md)。运行时代码是接口与行为的依据;用户操作说明与本文应保持一致。
5
+ 本文跟随 `main` 分支,功能对应 1.4.2,该版本改用 `fflate` 生成导出 ZIP;版本变更见[更新日志](../CHANGELOG.md)。运行时代码是接口与行为的依据;用户操作说明与本文应保持一致。
6
6
 
7
7
  ## 产品边界与模块
8
8
 
@@ -86,7 +86,7 @@ Host 的归档注册表决定归档归属;正常可见归档列表排除回收
86
86
 
87
87
  ## 工作区批量归档
88
88
 
89
- 客户端注册 `settings.section` 和 `shell.overlay`,在 **Settings → Session Archive / 设置 → 归档管理** 内维护工作区选择器,不依赖工作区菜单扩展或共享客户端 store。
89
+ 客户端注册 `settings.section` 和 `shell.overlay`,在 **Settings → Archive Management / 设置 → 归档管理** 内维护工作区选择器,不依赖工作区菜单扩展或共享客户端 store。
90
90
 
91
91
  1. 列出至少有一条符合条件聊天的工作区。
92
92
  2. 浏览器为每个选中工作区分别请求 preview;每个 preview 最多绑定 2,000 个有序 ID,凭据 token/nonce 有效 5 分钟且仅用一次。
@@ -192,7 +192,7 @@ apply 只接受凭据,不接受调用方增补会话 ID。每条执行前在
192
192
 
193
193
  ## ZIP 导出、导入与恢复
194
194
 
195
- 导出包含 manifest、每条 session.json 和 transcript.md,路径净化并处理重名。它仅检查每个选中来源一次,在返回顺序 ZIP 流之前暂存渲染后的条目。共用完整语义验证器和全部导入预算先行执行,因此成功导出可被本插件导入,之后的无效来源不会把已成功响应变成不完整备份。现代读取产生 v2 manifest/session 记录,保存 `source.inheritedEventCount`;旧读取保留 v1。导入兼容两版的普通记录,要求包与记录版本一致,校验 v2 继承边界。边界不明的 seeded v1 来源会被拒绝,不会展平。两版均不打包附件二进制或自动递归加入后代会话。
195
+ 导出包含 manifest、每条 session.json 和 transcript.md,路径净化并处理重名。它仅检查每个选中来源一次,在返回顺序 ZIP 流之前暂存渲染后的条目。ZIP 由 `fflate` 同步分片压缩生成,每条目按 256 KiB 分片推送并在分片之间让出事件循环;写入器不再依赖 `zip-stream`,因此不会引入会请求带斜杠内置模块名的 `readable-stream@4`。共用完整语义验证器和全部导入预算先行执行,因此成功导出可被本插件导入,之后的无效来源不会把已成功响应变成不完整备份。现代读取产生 v2 manifest/session 记录,保存 `source.inheritedEventCount`;旧读取保留 v1。导入兼容两版的普通记录,要求包与记录版本一致,校验 v2 继承边界。边界不明的 seeded v1 来源会被拒绝,不会展平。两版均不打包附件二进制或自动递归加入后代会话。
196
196
 
197
197
  共用限制为 2,000 条会话、4,001 个条目、4 MiB manifest、单会话 JSON 4 MiB、单条目/Markdown 8 MiB,以及解压总量 256 MiB。导入的压缩输入上限为 512 MiB。每个 JSON 文档限制深度 64、节点 100,000 个,所有字符串合计 4 Mi Unicode 码点。导入有界解压,验证声明/实际大小、路径、版本、生成器、JSON 预算及跨文件身份。它对齐本地条目、数据描述符和中央目录,包括大小与 CRC;拒绝截断、无效 UTF-8、重复项、加密、ZIP64、多磁盘及 Store/Deflate 之外的压缩方法。已有 ID 禁用;缺失工作区或附件引用给警告。确认凭据有效 10 分钟、单次使用,进程最多保留 8 份、总计 128 MiB。确认后的冲突检查和提交进入生命周期队列。
198
198
 
@@ -1,8 +1,8 @@
1
- # Session Archive user guide
1
+ # Archive Management user guide
2
2
 
3
3
  English · [简体中文](USER_GUIDE.zh-CN.md) · [Back to README](../README.md)
4
4
 
5
- Session Archive provides a place to browse and manage DSH's archived chats, plus workspace bulk archiving. This guide describes dsh-archived-chats 1.4.0; see the [changelog](../CHANGELOG.md) for release history. For interfaces and data formats, see the [architecture](ARCHITECTURE.en.md).
5
+ Archive Management provides a place to browse and manage DSH's archived chats, plus workspace bulk archiving. This document follows the `main` branch. This guide targets 1.4.2, whose export writer no longer carries a dependency that prevents startup on newer Hosts. See the [changelog](../CHANGELOG.md) for release history, including 1.4.1's English rename and feedback improvements. For interfaces and data formats, see the [architecture](ARCHITECTURE.en.md).
6
6
 
7
7
  ## Understand the three locations
8
8
 
@@ -16,11 +16,15 @@ Archiving is not deletion and creates no historical version. Moving to the Recyc
16
16
 
17
17
  ## Install and open
18
18
 
19
+ Search for `dsh-archived-chats` in DSH's plugin market, verify the author is **Ultronen**, and choose **Install**. After installation, follow the host's restart instructions. The settings entry is **归档管理** in Chinese; in English it is **Archive Management**.
20
+
21
+ Alternatively, run this command on the computer running DSH:
22
+
19
23
  ```sh
20
24
  dsh plugin --profile web add dsh-archived-chats@latest
21
25
  ```
22
26
 
23
- Restart DSH and open **Settings → Session Archive**. The views are **Archived**, **Recycle Bin**, **Storage & Retention**, **Origins & Branches**, and **About**.
27
+ Restart DSH and open **Settings → Archive Management** (Chinese: **设置 → 归档管理**). The views are **Archived**, **Recycle Bin**, **Storage & Retention**, **Origins & Branches**, and **About**.
24
28
 
25
29
  Before updating an older installation, read “Upgrades, old data, and downgrades” below, especially the snapshot-cleanup warning.
26
30
 
@@ -39,7 +43,7 @@ Only workspaces with eligible chats are listed. Blank sessions, chats in use, an
39
43
 
40
44
  Search Archived by title, workspace, tags, notes, messages, and tool results. Filter by type, workspace, and tag; sort by time or title. Content matches show excerpts. Long workspace titles wrap to remain fully visible. Click a workspace folder or title to expand or collapse its chats; the open or closed folder reflects the current state. Group collapse state is saved in the browser.
41
45
 
42
- Forked chats use the last valid title in their own event stream, including renames after branching. The Host creates a new chat ID with history inherited through the fork point; parent and child develop independently afterwards. Modern Hosts support preview and Recycle Bin moves, with ZIP export restrictions described under “Compatibility and limits.”
46
+ Forked chats use the last valid title in their own event stream, including renames after branching. The Host creates a new chat ID with history inherited through the fork point; parent and child develop independently afterwards. Modern Hosts support preview, Recycle Bin moves, and ZIP v2 export/import that preserves inherited history, even when the parent is absent. ZIPs do not include attachment bytes; see “Compatibility and limits” for older backups and Recycle Bin snapshot fallback.
43
47
 
44
48
  ## Preview, tags, and notes
45
49
 
@@ -129,6 +133,8 @@ Import requires a complete, internally consistent ZIP and rejects truncation, CR
129
133
 
130
134
  The browser buffers and validates the complete ZIP before initiating its download. The response-byte cap is 320 MiB and the complete fetch/body-read timeout is five minutes; this is not a peak-heap guarantee, and the current non-stream fallback has no stronger universal WebView memory promise. Download started does not mean the browser has saved the file to disk. A timeout or cap failure downloads no incomplete file and can be retried.
131
135
 
136
+ Export compresses the staged entries in 256 KiB slices and yields to the event loop between them, so a large backup does not freeze the rest of the Host while it is written. When a download is cancelled, the archive stops and the response fails; a cancelled export is never reported as a completed one.
137
+
132
138
  ZIP import and Recycle Bin recovery are separate workflows. A ZIP is not a Recycle Bin protection snapshot.
133
139
 
134
140
  ## Storage and automatic cleanup
@@ -143,6 +149,8 @@ This is separate from the old-snapshot cleanup below: **disabling automatic Recy
143
149
 
144
150
  ## Origins and Branches
145
151
 
152
+ Version 1.4.1 puts the scope explanation behind the help icon beside Managed chats. Hover, keyboard focus, or a touch tap reveals it. Auto-empty Recycle Bin on the storage page uses the same help control; enabling cleanup still shows the full permanent-deletion confirmation.
153
+
146
154
  This read-only view shows sources, forks, and subagent trees for archived and recycled chats, plus the active-session context needed to explain their relationships. Unrelated active chats are not listed.
147
155
 
148
156
  Chats are grouped by their actual workspace, with counts in workspace headings and browser-persisted workspace folding. Compact rows use title-side buttons to expand branches; initially only starting chats are shown, not every descendant. Details disclose full titles, timestamps, sources, and session IDs with Copy ID. Ungrouped chats have their own group.
@@ -153,6 +161,10 @@ Search temporarily opens matching workspaces and branches; clearing it restores
153
161
 
154
162
  ## About and updates
155
163
 
164
+ From 1.4.1, a red dot appears on the About tab only when a newer version is confirmed. Unchecked, failed, and up-to-date checks show no dot. Visiting About does not dismiss an available update.
165
+
166
+ Success notices within Archive Management, including View Archived after restoration, dismiss after four seconds and pause while hovered or keyboard-focused. Switching tabs clears the current notice. Errors do not auto-dismiss and can be closed manually. The native archive-success notice on the main chat page still closes after about three seconds. These interaction improvements are available from 1.4.1.
167
+
156
168
  About shows the running plugin version, author, license, and direct links to the project, language-appropriate guide, changelog, issue feedback, and plugin market. The title also displays the running version.
157
169
 
158
170
  Current version is read dynamically from the running backend, never substituted with the registry's latest version. A failed online check keeps the loaded version, identity, and links. Local metadata failure shows Unavailable and Reload information rather than an update-check error. If newly installed endpoints have not loaded yet, reload the DSH backend and retry.
@@ -163,6 +175,8 @@ When a newer version is found, Get update beside the title opens the plugin mark
163
175
 
164
176
  ## Upgrades, old data, and downgrades
165
177
 
178
+ **Names and versions:** Version 1.4.2 changes the export writer only; the menu names below are unchanged. Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
179
+
166
180
  The standalone History and cleanup-preview interfaces are retired. Archiving no longer creates versions. Recycle Bin protection snapshots support recovery; they are not a browsable version-history library.
167
181
 
168
182
  **After upgrading from an older release and starting the new version, startup recovery automatically removes old snapshots not referenced by current recycle records, including their plugin-owned attachment copies. They do not become Recycle Bin entries and cannot be recovered through the plugin afterward.** Referenced protection snapshots are retained. This cleanup does not delete source chats; retrying confirmed deletion tasks and enabled expiry cleanup are separate mechanisms.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](USER_GUIDE.md) · 简体中文 · [返回 README](../README.zh-CN.md)
4
4
 
5
- 归档管理为 DSH 已归档聊天提供查看和管理页面,并扩展按工作区批量归档。本文对应 dsh-archived-chats 1.4.0;版本变更见[更新日志](../CHANGELOG.md)。接口和数据格式见[架构说明](ARCHITECTURE.md)。
5
+ 归档管理为 DSH 已归档聊天提供查看和管理页面,并扩展按工作区批量归档。本文跟随 `main` 分支维护。本文适用于 1.4.2,其导出实现不再携带会导致新版 Host 启动失败的依赖;已安装用户请先升级。版本变更见[更新日志](../CHANGELOG.md),其中包含 1.4.1 的英文名称统一与交互提示优化。接口和数据格式见[架构说明](ARCHITECTURE.md)。
6
6
 
7
7
  ## 先理解三个位置
8
8
 
@@ -16,6 +16,10 @@
16
16
 
17
17
  ## 安装与打开
18
18
 
19
+ 在 DSH 插件市场搜索 `dsh-archived-chats`,核对作者为 **Ultronen** 后点击「安装」。安装完成后按宿主提示重启 DSH,打开 **设置 → 归档管理**。
20
+
21
+ 也可以在运行 DSH 的电脑上使用命令行安装:
22
+
19
23
  ```sh
20
24
  dsh plugin --profile web add dsh-archived-chats@latest
21
25
  ```
@@ -39,7 +43,7 @@ dsh plugin --profile web add dsh-archived-chats@latest
39
43
 
40
44
  在「已归档」中可以搜索标题、工作区、标签、备注、消息和工具结果,按类型、工作区、标签筛选,并按时间或标题排序。正文命中会显示摘要;工作区长标题自动换行、完整显示。点击工作区文件夹或标题可展开/收起聊天,文件夹开合样式表示当前状态,分组折叠状态保存在浏览器中。
41
45
 
42
- 分叉会话读取自身事件流中最后一个有效标题,包括分叉后的重命名。官方分叉创建独立 ID 的新会话,继承分叉点之前的历史,之后独立发展,不会跟随父会话继续同步。新版 Host 上可预览和移入回收站;ZIP 导出仍有限制,详见「兼容性与限制」。
46
+ 分叉会话读取自身事件流中最后一个有效标题,包括分叉后的重命名。官方分叉创建独立 ID 的新会话,继承分叉点之前的历史,之后独立发展,不会跟随父会话继续同步。新版 Host 上支持预览、移入回收站,以及保留继承历史的 ZIP v2 导出和导入。父会话不存在时也可从 v2 备份恢复;附件文件本体不包含在 ZIP 中,旧版备份与回收站快照回退的限制见「兼容性与限制」。
43
47
 
44
48
  ## 预览、标签与备注
45
49
 
@@ -129,6 +133,8 @@ JSON 是机器可读记录,Markdown 是可读副本。新版 Host 导出 ZIP v
129
133
 
130
134
  浏览器会在发起下载前缓冲并验证完整 ZIP。响应字节上限为 320 MiB,完整获取/读取响应体的超时为五分钟;这不是峰值堆内存保证,当前非流式兼容路径也没有更强的通用 WebView 内存承诺。“已开始下载”不表示浏览器已将文件保存到磁盘。超时或超限时不会下载不完整文件,可重试。
131
135
 
136
+ 导出按 256 KiB 分片压缩已暂存的条目,并在分片之间让出事件循环,因此生成大型备份时不会长时间占用 Host 主线程。下载被取消时归档立即停止且响应失败,已取消的导出不会被报告为完成。
137
+
132
138
  ZIP 导入和回收站恢复是两套不同流程,不要把 ZIP 当作回收站保护快照使用。
133
139
 
134
140
  ## 空间与自动清理
@@ -143,6 +149,8 @@ DSH 运行时约每分钟检查,关闭期间暂停,启动恢复完成后补
143
149
 
144
150
  ## 来源与分支
145
151
 
152
+ 1.4.1 将管理范围说明收起到「已管理会话」旁的提示图标;将鼠标放上去、用键盘聚焦或触屏点击即可查看。存储页「回收站自动清理」旁也使用相同的说明入口,开启清理时的永久删除确认仍会完整显示。
153
+
146
154
  只读展示归档和回收聊天的来源、分叉、子代理树,以及解释这些关系所需的活动会话上下文,不列出无关活动聊天。
147
155
 
148
156
  会话按实际所属工作区分组,工作区标题显示数量,折叠状态保存在浏览器。每条使用紧凑关系行,标题左侧箭头展开分支;默认先展示起始会话,不自动铺开全部后代。「详情」按需显示完整标题、时间、来源和会话 ID,并保留复制 ID。未分组聊天有独立分组。
@@ -153,6 +161,10 @@ DSH 运行时约每分钟检查,关闭期间暂停,启动恢复完成后补
153
161
 
154
162
  ## 关于与更新
155
163
 
164
+ 从 1.4.1 起,仅确认有新版本时,「关于」标签右上角显示红点;尚未检查、检查失败或已是最新版时不显示。打开「关于」不会清除仍可用的新版本提示。
165
+
166
+ 归档管理页面内的操作成功提示(包括恢复后的「查看已归档」)在 4 秒后自动收起,鼠标悬停或键盘焦点停留时暂停计时。切换标签会清除当前提示;错误提示不会自动消失,可以手动关闭。主聊天页面的原生归档成功提示仍约 3 秒后关闭。这些交互改进自 1.4.1 起提供。
167
+
156
168
  「关于」展示正在运行的插件版本、作者、许可证,并直达项目主页、对应语言的使用指南、更新日志、问题反馈和插件市场。页面标题旁也显示当前运行版本。
157
169
 
158
170
  「当前版本」动态读取正在运行的后端,不使用网上最新版代替。联网检查失败不会清除已读取的版本、作者或链接;本地信息读取失败则显示「暂不可用」和「重新读取」,不误报为检查更新失败。若刚更新插件但新接口尚未加载,请重新加载 DSH 后端后重试。
@@ -163,6 +175,8 @@ DSH 运行时约每分钟检查,关闭期间暂停,启动恢复完成后补
163
175
 
164
176
  ## 升级、旧数据与降级
165
177
 
178
+ **名称与版本:** 1.4.2 仅更换导出实现,下列菜单名称不变。1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
179
+
166
180
  独立「历史版本」和「预览清理」界面已退役。归档不再创建历史版本;回收站保护快照只用于恢复,不是用户可以浏览的历史版本库。
167
181
 
168
182
  **从旧版本升级并启动新版后,启动恢复流程会自动删除没有被当前回收记录引用的旧快照,包括相应的插件附件副本;这些快照不会转成回收站条目,也无法通过插件恢复。** 当前回收记录引用的保护快照会保留。此清理不删除来源聊天本身;已确认的待删除任务重试及已启用的到期清理是另外的机制。
package/lib/client.js CHANGED
@@ -197,6 +197,8 @@ window.__ModuleLoader__.load({
197
197
  "tab.insights": "空间与策略",
198
198
  "tab.lineage": "来源与分支",
199
199
  "tab.about": "关于",
200
+ "about.updateAvailable": "有新版本可用",
201
+ "help.label": "{label}说明",
200
202
  "trash.viewArchived": "查看已归档",
201
203
  "trash.listUnverified": "回收站目录无法读取:下面的列表可能包含已删除的聊天,归档修改已暂停。",
202
204
  "insights.loading": "正在分析空间…",
@@ -468,7 +470,7 @@ window.__ModuleLoader__.load({
468
470
  };
469
471
  const en = {
470
472
  "locale.intl": "en-US",
471
- "nav": "Session Archive",
473
+ "nav": "Archive Management",
472
474
  "archiveNotice.title": "Chat archived",
473
475
  "archiveNotice.view": "View",
474
476
  "archiveNotice.opening": "Opening…",
@@ -478,13 +480,15 @@ window.__ModuleLoader__.load({
478
480
  "archiveNotice.close": "Close archive notice",
479
481
  "archiveNotice.settings": "Settings",
480
482
  "archiveNotice.undoError": "Undo failed. Try again",
481
- "archiveNotice.viewError": "Could not open Session Archive. Try again",
482
- "page.title": "Session Archive",
483
+ "archiveNotice.viewError": "Could not open Archive Management. Try again",
484
+ "page.title": "Archive Management",
483
485
  "tab.archived": "Archived",
484
486
  "tab.trash": "Recycle Bin",
485
487
  "tab.insights": "Storage & Retention",
486
488
  "tab.lineage": "Origins & Branches",
487
489
  "tab.about": "About",
490
+ "about.updateAvailable": "Update available",
491
+ "help.label": "Help: {label}",
488
492
  "trash.viewArchived": "View Archived",
489
493
  "trash.listUnverified": "The Recycle Bin catalog is unreadable: this list may include deleted chats, and archive changes are paused.",
490
494
  "insights.loading": "Analyzing storage…",
@@ -729,8 +733,8 @@ window.__ModuleLoader__.load({
729
733
  "workspaceArchive.loading": "Preparing archive…",
730
734
  "workspaceArchive.skipped": "Skipped chats",
731
735
  "workspaceArchive.confirmTitle": "Archive {count} chats?",
732
- "workspaceArchive.confirmBody": "This will archive chats in \"{workspace}\". You can find them later under Archived in Session Archive.",
733
- "workspaceArchive.confirmBodyMany": "This will archive chats in {count} workspaces. You can find them later under Archived in Session Archive.",
736
+ "workspaceArchive.confirmBody": "This will archive chats in \"{workspace}\". You can find them later under Archived in Archive Management.",
737
+ "workspaceArchive.confirmBodyMany": "This will archive chats in {count} workspaces. You can find them later under Archived in Archive Management.",
734
738
  "workspaceArchive.skippedLive": "Skipping {count} running chats or chats with unconfirmed status.",
735
739
  "workspaceArchive.apply": "Archive all",
736
740
  "workspaceArchive.applying": "Archiving…",
@@ -1191,6 +1195,13 @@ window.__ModuleLoader__.load({
1191
1195
  .dac-tabs{display:flex;align-items:center;gap:4px;border-bottom:1px solid var(--dsw-alias-border-l2);overflow-x:auto}
1192
1196
  .dac-tab{flex:none;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:20px;padding:8px 12px;cursor:pointer}
1193
1197
  .dac-tab[aria-selected="true"]{border-bottom-color:var(--dsw-alias-label-primary,#171717);color:var(--dsw-alias-label-primary,#171717);font-weight:500}
1198
+ .dac-tab-label{position:relative;display:inline-block}
1199
+ .dac-update-dot{position:absolute;right:-7px;top:0;width:6px;height:6px;border-radius:50%;background:#dc2626;pointer-events:none}
1200
+ .dac-help-label{display:inline-flex;align-items:center;gap:5px}
1201
+ .dac-help{display:inline-flex;align-items:center;font-weight:400}
1202
+ .dac-help-button{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;padding:4px;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
1203
+ .dac-help-button:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1204
+ .dac-help-button:focus-visible,.dac-toast button:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:2px}
1194
1205
  .dac-mode-panel{display:flex;flex-direction:column;gap:14px}
1195
1206
  .dac-action-wrap{position:relative;display:inline-flex}
1196
1207
  .dac-action-trigger{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:5px 11px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;white-space:nowrap;cursor:pointer}
@@ -1280,8 +1291,9 @@ window.__ModuleLoader__.load({
1280
1291
  .dac-notice button{border:none;background:transparent;color:inherit;cursor:pointer;font:inherit;padding:0;display:inline-flex}
1281
1292
  .dac-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:1200;display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-state-success-secondary);border-radius:999px;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);font-size:13px;font-weight:500;line-height:20px;padding:7px 12px 7px 14px;box-shadow:var(--dsw-shadow-lv3);animation:dac-toast-in .28s cubic-bezier(.21,1.02,.55,1) both}
1282
1293
  .dac-toast svg{flex:none}
1283
- .dac-toast button{border:none;background:transparent;color:inherit;opacity:.55;cursor:pointer;font:inherit;padding:2px;margin-left:2px;display:inline-flex;border-radius:999px}
1284
- .dac-toast button:hover{opacity:1;background:var(--dsw-alias-state-success-secondary)}
1294
+ .dac-toast button{border:none;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);opacity:1;cursor:pointer;font:inherit;padding:3px;margin-left:2px;display:inline-flex;align-items:center;justify-content:center;border-radius:999px}
1295
+ .dac-toast button.dac-notice-action{padding:3px 9px;white-space:nowrap}
1296
+ .dac-toast button:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
1285
1297
  @keyframes dac-toast-in{from{opacity:0;transform:translateX(-50%) translateY(-10px) scale(.96)}to{opacity:1;transform:translateX(-50%) translateY(0) scale(1)}}
1286
1298
  @media (prefers-reduced-motion:reduce){.dac-toast{animation:none}}
1287
1299
  .dac-archive-notice{position:absolute;top:20px;left:50%;z-index:30;display:flex;align-items:center;gap:8px;width:max-content;max-width:calc(100% - 24px);min-height:42px;box-sizing:border-box;transform:translateX(-50%);border:1px solid var(--dsw-alias-border-l2);border-radius:11px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:5px 8px 5px 12px;box-shadow:var(--dsw-shadow-lv3);animation:dac-archive-notice-in .2s cubic-bezier(.21,1.02,.55,1) both}
@@ -1391,8 +1403,6 @@ window.__ModuleLoader__.load({
1391
1403
  .dac-import-row.dac-import-conflict{opacity:.58;cursor:default}
1392
1404
  .dac-import-main{display:flex;flex-direction:column;gap:2px;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}
1393
1405
  .dac-context-note{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;padding:9px 12px}
1394
- .dac-policy-bottom .dac-policy-note{flex:1;min-width:0}.dac-policy-bottom .dac-policy-actions{flex-shrink:0}.dac-policy-bottom{flex-wrap:nowrap}
1395
- .dac-policy-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1396
1406
  .dac-insights-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.dac-insights-card{display:flex;align-items:center;flex-direction:column;gap:5px;text-align:center;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px}.dac-insights-card span{color:var(--dsw-alias-label-tertiary);font-size:12px}.dac-insights-card strong{color:var(--dsw-alias-label-primary);font-size:18px}.dac-policy{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:14px}.dac-policy-row{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.dac-policy-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.dac-policy-label{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dac-policy .dac-select{min-width:100px;border-radius:8px}.dac-policy input{width:88px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:7px 9px}.dac-policy-actions{display:flex;justify-content:flex-end;gap:8px;margin-left:auto}.dac-policy-actions .dac-btn{white-space:nowrap}.dac-retention-dialog{width:min(620px,calc(100vw - 48px));max-height:min(720px,calc(100vh - 48px))}.dac-retention-list{display:flex;flex-direction:column;gap:8px;overflow:auto}.dac-retention-row{display:flex;gap:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:9px}.dac-retention-row span{display:flex;flex-direction:column}.dac-retention-row small{color:var(--dsw-alias-label-tertiary)}.dac-retention-row.dac-danger strong{color:var(--dsw-alias-state-error-primary)}
1397
1407
  .dac-lineage-toolbar{display:block;min-width:0}
1398
1408
  .dac-lineage-filter-row{display:flex;flex-direction:column;gap:8px;min-width:0}
@@ -1454,7 +1464,6 @@ window.__ModuleLoader__.load({
1454
1464
  .dac-lineage-trash{background:var(--dsw-alias-interactive-bg-hover-danger,var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12)));color:var(--dsw-alias-state-error-primary)}
1455
1465
  .dac-lineage-diagnostics{grid-column:1/-1;display:flex;flex-direction:column;gap:3px;margin-left:34px}
1456
1466
  .dac-lineage-diagnostic{display:block;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px;white-space:normal;overflow-wrap:anywhere;word-break:break-word}
1457
- .dac-lineage-scopebar{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;padding:9px 11px}
1458
1467
  .dac-import-main span{color:var(--dsw-alias-label-tertiary);font-size:11px}
1459
1468
  .dac-import-main small{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}
1460
1469
  .dac-snapshot-row .dac-row-main{gap:3px}.dac-snapshot-id{white-space:normal;overflow-wrap:anywhere;word-break:break-word}
@@ -1527,7 +1536,7 @@ window.__ModuleLoader__.load({
1527
1536
  // button's icon in place — mutating the existing <svg> (never replacing
1528
1537
  // React-managed nodes) and re-marking through dataset, so locale
1529
1538
  // re-renders and dialog reopens simply get re-patched by the observer.
1530
- const NAV_LABELS = ["归档管理", "Session Archive"];
1539
+ const NAV_LABELS = ["归档管理", "Archive Management"];
1531
1540
  const ARCHIVE_ICON_INNER = '<rect x="3" y="4" width="18" height="5" rx="1"></rect><path d="M5 9v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9"></path><path d="M10 13h4"></path>';
1532
1541
 
1533
1542
  function safeQueryAll(root, selector) {
@@ -3238,6 +3247,101 @@ window.__ModuleLoader__.load({
3238
3247
  });
3239
3248
  }
3240
3249
 
3250
+ // Both pointer and keyboard interaction pause the remaining display time.
3251
+ function useSuccessNoticeTimer(value, enabled, onDismiss) {
3252
+ const [interaction, setInteraction] = _react.useState(null);
3253
+ const remaining = _react.useRef({ value: null, ms: 4000 });
3254
+ const paused = interaction?.value === value && (interaction.hover || interaction.focus);
3255
+ const latest = _react.useRef(null);
3256
+ latest.current = { value, enabled, paused };
3257
+ _react.useEffect(() => {
3258
+ if (remaining.current.value !== value) remaining.current = { value, ms: 4000 };
3259
+ if (!enabled || paused) return;
3260
+ const started = Date.now();
3261
+ let active = true;
3262
+ const timer = setTimeout(() => {
3263
+ if (active && latest.current.value === value && latest.current.enabled && !latest.current.paused) {
3264
+ active = false;
3265
+ onDismiss();
3266
+ }
3267
+ }, remaining.current.ms);
3268
+ return () => {
3269
+ active = false;
3270
+ clearTimeout(timer);
3271
+ remaining.current.ms = Math.max(0, remaining.current.ms - Math.max(0, Date.now() - started));
3272
+ };
3273
+ }, [value, enabled, paused]);
3274
+ const set = (key, next) => setInteraction((current) => ({
3275
+ ...(current?.value === value ? current : { value, hover: false, focus: false }), [key]: next,
3276
+ }));
3277
+ return {
3278
+ onMouseEnter: () => set("hover", true), onMouseLeave: () => set("hover", false),
3279
+ onFocus: () => set("focus", true),
3280
+ onBlur: (event) => { if (!event.currentTarget.contains(event.relatedTarget)) set("focus", false); },
3281
+ };
3282
+ }
3283
+
3284
+ function HelpTooltip({ id, label, children }) {
3285
+ const [position, setPosition] = _react.useState(null);
3286
+ const wrapRef = _react.useRef(null);
3287
+ const hovered = _react.useRef(false);
3288
+ const focused = _react.useRef(false);
3289
+ const pinned = _react.useRef(false);
3290
+ const close = () => { pinned.current = false; setPosition(null); };
3291
+ const show = () => {
3292
+ const rect = wrapRef.current?.getBoundingClientRect();
3293
+ if (!rect) return;
3294
+ const viewportWidth = window.innerWidth || 1024;
3295
+ const viewportHeight = window.innerHeight || 768;
3296
+ const width = Math.min(360, viewportWidth - 24);
3297
+ const below = viewportHeight - rect.bottom - 12;
3298
+ const above = rect.top - 12;
3299
+ const placeBelow = below >= Math.min(200, above);
3300
+ setPosition({ width, boxSizing: "border-box", left: Math.max(12, Math.min(rect.left, viewportWidth - width - 12)),
3301
+ maxHeight: Math.max(40, Math.min(240, placeBelow ? below : above)),
3302
+ ...(placeBelow ? { top: rect.bottom } : { bottom: viewportHeight - rect.top }) });
3303
+ };
3304
+ _react.useEffect(() => {
3305
+ if (position === null) return;
3306
+ const outside = (event) => { if (!wrapRef.current?.contains(event.target)) close(); };
3307
+ const escape = (event) => {
3308
+ if (event.key !== "Escape") return;
3309
+ event.preventDefault();
3310
+ event.stopPropagation();
3311
+ event.stopImmediatePropagation?.();
3312
+ close();
3313
+ };
3314
+ document.addEventListener("keydown", escape, true);
3315
+ document.addEventListener("pointerdown", outside);
3316
+ document.addEventListener("focusin", outside);
3317
+ window.addEventListener("scroll", outside, true);
3318
+ window.addEventListener("resize", close);
3319
+ return () => {
3320
+ document.removeEventListener("keydown", escape, true);
3321
+ document.removeEventListener("pointerdown", outside);
3322
+ document.removeEventListener("focusin", outside);
3323
+ window.removeEventListener("scroll", outside, true);
3324
+ window.removeEventListener("resize", close);
3325
+ };
3326
+ }, [position]);
3327
+ return (0, jsx.jsxs)("span", { className: "dac-help", ref: wrapRef,
3328
+ onMouseEnter: () => { hovered.current = true; show(); },
3329
+ onMouseLeave: () => { hovered.current = false; if (!focused.current && !pinned.current) close(); },
3330
+ onFocus: () => { focused.current = true; show(); },
3331
+ onBlur: (event) => { if (!event.currentTarget.contains(event.relatedTarget)) { focused.current = false; if (!hovered.current) close(); } },
3332
+ onKeyDown: (event) => { if (event.key === "Escape" && position !== null) { event.preventDefault(); event.stopPropagation(); close(); } },
3333
+ children: [
3334
+ (0, jsx.jsx)("button", { type: "button", className: "dac-help-button", "aria-label": label,
3335
+ "aria-describedby": position !== null ? id : void 0,
3336
+ onClick: () => { if (pinned.current) close(); else { pinned.current = true; show(); } },
3337
+ children: (0, jsx.jsxs)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, "aria-hidden": true, children: [
3338
+ (0, jsx.jsx)("circle", { cx: 12, cy: 12, r: 9 }),
3339
+ (0, jsx.jsx)("path", { d: "M12 11v6M12 7v1", strokeLinecap: "round" }),
3340
+ ] }) }),
3341
+ position !== null && (0, jsx.jsx)("span", { id, role: "tooltip", className: "dac-note-tooltip", style: position, children }),
3342
+ ] });
3343
+ }
3344
+
3241
3345
  function NotePreview({ note, sessionId, t }) {
3242
3346
  const [position, setPosition] = _react.useState(null);
3243
3347
  const wrapRef = _react.useRef(null);
@@ -3590,11 +3694,7 @@ window.__ModuleLoader__.load({
3590
3694
  } catch (error) { if (!signal?.aborted) setState({ status: "error", value: null, error }); }
3591
3695
  }, []);
3592
3696
  _react.useEffect(() => { const controller = new AbortController(); void load(controller.signal); return () => controller.abort(); }, [load]);
3593
- _react.useEffect(() => {
3594
- if (message === null || messageKind !== "ok") return undefined;
3595
- const timer = setTimeout(() => setMessage(null), 3000);
3596
- return () => clearTimeout(timer);
3597
- }, [message, messageKind]);
3697
+ const messageInteraction = useSuccessNoticeTimer(message, message !== null && messageKind === "ok", () => setMessage(null));
3598
3698
  const setNumber = (key, value, nullable) => setDraft((current) => ({ ...current, [key]: nullable && value === "" ? null : Number(value) }));
3599
3699
  const retentionDays = draft?.recycleMaxAgeDays;
3600
3700
  const customDays = customRetention || (retentionDays != null && ![7, 30, 90].includes(retentionDays));
@@ -3653,11 +3753,15 @@ window.__ModuleLoader__.load({
3653
3753
  (0, jsx.jsx)("strong", { children: display }),
3654
3754
  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") })
3655
3755
  ] }, label)) }),
3656
- message && (0, jsx.jsx)("div", { className: messageKind === "ok" ? "dac-toast" : "dac-notice", role: "status", children: message }),
3756
+ message && (0, jsx.jsxs)("div", { ...messageInteraction, className: messageKind === "ok" ? "dac-toast" : "dac-notice", role: messageKind === "ok" ? "status" : "alert", children: [message,
3757
+ (0, jsx.jsx)("button", { type: "button", "aria-label": t("notice.dismiss"), onClick: () => setMessage(null), children: (0, jsx.jsx)(IconClose, {}) }),
3758
+ ] }),
3657
3759
  draft && (0, jsx.jsxs)("div", { className: "dac-policy", children: [
3658
3760
  (0, jsx.jsxs)("div", { className: "dac-policy-row", children: [
3659
3761
  (0, jsx.jsxs)("div", { className: "dac-policy-controls", children: [
3660
- (0, jsx.jsx)("span", { className: "dac-policy-label", children: t("retention.recycleAge") }),
3762
+ (0, jsx.jsxs)("span", { className: "dac-policy-label dac-help-label", children: [t("retention.recycleAge"),
3763
+ (0, jsx.jsx)(HelpTooltip, { id: "dac-retention-help", label: t("help.label").replace("{label}", t("retention.recycleAge")), children: t("retention.note") }),
3764
+ ] }),
3661
3765
  (0, jsx.jsx)(Select, { disabled: busy, ariaLabel: t("retention.recycleAge"), value: customDays ? "custom" : String(retentionDays ?? ""), onChange: (choice) => {
3662
3766
  setCustomRetention(choice === "custom");
3663
3767
  setDraft((current) => ({ ...current, recycleAutoDelete: choice !== "", recycleMaxAgeDays: choice === "custom" ? current.recycleMaxAgeDays : choice === "" ? null : Number(choice) }));
@@ -3666,9 +3770,6 @@ window.__ModuleLoader__.load({
3666
3770
  ] }),
3667
3771
  (0, jsx.jsx)("div", { className: "dac-policy-actions", children: (0, jsx.jsx)("button", { className: "dac-btn", disabled: busy || !validDays, onClick: save, children: t("retention.save") }) })
3668
3772
  ] }),
3669
- (0, jsx.jsxs)("div", { className: "dac-policy-row dac-policy-bottom", children: [
3670
- (0, jsx.jsx)("div", { className: "dac-policy-note", children: t("retention.note") }),
3671
- ] })
3672
3773
  ] }),
3673
3774
  ] }),
3674
3775
  policyPreview && (0, jsx.jsx)(ConfirmDialog, {
@@ -3870,7 +3971,9 @@ window.__ModuleLoader__.load({
3870
3971
  ] }),
3871
3972
  (0, jsx.jsxs)("div", { className: "dac-lineage-list-head", children: [
3872
3973
  (0, jsx.jsxs)("div", { className: "dac-lineage-list-head-copy", children: [
3873
- (0, jsx.jsx)("span", { children: t("lineage.managedHeading") }),
3974
+ (0, jsx.jsxs)("span", { className: "dac-help-label", children: [t("lineage.managedHeading"),
3975
+ (0, jsx.jsx)(HelpTooltip, { id: "dac-lineage-help", label: t("help.label").replace("{label}", t("lineage.managedHeading")), children: t("lineage.scopeNote") }),
3976
+ ] }),
3874
3977
  (0, jsx.jsx)("small", { children: managedCountText })
3875
3978
  ] }),
3876
3979
  (0, jsx.jsxs)("div", { className: "dac-lineage-fold-actions", children: [
@@ -3891,7 +3994,6 @@ window.__ModuleLoader__.load({
3891
3994
  open && (0, jsx.jsx)("div", { role: "list", "aria-label": lineageWorkspaceLabel(t, { workspace: group.workspace }), className: "dac-lineage-tree", children: (0, jsx.jsx)(LineageTreeNodes, { nodes: group.roots, diagnostics: state.value.diagnostics, collapsed: visibleCollapsed, copiedId, foldingDisabled: searchActive, t, onCopy: copyId, onToggle: (id) => setCollapsed((current) => setVisibleSelection(current, [id], !current.has(id))) }) })
3892
3995
  ] }, group.key);
3893
3996
  }) }),
3894
- (0, jsx.jsx)("div", { className: "dac-lineage-scopebar", role: "note", children: t("lineage.scopeNote") })
3895
3997
  ] });
3896
3998
  }
3897
3999
 
@@ -3954,7 +4056,7 @@ window.__ModuleLoader__.load({
3954
4056
  ["lineage", "tab.lineage"],
3955
4057
  ["about", "tab.about"],
3956
4058
  ];
3957
- function ArchiveTabs({ pageMode, onChange, t }) {
4059
+ function ArchiveTabs({ pageMode, onChange, t, updateStatus }) {
3958
4060
  const move = (event, index) => {
3959
4061
  let next = index;
3960
4062
  if (event.key === "ArrowRight") next = (index + 1) % ARCHIVE_TABS.length;
@@ -3974,10 +4076,11 @@ window.__ModuleLoader__.load({
3974
4076
  className: "dac-tab",
3975
4077
  "aria-selected": pageMode === mode,
3976
4078
  "aria-controls": `dac-panel-${mode}`,
4079
+ "aria-label": mode === "about" && updateStatus === "available" ? `${t(label)} · ${t("about.updateAvailable")}` : void 0,
3977
4080
  tabIndex: pageMode === mode ? 0 : -1,
3978
4081
  onKeyDown: (event) => move(event, index),
3979
4082
  onClick: () => onChange(mode),
3980
- children: t(label),
4083
+ children: (0, jsx.jsxs)("span", { className: "dac-tab-label", children: [t(label), mode === "about" && updateStatus === "available" && (0, jsx.jsx)("span", { className: "dac-update-dot", "aria-hidden": true })] }),
3981
4084
  }, mode)) });
3982
4085
  }
3983
4086
 
@@ -4130,15 +4233,11 @@ window.__ModuleLoader__.load({
4130
4233
  return markArchiveDialog(pageRef.current);
4131
4234
  }, [sessions, loadError]);
4132
4235
 
4133
- // Success confirmations are transient toasts;
4134
- // errors stay until dismissed.
4135
- _react.useEffect(() => {
4136
- if (notice?.kind !== "ok" || notice.action === "view-archived") return undefined;
4137
- const timer = setTimeout(() => {
4138
- setNotice((current) => (current === notice ? null : current));
4139
- }, 4000);
4140
- return () => clearTimeout(timer);
4141
- }, [notice]);
4236
+ const noticeInteraction = useSuccessNoticeTimer(notice, notice?.kind === "ok", () => setNotice((current) => current === notice ? null : current));
4237
+ const changePageMode = (mode) => {
4238
+ if (mode !== pageMode) setNotice(null);
4239
+ setPageMode(mode);
4240
+ };
4142
4241
 
4143
4242
  const toggleCollapsed = (key) => {
4144
4243
  setCollapsed((prev) => {
@@ -4868,7 +4967,7 @@ window.__ModuleLoader__.load({
4868
4967
  })
4869
4968
  ]
4870
4969
  }),
4871
- (0, jsx.jsx)(ArchiveTabs, { pageMode, onChange: setPageMode, t }),
4970
+ (0, jsx.jsx)(ArchiveTabs, { pageMode, onChange: changePageMode, t, updateStatus: about?.update?.status }),
4872
4971
  pageMode === "archived" && (0, jsx.jsxs)("div", { id: "dac-panel-archived", role: "tabpanel", "aria-labelledby": "dac-tab-archived", className: "dac-mode-panel", children: [
4873
4972
  metadataStatus === "unavailable" && (0, jsx.jsx)("div", {
4874
4973
  className: "dac-warn",
@@ -4932,6 +5031,7 @@ window.__ModuleLoader__.load({
4932
5031
  }),
4933
5032
  notice !== null && notice.kind === "ok" && (0, jsx.jsxs)("div", {
4934
5033
  className: "dac-toast",
5034
+ ...noticeInteraction,
4935
5035
  role: "status",
4936
5036
  style: { top: `${toastTop()}px` },
4937
5037
  children: [
@@ -4986,7 +5086,7 @@ window.__ModuleLoader__.load({
4986
5086
  (0, jsx.jsx)("button", { type: "button", className: "dac-retry", onClick: loadTrash, children: t("state.retry") }),
4987
5087
  ] }),
4988
5088
  trash.status === "ready" && trash.trashStatus !== "ready" && (0, jsx.jsx)("div", { className: "dac-warn", role: "status", children: t("trash.unavailable") }),
4989
- notice !== null && notice.kind === "ok" && (0, jsx.jsxs)("div", { className: "dac-toast", role: "status", style: { top: `${toastTop()}px` }, children: [
5089
+ notice !== null && notice.kind === "ok" && (0, jsx.jsxs)("div", { ...noticeInteraction, className: "dac-toast", role: "status", style: { top: `${toastTop()}px` }, children: [
4990
5090
  (0, jsx.jsx)(IconCheckCircle, {}), (0, jsx.jsx)("span", { children: notice.text }),
4991
5091
  notice.action === "undo-trash" && (0, jsx.jsx)("button", { type: "button", className: "dac-notice-action", onClick: () => undoTrashMove(notice.ids), children: t("trash.undo") }),
4992
5092
  notice.action === "view-archived" && (0, jsx.jsx)("button", { type: "button", className: "dac-notice-action", onClick: viewRestoredArchive, children: t("trash.viewArchived") }),
@@ -5139,6 +5239,8 @@ window.__ModuleLoader__.load({
5139
5239
  exports.SETTINGS_NS = SETTINGS_NS;
5140
5240
  exports.__test = { NotePreview, formatBytes, matchesArchivedSession, filterByTag, sortArchivedSessions, setVisibleSelection, uniqueSessionIds, createArchiveNoticeController, installArchiveNoticeInterceptor, openArchiveSettings, ArchiveNoticeOverlay, WorkspaceArchiveChooserDialog, WorkspaceArchiveDialog, MetadataDialog, ImportDialog, ArchiveTabs, defaultRetentionSelection, filterLineageForest, groupLineageWorkspaces, SnapshotInsightRows, LineageTreeNodes, StorageRetentionPanel, ArchiveRelationshipsPanel, groupTrashSessions, trashStatusLabel, markArchiveDialog, submitExport, submitImportFile, fetchTrash, fetchInsights, saveRetentionPolicy, previewRetention, applyRetention, fetchLineage, restoreTrash, purgeTrash, emptyTrash, fetchArchivePreview, fetchArchiveImage, fetchArchiveSearch, fetchWorkspaceArchiveWorkspaces, previewWorkspaceArchive, applyWorkspaceArchive, buildPreviewNodes, previewCopyText, groupPreviewSegments, resolvePreviewPrimitives, PreviewMarkdown, PreviewReasoning, PreviewDisclosure, PreviewMessage, PreviewActions, previewFocusables, PreviewImage, unarchiveFailureText, importWarningText, editIconSpec: EDIT_ICON_SPEC };
5141
5241
  exports.__test.importFailureText = importFailureText;
5242
+ exports.__test.useSuccessNoticeTimer = useSuccessNoticeTimer;
5243
+ exports.__test.HelpTooltip = HelpTooltip;
5142
5244
  exports.apply = apply;
5143
5245
  exports.inject = inject;
5144
5246
  return module.exports;
package/lib/export.js CHANGED
@@ -2,7 +2,8 @@ import {
2
2
  deriveEventMessage,
3
3
  isAppendSurfaceEvent,
4
4
  } from '@deepseek-ai/dsh-session';
5
- import ZipStream from 'zip-stream';
5
+ import { Readable } from 'node:stream';
6
+ import { Zip, ZipDeflate, strToU8 } from 'fflate';
6
7
  import { IMPORT_LIMITS, validateBackupSemantics, validateJsonValue } from './import.js';
7
8
 
8
9
  const EXPORT_FORMAT = 'dsh-archived-chats/export';
@@ -302,13 +303,122 @@ function formattedJson(value) {
302
303
  return `${JSON.stringify(value, null, 2)}\n`;
303
304
  }
304
305
 
305
- function addZipEntry(archive, source, name, date) {
306
- return new Promise((resolve, reject) => {
307
- archive.entry(source, { name, date }, (error) => {
308
- if (error) reject(error);
309
- else resolve();
310
- });
306
+ /**
307
+ * Bytes handed to one synchronous deflate call. fflate compresses on the calling
308
+ * thread, so a whole entry at once would stall the event loop for the length of
309
+ * its compression. Slicing bounds each stall to a few milliseconds while leaving
310
+ * the archive byte-for-byte deterministic for a given plan.
311
+ */
312
+ const ZIP_ENTRY_SLICE_BYTES = 256 * 1024;
313
+ const ZIP_COMPRESSION_LEVEL = 9;
314
+ const ZIP_STREAM_HIGH_WATER_MARK = 64 * 1024;
315
+
316
+ /**
317
+ * Bridge fflate's callback-driven archive into a Node readable. fflate pushes
318
+ * synchronously from `push()`, so the chunks are queued and drained through the
319
+ * readable's own backpressure instead of being forwarded directly.
320
+ */
321
+ function createArchiveStream(staged, entryDate) {
322
+ const state = {
323
+ queue: [],
324
+ parked: null,
325
+ halted: false,
326
+ started: false,
327
+ failure: null,
328
+ finalized: false,
329
+ };
330
+
331
+ const stream = new Readable({
332
+ highWaterMark: ZIP_STREAM_HIGH_WATER_MARK,
333
+ read() {
334
+ if (state.parked !== null) {
335
+ const resume = state.parked;
336
+ state.parked = null;
337
+ resume();
338
+ }
339
+ if (!state.started) {
340
+ state.started = true;
341
+ void pump();
342
+ }
343
+ },
344
+ destroy(error, done) {
345
+ // Release a producer parked on backpressure, otherwise the pump would wait
346
+ // for a read that a destroyed stream never issues.
347
+ state.halted = true;
348
+ if (state.parked !== null) {
349
+ const resume = state.parked;
350
+ state.parked = null;
351
+ resume();
352
+ }
353
+ done(error);
354
+ },
355
+ });
356
+
357
+ const archive = new Zip((error, chunk) => {
358
+ if (error) {
359
+ state.failure ??= error;
360
+ return;
361
+ }
362
+ if (chunk !== null && chunk.byteLength > 0) state.queue.push(chunk);
311
363
  });
364
+
365
+ const stopped = () => state.halted || stream.destroyed;
366
+
367
+ /** Push one chunk, waiting for the consumer when the buffer is full. */
368
+ const emit = async (chunk) => {
369
+ if (stopped()) return false;
370
+ if (stream.push(chunk)) return true;
371
+ await new Promise((resolve) => { state.parked = resolve; });
372
+ return !stopped();
373
+ };
374
+
375
+ /** Forward everything fflate produced for the current push. */
376
+ const drain = async () => {
377
+ while (state.queue.length > 0) {
378
+ const chunk = state.queue.shift();
379
+ if (!(await emit(chunk))) return false;
380
+ }
381
+ return !stopped();
382
+ };
383
+
384
+ const pump = async () => {
385
+ try {
386
+ for (const entry of staged) {
387
+ const file = new ZipDeflate(entry.name, { level: ZIP_COMPRESSION_LEVEL });
388
+ file.mtime = entryDate;
389
+ archive.add(file);
390
+ const bytes = strToU8(entry.source);
391
+ if (bytes.byteLength === 0) {
392
+ file.push(bytes, true);
393
+ if (!(await drain())) return;
394
+ continue;
395
+ }
396
+ for (let offset = 0; offset < bytes.byteLength; offset += ZIP_ENTRY_SLICE_BYTES) {
397
+ const end = Math.min(offset + ZIP_ENTRY_SLICE_BYTES, bytes.byteLength);
398
+ const last = end >= bytes.byteLength;
399
+ file.push(bytes.subarray(offset, end), last);
400
+ if (!(await drain())) return;
401
+ // Yield between slices so other requests keep their turn on the loop.
402
+ if (!last && !stopped()) await new Promise((resolve) => { setImmediate(resolve); });
403
+ }
404
+ if (stopped()) return;
405
+ }
406
+ archive.end();
407
+ if (!(await drain())) return;
408
+ if (state.failure !== null) throw state.failure;
409
+ state.finalized = true;
410
+ stream.push(null);
411
+ } catch (error) {
412
+ stream.destroy(error);
413
+ }
414
+ };
415
+
416
+ const completion = new Promise((resolve, reject) => {
417
+ stream.once('end', resolve);
418
+ stream.once('error', reject);
419
+ });
420
+
421
+ return { stream, completion, archive };
312
422
  }
313
423
 
314
424
  /**
@@ -365,28 +475,15 @@ export async function createExportZip({ plan, inspect, generatorVersion, limits:
365
475
  throw Object.assign(new Error('export source is invalid'), { code: 'export-source-invalid', errors: semanticErrors });
366
476
  }
367
477
 
368
- const archive = new ZipStream({ level: 9 });
369
478
  const entryDate = new Date(plan.exportedAt);
370
-
371
- const completion = new Promise((resolve, reject) => {
372
- archive.once('end', resolve);
373
- archive.once('error', reject);
374
- });
375
-
376
- const write = async () => {
377
- for (const entry of staged) await addZipEntry(archive, entry.source, entry.name, entryDate);
378
- archive.finalize();
379
- };
380
-
381
- void write().catch((error) => {
382
- archive.destroy(error);
383
- });
479
+ const { stream, completion, archive } = createArchiveStream(staged, entryDate);
384
480
 
385
481
  return {
386
- stream: archive,
482
+ stream,
387
483
  completion,
388
484
  abort(error = new Error('export aborted')) {
389
- archive.destroy(error);
485
+ archive.terminate();
486
+ stream.destroy(error);
390
487
  },
391
488
  };
392
489
  }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Browser client entry — the Session Archive settings section with a workspace
2
+ * Browser client entry — the Archive Management settings section with a workspace
3
3
  * chooser and one-confirmation bulk archive flow. The flow stays inside the
4
4
  * plugin-owned Settings surface and requires no workspace-row extension slot.
5
5
  * The searchable manager includes Archived, Recycle Bin, Storage & Retention,
6
- * and Origins & Branches views. Removing an archived chat moves it to
7
- * recoverable trash and exposes immediate Undo. The recycle view provides
8
- * scoped read-only previews, original/snapshot restore, guarded
6
+ * Origins & Branches, and About views. Only workspace actions move archived
7
+ * chats to the Recycle Bin and offer immediate Undo. Delete/Delete all instead
8
+ * require confirmation for irreversible permanent deletion. Recycle restore
9
+ * returns chats to Archived; Unarchive returns them to the main chat area.
10
+ * The recycle view provides original-backed read-only previews,
11
+ * capability-dependent original/snapshot restore, guarded
9
12
  * permanent purge and empty operations, degraded-state warnings, responsive
10
13
  * rows, and accessible confirmation dialogs, all localized in English and 中文.
11
14
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-archived-chats",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "查看、搜索和恢复已归档会话,支持按工作区批量归档、备份导入导出与回收站管理。",
5
5
  "license": "MIT",
6
6
  "author": "Ultronen",
@@ -105,8 +105,7 @@
105
105
  }
106
106
  },
107
107
  "dependencies": {
108
- "fflate": "^0.8.3",
109
- "zip-stream": "^7.0.5"
108
+ "fflate": "^0.8.3"
110
109
  },
111
110
  "devDependencies": {
112
111
  "@deepseek-ai/dsh-session": "0.1.5-rc.2",