@michengai/dsh-archive-manager 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,134 +1,188 @@
1
- <p align="center">
2
- <img src="assets/icon.png" alt="DSH Archive Manager" width="96">
3
- </p>
4
-
5
- <h1 align="center">DSH Archive Manager</h1>
6
-
7
- <p align="center">
8
- <strong>A DeepSeek Harness Web plugin for safely managing archived sessions.</strong>
9
- </p>
10
-
11
- <p align="center">
12
- <a href="https://github.com/MichengAI/dsh-archive-manager/issues">Report an issue</a>
13
- · <a href="https://www.npmjs.com/package/@michengai/dsh-archive-manager">View on npm</a>
14
- · <a href="README.zh-CN.md">简体中文</a>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="Apache License 2.0"></a>
19
- <a href="https://www.npmjs.com/package/@michengai/dsh-archive-manager"><img src="https://img.shields.io/npm/v/%40michengai/dsh-archive-manager?label=npm" alt="npm package"></a>
20
- <img src="https://img.shields.io/badge/DSH-Web%20Plugin-10b981" alt="DSH Web Plugin">
21
- <img src="https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 22 or later">
22
- </p>
23
-
24
- <p align="center">
25
- <img src="assets/screenshots/archived-sessions.png" alt="Archived Sessions settings page" width="1200">
26
- </p>
27
-
28
- > DSH Archive Manager is a community-maintained plugin, not an official DeepSeek AI product.
29
-
30
- ## Features
31
-
32
- - Archives a session from the sidebar session menu.
33
- - Lists archived sessions by workspace in **Settings → Archived Sessions**.
34
- - Safely restores a session to its original workspace position.
35
- - Permanently deletes a confirmed session, its workspace association, archive marker, and projection cache.
36
- - Removes unloaded deleted sessions from connected sidebars immediately.
37
-
38
- ## Prerequisites
39
-
40
- - A working DeepSeek Harness Web installation with `dsh` available in PowerShell.
41
- - Examples use the `web` profile; replace it with the target profile.
42
- - Source installation and development require Node.js 22+ and pnpm. npm installation does not require running `npm install` in an arbitrary directory.
43
-
44
- ## Installation
45
-
46
- ### Install from npm
47
-
48
- Run this from any PowerShell directory. Install into the DSH profile through `dsh plugin`, not as a dependency of an unrelated project:
49
-
50
- ```powershell
51
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
52
- $OutputEncoding = [System.Text.Encoding]::UTF8
53
- dsh plugin --profile web add @michengai/dsh-archive-manager
54
- dsh --profile web --dump-config
55
- ```
56
-
57
- Restart DSH Web and hard-refresh the browser after installation or upgrade. If a package mirror is behind, append `--registry=https://registry.npmjs.org/`.
58
-
59
- ### Install from source
60
-
61
- Use this for debugging or unpublished changes. The cloned directory becomes the plugin source path:
62
-
63
- ```powershell
64
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
65
- $OutputEncoding = [System.Text.Encoding]::UTF8
66
- Set-Location D:\Repository\deepseek-harness-plugin
67
- git clone https://github.com/MichengAI/dsh-archive-manager.git
68
- Set-Location .\dsh-archive-manager
69
- pnpm install --frozen-lockfile
70
- pnpm build
71
- dsh plugin --profile web add .
72
- dsh --profile web --dump-config
73
- ```
74
-
75
- Restart DSH Web and hard-refresh the browser. Local installation applies `cordis.patch.yml`; do not copy `lib` or client files manually.
76
-
77
- ## Usage
78
-
79
- 1. Open the sidebar session menu and choose **Archive session**.
80
- 2. Open **Settings → Archived Sessions** to inspect sessions by workspace.
81
- 3. Select **Unarchive** to restore a session, or use the delete icon to remove it permanently.
82
- 4. Confirm deletion. **It cannot be undone.**
83
-
84
- <p align="center">
85
- <img src="assets/screenshots/archive-session-menu.png" alt="Archive a session from the session menu" width="1200">
86
- </p>
87
-
88
- If the entry is missing after installation or upgrade, restart DSH Web and hard-refresh the browser. It is located directly after **Connectors** in Settings.
89
-
90
- ## Data handling limits
91
-
92
- - Deletion always requires confirmation.
93
- - It removes the session directory, workspace records, archive set, and projection cache.
94
- - A live session finishes writing before cleanup to prevent data truncation.
95
- - The plugin replaces DSH’s default workspace and projection services. Install through the DSH profile instead of manually composing the patch.
96
-
97
- ## Secondary development
98
-
99
- This repository has no `src` directory. `lib` is directly maintained runtime source, which is its current layout rather than the recommended layout for new plugins. New plugins should prefer `src` built to `lib`.
100
-
101
- - [lib\index.js](lib/index.js): host service entry point.
102
- - [lib\workspace.js](lib/workspace.js): archived-session and workspace service.
103
- - [lib\projcache.js](lib/projcache.js): session projection cache.
104
- - [lib\client.js](lib/client.js): Settings page and archive UI.
105
- - `test\*.test.mjs`: host, client, Remote, and styling coverage.
106
-
107
- After changing the runtime source, validate, test, and install from the local directory:
108
-
109
- ```powershell
110
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
111
- $OutputEncoding = [System.Text.Encoding]::UTF8
112
- pnpm build
113
- pnpm test
114
- pnpm pack:check
115
- dsh plugin --profile web add .
116
- ```
117
-
118
- `pnpm build` validates package integrity; it does not compile `lib` into another directory.
119
-
120
- ## Validation
121
-
122
- ```powershell
123
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
124
- $OutputEncoding = [System.Text.Encoding]::UTF8
125
- pnpm build
126
- pnpm test
127
- pnpm pack:check
128
- ```
129
-
130
- `prepublishOnly` runs the build check and tests before publishing.
131
-
132
- ## License
133
-
134
- Licensed under [Apache License 2.0](LICENSE).
1
+ <div align="center">
2
+ <table cellpadding="0" cellspacing="0">
3
+ <tr>
4
+ <td valign="middle" align="center">
5
+ <img src="assets/icon.png" alt="DSH Archive Manager logo" width="168">
6
+ </td>
7
+ <td valign="middle">
8
+ <pre>
9
+ ____ ____ _ _
10
+ | _ \/ ___|| | | |
11
+ | | | \___ \| |_| |
12
+ | |_| |___) | _ |
13
+ |____/|____/|_| |_|
14
+ ARCHIVE MANAGER
15
+ Safe archived session control
16
+ DeepSeek Harness plugin
17
+ </pre>
18
+ </td>
19
+ </tr>
20
+ </table>
21
+ </div>
22
+
23
+ <div align="center">
24
+
25
+ # DSH Archive Manager
26
+
27
+ **Safely manage archived sessions in DeepSeek Harness**
28
+
29
+ [简体中文](README.zh-CN.md) · [Apache-2.0](LICENSE)
30
+
31
+ [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
32
+ [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-archive-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
33
+ [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/MichengAI/dsh-archive-manager)
34
+ [![Node.js 22 or later](https://img.shields.io/badge/Node.js-22%20or%20later-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
35
+ </div>
36
+
37
+ > DSH Archive Manager is a community-maintained DeepSeek Harness (DSH) plugin, not an official DeepSeek AI product.
38
+
39
+ ## Features
40
+
41
+ - Archive a session from the sidebar session menu.
42
+ - Search archived chats and filter them by workspace in **Settings → Archived**.
43
+ - Restore a session to its original workspace with **Unarchive**.
44
+ - Permanently delete a confirmed session, its workspace association, archive marker, and projection cache.
45
+ - Delete all archived chats after confirmation, including child agents.
46
+ - Remove unloaded deleted sessions from connected sidebars immediately.
47
+ - Paste one sentence into DSH, Codex, or WorkBuddy and let that agent install the plugin locally.
48
+
49
+ ## Screenshots
50
+
51
+ Open the sidebar session menu and choose **Archive session**:
52
+
53
+ ![Archive a session from the session menu](assets/screenshots/archive-session-menu.png)
54
+
55
+ Search, filter by project, unarchive, or permanently delete chats in **Settings → Archived**:
56
+
57
+ ![Archived chats settings page](assets/screenshots/archived-sessions.png)
58
+
59
+ ## Prerequisites
60
+
61
+ - A working DeepSeek Harness Web installation with `dsh` available in PowerShell.
62
+ - Examples use the `web` profile; replace it with the target profile.
63
+ - Source installation and development require Node.js 22+ and pnpm. npm installation does not require running `pnpm install` separately.
64
+
65
+ ## Installation
66
+
67
+ `dsh plugin add` forwards to `pnpm add` in the profile directory. Without a version and official registry, a local mirror or minimum-release-age policy can leave you on an older build.
68
+
69
+ ### Ask another agent to install it
70
+
71
+ This plugin runs inside DeepSeek Harness Web. Copy one of the sentences below into DSH, Codex, or WorkBuddy and let that agent install it into your local `web` profile.
72
+
73
+ From npm:
74
+
75
+ ```text
76
+ Install the latest DSH plugin @michengai/dsh-archive-manager into my local web profile using the official npm registry: dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/. Then run dsh --profile web --dump-config, confirm archive-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
77
+ ```
78
+
79
+ From source:
80
+
81
+ ```text
82
+ Install the DSH plugin from source at https://github.com/MichengAI/dsh-archive-manager: clone it, run pnpm install --frozen-lockfile and pnpm build, then run dsh plugin --profile web add . from that directory. Do not copy lib by itself. Then run dsh --profile web --dump-config, confirm archive-manager is mounted, and remind me to restart DSH Web and hard-refresh the browser.
83
+ ```
84
+
85
+ | Product | How to use it |
86
+ | --- | --- |
87
+ | DSH | Send one of the sentences above to the current session. |
88
+ | Codex | Send one of the sentences above to Codex and let it install locally. |
89
+ | WorkBuddy | Send one of the sentences above to WorkBuddy; for a source install you can also paste `https://github.com/MichengAI/dsh-archive-manager`. |
90
+
91
+ Codex and WorkBuddy only install the plugin. After that, open DSH Web and use **Settings → Archived**.
92
+
93
+ You can also run the same npm command yourself:
94
+
95
+ ```powershell
96
+ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
97
+ ```
98
+
99
+ If `dsh` is not on PATH, replace the leading `dsh` with `npx --yes @deepseek-ai/dsh`.
100
+
101
+ ### Install the latest package from the official npm registry
102
+
103
+ Run this from any PowerShell directory:
104
+
105
+ ```powershell
106
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
107
+ $OutputEncoding = [System.Text.Encoding]::UTF8
108
+ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
109
+ dsh --profile web --dump-config
110
+ ```
111
+
112
+ To pin a release, replace `@latest` with a version such as `@0.1.8`.
113
+
114
+ The configuration output should contain `workspace-archive-manager` and `ui-workspace-archive-manager`. Restart DSH Web and hard-refresh the browser. Do not copy client files manually: the Settings page and archive menu need the mounted plugin.
115
+
116
+ ### Install from source
117
+
118
+ Use this for debugging or unpublished changes. The cloned directory becomes the plugin source path:
119
+
120
+ ```powershell
121
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
122
+ $OutputEncoding = [System.Text.Encoding]::UTF8
123
+ Set-Location D:\Repository\deepseek-harness-plugin
124
+ git clone https://github.com/MichengAI/dsh-archive-manager.git
125
+ Set-Location .\dsh-archive-manager
126
+ pnpm install --frozen-lockfile
127
+ pnpm build
128
+ dsh plugin --profile web add .
129
+ dsh --profile web --dump-config
130
+ ```
131
+
132
+ Restart DSH Web and hard-refresh the browser. `dsh plugin ... add .` reads the package metadata and `cordis.patch.yml`; do not install by copying `lib` directly.
133
+
134
+ ## Usage
135
+
136
+ 1. Open the sidebar session menu and choose **Archive session**.
137
+ 2. Open **Settings → Archived** to inspect sessions by workspace.
138
+ 3. Use the search box or project filter to narrow the list.
139
+ 4. Select **Unarchive** to restore a session, or use the delete icon to remove it permanently.
140
+ 5. Confirm deletion. **It cannot be undone.**
141
+
142
+ If the entry is missing after installation or upgrade, restart DSH Web and hard-refresh the browser. It is located directly after **Connectors** in Settings.
143
+
144
+ ## Data handling limits
145
+
146
+ - Deletion always requires confirmation.
147
+ - It removes the session directory, workspace records, archive set, and projection cache.
148
+ - A live session finishes writing before cleanup to prevent data truncation.
149
+ - The plugin replaces DSH’s default workspace and projection services. Install through the DSH profile instead of manually composing the patch.
150
+
151
+ ## Secondary development
152
+
153
+ This repository has no `src` directory. `lib` is directly maintained runtime source, which is its current layout rather than the recommended layout for new plugins. New plugins should prefer `src` built to `lib`.
154
+
155
+ - [lib\index.js](lib/index.js): host service entry point.
156
+ - [lib\workspace.js](lib/workspace.js): archived-session and workspace service.
157
+ - [lib\projcache.js](lib/projcache.js): session projection cache.
158
+ - [lib\client.js](lib/client.js): Settings page and archive UI.
159
+ - `test\*.test.mjs`: host, client, Remote, and styling coverage.
160
+
161
+ After changing the runtime source, validate, test, and install from the local directory:
162
+
163
+ ```powershell
164
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
165
+ $OutputEncoding = [System.Text.Encoding]::UTF8
166
+ pnpm build
167
+ pnpm test
168
+ pnpm pack:check
169
+ dsh plugin --profile web add .
170
+ ```
171
+
172
+ `pnpm build` validates package integrity; it does not compile `lib` into another directory.
173
+
174
+ ## Validation
175
+
176
+ ```powershell
177
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
178
+ $OutputEncoding = [System.Text.Encoding]::UTF8
179
+ pnpm build
180
+ pnpm test
181
+ pnpm pack:check
182
+ ```
183
+
184
+ `prepublishOnly` runs the build check and tests before publishing.
185
+
186
+ ## License
187
+
188
+ Licensed under [Apache License 2.0](LICENSE).
package/README.zh-CN.md CHANGED
@@ -1,134 +1,188 @@
1
- <p align="center">
2
- <img src="assets/icon.png" alt="DSH Archive Manager" width="96">
3
- </p>
4
-
5
- <h1 align="center">DSH Archive Manager</h1>
6
-
7
- <p align="center">
8
- <strong>安全管理已归档会话的 DeepSeek Harness Web 插件。</strong>
9
- </p>
10
-
11
- <p align="center">
12
- <a href="https://github.com/MichengAI/dsh-archive-manager/issues">反馈问题</a>
13
- · <a href="https://www.npmjs.com/package/@michengai/dsh-archive-manager">在 npm 查看</a>
14
- · <a href="README.md">English</a>
15
- </p>
16
-
17
- <p align="center">
18
- <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="Apache License 2.0"></a>
19
- <a href="https://www.npmjs.com/package/@michengai/dsh-archive-manager"><img src="https://img.shields.io/npm/v/%40michengai/dsh-archive-manager?label=npm" alt="npm 包"></a>
20
- <img src="https://img.shields.io/badge/DSH-Web%20Plugin-10b981" alt="DSH Web Plugin">
21
- <img src="https://img.shields.io/badge/Node.js-22%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 22 或更高版本">
22
- </p>
23
-
24
- <p align="center">
25
- <img src="assets/screenshots/archived-sessions.png" alt="已归档会话设置页面" width="1200">
26
- </p>
27
-
28
- > DSH Archive Manager 是社区维护的插件,并非 DeepSeek AI 官方产品。
29
-
30
- ## 功能概览
31
-
32
- - 在侧栏会话菜单中选择「归档会话」。
33
- - 在「设置 → 已归档」按工作区展示归档会话。
34
- - 安全取消归档,将会话恢复到原工作区位置。
35
- - 经确认后永久删除会话、工作区归属、归档标记和投影缓存。
36
- - 已删除的未加载归档会话会立即从已连接客户端的侧栏移除。
37
-
38
- ## 前置条件
39
-
40
- - 已可正常运行 DeepSeek Harness Web,且可在 PowerShell 中使用 `dsh`。
41
- - 以下示例使用 `web` profile;请替换为实际目标 profile。
42
- - 从源码安装或二次开发需要 Node.js 22+ 与 pnpm;仅从 npm 安装无需在任意目录执行 `npm install`。
43
-
44
- ## 安装
45
-
46
- ### 从 npm 安装
47
-
48
- 在任意 PowerShell 目录执行。请通过 `dsh plugin` 安装到 DSH profile,而不是在普通项目中安装该 npm 包:
49
-
50
- ```powershell
51
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
52
- $OutputEncoding = [System.Text.Encoding]::UTF8
53
- dsh plugin --profile web add @michengai/dsh-archive-manager
54
- dsh --profile web --dump-config
55
- ```
56
-
57
- 安装或升级后重启 DSH Web,并在浏览器硬刷新。若镜像未同步最新版本,可在安装命令末尾追加 `--registry=https://registry.npmjs.org/`。
58
-
59
- ### 从源码安装
60
-
61
- 适用于调试或使用未发布改动。克隆后的目录会直接作为插件安装路径:
62
-
63
- ```powershell
64
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
65
- $OutputEncoding = [System.Text.Encoding]::UTF8
66
- Set-Location D:\Repository\deepseek-harness-plugin
67
- git clone https://github.com/MichengAI/dsh-archive-manager.git
68
- Set-Location .\dsh-archive-manager
69
- pnpm install --frozen-lockfile
70
- pnpm build
71
- dsh plugin --profile web add .
72
- dsh --profile web --dump-config
73
- ```
74
-
75
- 完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会自动应用 `cordis.patch.yml`;不要手工复制 `lib` 或客户端文件。
76
-
77
- ## 使用
78
-
79
- 1. 在侧栏右键或打开会话菜单,选择「归档会话」。
80
- 2. 打开「设置 → 已归档」,按工作区查看归档会话。
81
- 3. 点击「取消归档」恢复会话;点击删除图标永久移除会话。
82
- 4. 删除前确认提示。**删除无法撤销。**
83
-
84
- <p align="center">
85
- <img src="assets/screenshots/archive-session-menu.png" alt="从会话菜单归档会话" width="1200">
86
- </p>
87
-
88
- 安装或升级后找不到入口时,重启 DSH Web 并硬刷新浏览器;入口位于「设置」中,连接器之后。
89
-
90
- ## 数据处理边界
91
-
92
- - 删除操作始终需要确认。
93
- - 删除会移除会话目录、工作区记录、归档集合和投影缓存。
94
- - 正在写入的会话会在完成写入后清理,避免截断数据。
95
- - 本插件替换 DSH 默认的工作区和会话投影服务;请仅通过 DSH profile 安装,避免手工拼接补丁配置。
96
-
97
- ## 二次开发
98
-
99
- 当前仓库未提供 `src` 源目录,`lib` 是直接维护的运行源码;这是当前仓库的实现方式,不是新插件的推荐布局。新插件建议使用 `src` 开发并构建到 `lib`:
100
-
101
- - [lib\index.js](lib/index.js):客户端插件 Host 服务入口。
102
- - [lib\workspace.js](lib/workspace.js):归档会话和工作区服务实现。
103
- - [lib\projcache.js](lib/projcache.js):会话投影缓存实现。
104
- - [lib\client.js](lib/client.js):设置页和归档会话界面。
105
- - `test\*.test.mjs`:Host、客户端、Remote 和样式边界测试。
106
-
107
- 修改后运行检查、测试并用本地目录重新安装:
108
-
109
- ```powershell
110
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
111
- $OutputEncoding = [System.Text.Encoding]::UTF8
112
- pnpm build
113
- pnpm test
114
- pnpm pack:check
115
- dsh plugin --profile web add .
116
- ```
117
-
118
- `pnpm build` 负责发布包完整性检查,不会将 `lib` 重新编译为其他目录。
119
-
120
- ## 验证
121
-
122
- ```powershell
123
- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
124
- $OutputEncoding = [System.Text.Encoding]::UTF8
125
- pnpm build
126
- pnpm test
127
- pnpm pack:check
128
- ```
129
-
130
- `prepublishOnly` 会在发布前执行构建检查与测试。
131
-
132
- ## 许可证
133
-
134
- 本项目采用 [Apache License 2.0](LICENSE)。
1
+ <div align="center">
2
+ <table cellpadding="0" cellspacing="0">
3
+ <tr>
4
+ <td valign="middle" align="center">
5
+ <img src="assets/icon.png" alt="DSH Archive Manager 标识" width="168">
6
+ </td>
7
+ <td valign="middle">
8
+ <pre>
9
+ ____ ____ _ _
10
+ | _ \/ ___|| | | |
11
+ | | | \___ \| |_| |
12
+ | |_| |___) | _ |
13
+ |____/|____/|_| |_|
14
+ ARCHIVE MANAGER
15
+ Safe archived session control
16
+ DeepSeek Harness plugin
17
+ </pre>
18
+ </td>
19
+ </tr>
20
+ </table>
21
+ </div>
22
+
23
+ <div align="center">
24
+
25
+ # DSH Archive Manager
26
+
27
+ **在 DeepSeek Harness 中安全管理已归档会话**
28
+
29
+ [English](README.md) · [Apache-2.0](LICENSE)
30
+
31
+ [![许可证:Apache-2.0](https://img.shields.io/badge/许可证-Apache--2.0-blue.svg)](LICENSE)
32
+ [![npm package](https://img.shields.io/npm/v/%40michengai%2Fdsh-archive-manager.svg?label=npm%20package)](https://www.npmjs.com/package/@michengai/dsh-archive-manager)
33
+ [![DSH Web Plugin](https://img.shields.io/badge/DSH%20Web-Plugin-0f766e.svg)](https://github.com/MichengAI/dsh-archive-manager)
34
+ [![Node.js 22 or later](https://img.shields.io/badge/Node.js-22%20or%20later-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
35
+ </div>
36
+
37
+ > DSH Archive Manager 是社区维护的 DeepSeek Harness(DSH)插件,并非 DeepSeek AI 官方产品。
38
+
39
+ ## 功能概览
40
+
41
+ - 在侧栏会话菜单中选择「归档会话」。
42
+ - 在「设置 → 已归档」按工作区查看归档会话,并支持搜索和按项目筛选。
43
+ - 安全取消归档,将会话恢复到原工作区位置。
44
+ - 经确认后永久删除会话、工作区归属、归档标记和投影缓存。
45
+ - 经确认后删除全部已归档聊天,包含子代理。
46
+ - 已删除的未加载归档会话会立即从已连接客户端的侧栏移除。
47
+ - 可把一句话复制到 DSH、Codex 或 WorkBuddy,让对方代装到本机 DSH。
48
+
49
+ ## 界面预览
50
+
51
+ 在侧栏会话菜单中选择「归档会话」:
52
+
53
+ ![从会话菜单归档会话](assets/screenshots/archive-session-menu.png)
54
+
55
+ 在「设置 → 已归档」中搜索、按项目筛选、取消归档或永久删除:
56
+
57
+ ![已归档聊天设置页面](assets/screenshots/archived-sessions.png)
58
+
59
+ ## 前置条件
60
+
61
+ - 已可正常运行 DeepSeek Harness Web,且可在 PowerShell 中使用 `dsh`。
62
+ - 以下示例使用 `web` profile;请替换为实际目标 profile。
63
+ - 从源码安装或二次开发需要 Node.js 22+ 与 pnpm;仅从 npm 安装无需另外执行 `pnpm install`。
64
+
65
+ ## 安装
66
+
67
+ `dsh plugin add` 会转发到 profile 目录里的 `pnpm add`。不写版本、不指定官方源时,本机镜像和最短发布间隔可能让你停在旧版。
68
+
69
+ ### 交给其他 Agent 一句话安装
70
+
71
+ 本插件运行在 DeepSeek Harness Web 里。把下面其中一句复制到 DSH、Codex 或 WorkBuddy,让它代你安装到本机 `web` profile。
72
+
73
+ 从 npm 安装:
74
+
75
+ ```text
76
+ 请把 DSH 插件 @michengai/dsh-archive-manager 最新版装进本机 web profile,使用官方 npm 源执行:dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/。装完执行 dsh --profile web --dump-config,确认已挂载 archive-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
77
+ ```
78
+
79
+ 从源码安装:
80
+
81
+ ```text
82
+ 请从源码安装 DSH 插件 https://github.com/MichengAI/dsh-archive-manager:克隆到本机后执行 pnpm install --frozen-lockfile 和 pnpm build,再用 dsh plugin --profile web add . 把当前目录装进 web profile。不要只复制 lib。装完执行 dsh --profile web --dump-config,确认已挂载 archive-manager,并提醒我重启 DSH Web 后硬刷新浏览器。
83
+ ```
84
+
85
+ | 产品 | 怎么用 |
86
+ | --- | --- |
87
+ | DSH | 把上面其中一句发给当前会话。 |
88
+ | Codex | 把上面其中一句发给 Codex,让它在本机执行安装。 |
89
+ | WorkBuddy | 把上面其中一句发给 WorkBuddy;源码安装也可同时粘贴仓库地址 `https://github.com/MichengAI/dsh-archive-manager`。 |
90
+
91
+ Codex 和 WorkBuddy 只负责代装;装好后仍要打开 DSH Web 使用「设置 → 已归档」。
92
+
93
+ 也可以自己执行同一条 npm 命令:
94
+
95
+ ```powershell
96
+ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
97
+ ```
98
+
99
+ 未把 `dsh` 装进 PATH 时,把开头的 `dsh` 换成 `npx --yes @deepseek-ai/dsh`。
100
+
101
+ ### 从官方 npm 安装最新版
102
+
103
+ 在任意 PowerShell 目录执行:
104
+
105
+ ```powershell
106
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
107
+ $OutputEncoding = [System.Text.Encoding]::UTF8
108
+ dsh plugin --profile web add @michengai/dsh-archive-manager@latest --registry=https://registry.npmjs.org/
109
+ dsh --profile web --dump-config
110
+ ```
111
+
112
+ 需要钉死某一版时,把 `@latest` 换成具体版本,例如 `@0.1.8`。
113
+
114
+ 配置输出中应包含 `workspace-archive-manager` 与 `ui-workspace-archive-manager`。安装后重启 DSH Web 并在浏览器硬刷新;请勿手工复制客户端文件,否则设置页和归档菜单不会被挂载。
115
+
116
+ ### 从源码安装
117
+
118
+ 适用于调试或使用未发布改动。克隆后的目录会直接作为插件安装路径:
119
+
120
+ ```powershell
121
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
122
+ $OutputEncoding = [System.Text.Encoding]::UTF8
123
+ Set-Location D:\Repository\deepseek-harness-plugin
124
+ git clone https://github.com/MichengAI/dsh-archive-manager.git
125
+ Set-Location .\dsh-archive-manager
126
+ pnpm install --frozen-lockfile
127
+ pnpm build
128
+ dsh plugin --profile web add .
129
+ dsh --profile web --dump-config
130
+ ```
131
+
132
+ 完成后重启 DSH Web 并硬刷新浏览器。`dsh plugin ... add .` 会读取当前目录的包信息和 `cordis.patch.yml`;不要改为直接复制 `lib` 目录。
133
+
134
+ ## 使用
135
+
136
+ 1. 在侧栏右键或打开会话菜单,选择「归档会话」。
137
+ 2. 打开「设置 → 已归档」,按工作区查看归档会话。
138
+ 3. 用搜索框或项目筛选收窄列表。
139
+ 4. 点击「取消归档」恢复会话;点击删除图标永久移除会话。
140
+ 5. 删除前确认提示。**删除无法撤销。**
141
+
142
+ 安装或升级后找不到入口时,重启 DSH Web 并硬刷新浏览器;入口位于「设置」中,连接器之后。
143
+
144
+ ## 数据处理边界
145
+
146
+ - 删除操作始终需要确认。
147
+ - 删除会移除会话目录、工作区记录、归档集合和投影缓存。
148
+ - 正在写入的会话会在完成写入后清理,避免截断数据。
149
+ - 本插件替换 DSH 默认的工作区和会话投影服务;请仅通过 DSH profile 安装,避免手工拼接补丁配置。
150
+
151
+ ## 二次开发
152
+
153
+ 当前仓库未提供 `src` 源目录,`lib` 是直接维护的运行源码;这是当前仓库的实现方式,不是新插件的推荐布局。新插件建议使用 `src` 开发并构建到 `lib`:
154
+
155
+ - [lib\index.js](lib/index.js):客户端插件 Host 服务入口。
156
+ - [lib\workspace.js](lib/workspace.js):归档会话和工作区服务实现。
157
+ - [lib\projcache.js](lib/projcache.js):会话投影缓存实现。
158
+ - [lib\client.js](lib/client.js):设置页和归档会话界面。
159
+ - `test\*.test.mjs`:Host、客户端、Remote 和样式边界测试。
160
+
161
+ 修改后运行检查、测试并用本地目录重新安装:
162
+
163
+ ```powershell
164
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
165
+ $OutputEncoding = [System.Text.Encoding]::UTF8
166
+ pnpm build
167
+ pnpm test
168
+ pnpm pack:check
169
+ dsh plugin --profile web add .
170
+ ```
171
+
172
+ `pnpm build` 负责发布包完整性检查,不会将 `lib` 重新编译为其他目录。
173
+
174
+ ## 验证
175
+
176
+ ```powershell
177
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
178
+ $OutputEncoding = [System.Text.Encoding]::UTF8
179
+ pnpm build
180
+ pnpm test
181
+ pnpm pack:check
182
+ ```
183
+
184
+ `prepublishOnly` 会在发布前执行构建检查与测试。
185
+
186
+ ## 许可证
187
+
188
+ 本项目采用 [Apache License 2.0](LICENSE)。
@@ -0,0 +1,11 @@
1
+ README 左侧使用仓库已有 assets/icon.png(宽 168,不随表格均分撑开)
2
+ 右侧字符:
3
+
4
+ ____ ____ _ _
5
+ | _ \/ ___|| | | |
6
+ | | | \___ \| |_| |
7
+ | |_| |___) | _ |
8
+ |____/|____/|_| |_|
9
+ ARCHIVE MANAGER
10
+ Safe archived session control
11
+ DeepSeek Harness plugin
package/lib/client.js CHANGED
@@ -229,8 +229,7 @@ window.__ModuleLoader__.load({
229
229
  return ordered;
230
230
  }
231
231
  /**
232
- * 归档设置页“删除全部”的目标:全部已归档 id,加上这些父会话下仍在目录中的 subagent 子会话。
233
- * 每棵树都是先子后父,避免级联删除后再删子会话时被当成未知。
232
+ * 仅供 __test 使用:展开已归档 id 及其仍在目录中的 subagent 子会话,先子后父。运行时删除只提交根会话或归档集合。
234
233
  */
235
234
  function collectArchivedDeleteAllIds(archivedSessionIds, byId) {
236
235
  const catalog = byId ?? {};
@@ -276,6 +275,15 @@ window.__ModuleLoader__.load({
276
275
  const detail = reason instanceof Error ? reason.message : String(reason);
277
276
  return t("archives.unarchiveFailed", { detail });
278
277
  }
278
+ function formatArchiveError(reason, t) {
279
+ if (isUnknownSessionError(reason)) return t("archives.archiveUnknown");
280
+ const detail = reason instanceof Error ? reason.message : String(reason);
281
+ return t("archives.archiveFailed", { detail });
282
+ }
283
+ function formatForkError(reason, t) {
284
+ const detail = reason instanceof Error ? reason.message : String(reason);
285
+ return t("archives.forkFailed", { detail });
286
+ }
279
287
  /**
280
288
  * A blank session is the selected Workspace's provisional New Session row;
281
289
  * its canonical title never enters search (blank rows are query-excluded)
@@ -1661,7 +1669,11 @@ window.__ModuleLoader__.load({
1661
1669
  now,
1662
1670
  onOpen: open,
1663
1671
  onRename: onSessionRename,
1664
- onFork: forkSession,
1672
+ onFork: (sessionId) => {
1673
+ Promise.resolve(forkSession(sessionId)).catch((reason) => {
1674
+ showArchivedToast(formatForkError(reason, t));
1675
+ });
1676
+ },
1665
1677
  onArchive: onSessionArchive,
1666
1678
  onUnarchive: onSessionUnarchive,
1667
1679
  onDeleteSession: onSessionDelete,
@@ -1797,7 +1809,11 @@ window.__ModuleLoader__.load({
1797
1809
  now,
1798
1810
  onOpen: open,
1799
1811
  onRename: onSessionRename,
1800
- onFork: forkSession,
1812
+ onFork: (sessionId) => {
1813
+ Promise.resolve(forkSession(sessionId)).catch((reason) => {
1814
+ showArchivedToast(formatForkError(reason, t));
1815
+ });
1816
+ },
1801
1817
  onArchive: onSessionArchive,
1802
1818
  onUnarchive: onSessionUnarchive,
1803
1819
  onDeleteSession: onSessionDelete,
@@ -1918,14 +1934,17 @@ window.__ModuleLoader__.load({
1918
1934
  const archivedSet = (0, react.useMemo)(() => new Set(archivedSessionIds), [archivedSessionIds]);
1919
1935
  const [archivedToast, setArchivedToast] = (0, react.useState)(null);
1920
1936
  const archivedToastSeq = (0, react.useRef)(0);
1937
+ const showArchivedToast = (text) => {
1938
+ archivedToastSeq.current += 1;
1939
+ setArchivedToast({
1940
+ text,
1941
+ seq: archivedToastSeq.current
1942
+ });
1943
+ };
1921
1944
  /** Open a session, unless it is archived (show the archived hint instead). */
1922
1945
  const guardedOpen = (sessionId) => {
1923
1946
  if (archivedSet.has(sessionId)) {
1924
- archivedToastSeq.current += 1;
1925
- setArchivedToast({
1926
- text: t("archived.notOpenable"),
1927
- seq: archivedToastSeq.current
1928
- });
1947
+ showArchivedToast(t("archived.notOpenable"));
1929
1948
  return;
1930
1949
  }
1931
1950
  open(sessionId);
@@ -2091,7 +2110,7 @@ window.__ModuleLoader__.load({
2091
2110
  };
2092
2111
  const onSessionArchive = (sessionId) => {
2093
2112
  archiveSession(sessionId).catch((reason) => {
2094
- console.warn("session archive rejected:", reason);
2113
+ showArchivedToast(formatArchiveError(reason, t));
2095
2114
  });
2096
2115
  };
2097
2116
  const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
@@ -2124,7 +2143,7 @@ window.__ModuleLoader__.load({
2124
2143
  };
2125
2144
  const onSessionUnarchive = (sessionId) => {
2126
2145
  unarchiveSession(sessionId).catch((reason) => {
2127
- console.warn("session unarchive rejected:", reason);
2146
+ showArchivedToast(formatUnarchiveError(reason, t));
2128
2147
  });
2129
2148
  };
2130
2149
  const [deleteSessionTarget, setDeleteSessionTarget] = (0, react.useState)(null);
@@ -2148,17 +2167,8 @@ window.__ModuleLoader__.load({
2148
2167
  setDeletingSession(true);
2149
2168
  setDeleteSessionError(null);
2150
2169
  const rootId = deleteSessionTarget.sessionId;
2151
- const ids = collectSessionAndDescendantIds(rootId, sessionSnapshot.byId);
2152
2170
  (async () => {
2153
- for (const id of ids) {
2154
- try {
2155
- await deleteSession(id);
2156
- } catch (reason) {
2157
- const message = reason instanceof Error ? reason.message : String(reason);
2158
- if (id !== rootId && isUnknownSessionError(reason)) continue;
2159
- throw reason;
2160
- }
2161
- }
2171
+ await deleteSession(rootId);
2162
2172
  })().then(() => {
2163
2173
  setDeletingSession(false);
2164
2174
  setDeleteSessionTarget(null);
@@ -2545,7 +2555,7 @@ window.__ModuleLoader__.load({
2545
2555
  }
2546
2556
  //#endregion
2547
2557
  //#region dsh-archive-manager: settings section
2548
- const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{min-width:132px;height:32px;padding:0 28px 0 11px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px}.dsham_settingsGroup{margin:0 0 20px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
2558
+ const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{position:relative;min-width:168px;flex:none}.dsham_selectTrigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;line-height:20px;text-align:left}.dsham_selectTrigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_selectTrigger[aria-expanded='true']{border-color:var(--dsw-alias-state-success-primary)}.dsham_selectValue{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsham_selectCaret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dsham_selectMenu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;min-width:100%;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dsham_selectOption{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dsham_selectOption:hover,.dsham_selectOption[data-active='true']{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectOption[aria-selected='true']{color:var(--dsw-alias-state-success-primary)}.dsham_settingsGroup{margin:0 0 20px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
2549
2559
  const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
2550
2560
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
2551
2561
  const tag = document.createElement("style");
@@ -2554,6 +2564,138 @@ window.__ModuleLoader__.load({
2554
2564
  tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
2555
2565
  document.head.appendChild(tag);
2556
2566
  }
2567
+ /** 设置页项目筛选:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
2568
+ function ArchiveProjectSelect({ id, value, options, onChange, "aria-label": ariaLabel }) {
2569
+ const [open, setOpen] = (0, react.useState)(false);
2570
+ const selectedIndex = Math.max(0, options.findIndex((option) => option.value === value));
2571
+ const [active, setActive] = (0, react.useState)(selectedIndex);
2572
+ const rootRef = (0, react.useRef)(null);
2573
+ const triggerRef = (0, react.useRef)(null);
2574
+ const listRef = (0, react.useRef)(null);
2575
+ const wasOpen = (0, react.useRef)(false);
2576
+ const selected = options[selectedIndex];
2577
+ (0, react.useEffect)(() => {
2578
+ if (!open) return;
2579
+ setActive(selectedIndex);
2580
+ const onPointerDown = (event) => {
2581
+ const target = event.target;
2582
+ if (target instanceof Node && rootRef.current?.contains(target) === true) return;
2583
+ setOpen(false);
2584
+ };
2585
+ document.addEventListener("pointerdown", onPointerDown);
2586
+ return () => document.removeEventListener("pointerdown", onPointerDown);
2587
+ }, [open, selectedIndex]);
2588
+ (0, react.useEffect)(() => {
2589
+ if (open) {
2590
+ listRef.current?.focus();
2591
+ wasOpen.current = true;
2592
+ return;
2593
+ }
2594
+ if (wasOpen.current) {
2595
+ triggerRef.current?.focus();
2596
+ wasOpen.current = false;
2597
+ }
2598
+ }, [open]);
2599
+ (0, react.useEffect)(() => {
2600
+ if (!open) return;
2601
+ document.getElementById(id + "-opt-" + String(active))?.scrollIntoView({ block: "nearest" });
2602
+ }, [active, open, id]);
2603
+ const choose = (next) => {
2604
+ onChange(next);
2605
+ setOpen(false);
2606
+ };
2607
+ const move = (next) => {
2608
+ if (options.length === 0) return;
2609
+ setActive(Math.min(options.length - 1, Math.max(0, next)));
2610
+ };
2611
+ return (0, react_jsx_runtime.jsxs)("div", {
2612
+ className: "dsham_settingsFilter",
2613
+ ref: rootRef,
2614
+ children: [(0, react_jsx_runtime.jsxs)("button", {
2615
+ id,
2616
+ ref: triggerRef,
2617
+ type: "button",
2618
+ className: "dsham_selectTrigger",
2619
+ "aria-label": ariaLabel,
2620
+ "aria-haspopup": "listbox",
2621
+ "aria-expanded": open,
2622
+ "aria-controls": id + "-list",
2623
+ onClick: () => setOpen((current) => !current),
2624
+ onKeyDown: (event) => {
2625
+ if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") {
2626
+ event.preventDefault();
2627
+ setOpen(true);
2628
+ }
2629
+ },
2630
+ children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_selectValue", children: selected === void 0 ? "" : selected.label }), (0, react_jsx_runtime.jsx)("svg", {
2631
+ className: "dsham_selectCaret",
2632
+ viewBox: "0 0 12 12",
2633
+ "aria-hidden": true,
2634
+ focusable: false,
2635
+ children: (0, react_jsx_runtime.jsx)("path", {
2636
+ d: "M2.5 4.5L6 8l3.5-3.5",
2637
+ fill: "none",
2638
+ stroke: "currentColor",
2639
+ strokeWidth: "1.5",
2640
+ strokeLinecap: "round",
2641
+ strokeLinejoin: "round"
2642
+ })
2643
+ })]
2644
+ }), open ? (0, react_jsx_runtime.jsx)("div", {
2645
+ id: id + "-list",
2646
+ ref: listRef,
2647
+ className: "dsham_selectMenu",
2648
+ role: "listbox",
2649
+ tabIndex: 0,
2650
+ "aria-activedescendant": id + "-opt-" + String(active),
2651
+ onKeyDown: (event) => {
2652
+ if (event.key === "ArrowDown") {
2653
+ event.preventDefault();
2654
+ move(active + 1);
2655
+ return;
2656
+ }
2657
+ if (event.key === "ArrowUp") {
2658
+ event.preventDefault();
2659
+ move(active - 1);
2660
+ return;
2661
+ }
2662
+ if (event.key === "Home") {
2663
+ event.preventDefault();
2664
+ move(0);
2665
+ return;
2666
+ }
2667
+ if (event.key === "End") {
2668
+ event.preventDefault();
2669
+ move(options.length - 1);
2670
+ return;
2671
+ }
2672
+ if (event.key === "Enter" || event.key === " ") {
2673
+ event.preventDefault();
2674
+ const option = options[active];
2675
+ if (option !== void 0) choose(option.value);
2676
+ return;
2677
+ }
2678
+ if (event.key === "Escape" || event.key === "Tab") {
2679
+ event.preventDefault();
2680
+ event.stopPropagation();
2681
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
2682
+ setOpen(false);
2683
+ }
2684
+ },
2685
+ children: options.map((option, index) => (0, react_jsx_runtime.jsx)("button", {
2686
+ id: id + "-opt-" + String(index),
2687
+ type: "button",
2688
+ role: "option",
2689
+ className: "dsham_selectOption",
2690
+ "aria-selected": option.value === value,
2691
+ "data-active": index === active,
2692
+ onMouseEnter: () => setActive(index),
2693
+ onClick: () => choose(option.value),
2694
+ children: option.label
2695
+ }, option.value === "all" ? "all" : option.value))
2696
+ }) : null]
2697
+ });
2698
+ }
2557
2699
  /** 管理设置页中的归档会话,数据直接订阅 DSH 的会话与工作区投影。 */
2558
2700
  function ArchivedSessionsSection({ sessionStore, workspaceStore, unarchiveSession, deleteSession, t }) {
2559
2701
  const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
@@ -2564,7 +2706,7 @@ window.__ModuleLoader__.load({
2564
2706
  const [query, setQuery] = (0, react.useState)("");
2565
2707
  const [project, setProject] = (0, react.useState)("all");
2566
2708
  const groups = (0, react.useMemo)(() => {
2567
- // 列表仍隐藏 subagent;“删除全部”另用 collectArchivedDeleteAllIds 收父会话和子会话。
2709
+ // 列表仍隐藏 subagent;删除全部只提交列表里可见的已归档会话,子代理由服务端级联。
2568
2710
  const archived = new Set(workspaceState.archivedSessionIds);
2569
2711
  const byWorkspace = workspaceState.items.map((workspace) => ({
2570
2712
  title: workspace.title,
@@ -2589,8 +2731,7 @@ window.__ModuleLoader__.load({
2589
2731
  sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
2590
2732
  })).filter((group) => group.sessions.length > 0);
2591
2733
  }, [groups, project, query, t]);
2592
- const archivedSessionIds = (0, react.useMemo)(() => groups.flatMap((group) => group.sessions.map((session) => session.id)), [groups]);
2593
- const deleteAllSessionIds = (0, react.useMemo)(() => collectArchivedDeleteAllIds(workspaceState.archivedSessionIds, sessions.byId), [workspaceState.archivedSessionIds, sessions]);
2734
+ const deleteAllSessionIds = (0, react.useMemo)(() => groups.flatMap((group) => group.sessions.map((session) => session.id)), [groups]);
2594
2735
  const onUnarchive = (sessionId) => {
2595
2736
  setError(null);
2596
2737
  unarchiveSession(sessionId).catch((reason) => {
@@ -2600,17 +2741,28 @@ window.__ModuleLoader__.load({
2600
2741
  const closeDelete = () => {
2601
2742
  if (!busy) setDeleteTarget(null);
2602
2743
  };
2744
+ (0, react.useEffect)(() => {
2745
+ if (deleteTarget === null) return;
2746
+ const onKeyDown = (event) => {
2747
+ if (event.key !== "Escape") return;
2748
+ event.preventDefault();
2749
+ event.stopPropagation();
2750
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
2751
+ if (!busy) setDeleteTarget(null);
2752
+ };
2753
+ window.addEventListener("keydown", onKeyDown, true);
2754
+ return () => window.removeEventListener("keydown", onKeyDown, true);
2755
+ }, [deleteTarget, busy]);
2603
2756
  const confirmDelete = async () => {
2604
2757
  if (busy || deleteTarget === null) return;
2605
2758
  setBusy(true);
2606
2759
  setError(null);
2607
2760
  try {
2608
- for (const id of deleteTarget.all ? deleteAllSessionIds : collectSessionAndDescendantIds(deleteTarget.id, sessions.byId)) {
2761
+ for (const id of deleteTarget.all ? deleteAllSessionIds : [deleteTarget.id]) {
2609
2762
  try {
2610
2763
  await deleteSession(id);
2611
2764
  } catch (reason) {
2612
- const message = reason instanceof Error ? reason.message : String(reason);
2613
- if ((deleteTarget.all || id !== deleteTarget.id) && isUnknownSessionError(reason)) continue;
2765
+ if (deleteTarget.all && isUnknownSessionError(reason)) continue;
2614
2766
  throw reason;
2615
2767
  }
2616
2768
  }
@@ -2624,7 +2776,7 @@ window.__ModuleLoader__.load({
2624
2776
  return (0, react_jsx_runtime.jsxs)("section", {
2625
2777
  className: "dsham_settings",
2626
2778
  "aria-label": t("archives.title"),
2627
- children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)("select", { className: "dsham_settingsFilter", value: project, onChange: (event) => setProject(event.target.value), "aria-label": t("archives.projectFilter"), children: [(0, react_jsx_runtime.jsx)("option", { value: "all", children: t("archives.allProjects") }), groups.map((group) => (0, react_jsx_runtime.jsx)("option", { value: group.title, children: group.title }, group.title))] })] }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => (0, react_jsx_runtime.jsxs)("section", {
2779
+ children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })] }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...groups.map((group) => ({ value: group.title, label: group.title }))], onChange: setProject, "aria-label": t("archives.projectFilter") })] }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => (0, react_jsx_runtime.jsxs)("section", {
2628
2780
  className: "dsham_settingsGroup",
2629
2781
  children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
2630
2782
  className: "dsham_settingsList",
@@ -2696,12 +2848,15 @@ window.__ModuleLoader__.load({
2696
2848
  "archives.timestamp": "{date},{time}",
2697
2849
  "archives.deleteAll": "全部删除",
2698
2850
  "archives.deleteAllTitle": "删除全部已归档聊天",
2699
- "archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理和记录,此操作不可恢复。",
2851
+ "archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理(含正在运行的)和记录,此操作不可恢复。",
2700
2852
  "archives.deleteAllPending": "正在删除已归档聊天…",
2701
2853
  "archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
2702
- "archives.unarchiveFailed": "取消归档失败:{detail}",
2854
+ "archives.unarchiveFailed": "取消归档失败:{detail}",
2855
+ "archives.archiveUnknown": "会话已不存在,无法归档。",
2856
+ "archives.archiveFailed": "归档失败:{detail}",
2857
+ "archives.forkFailed": "分叉会话失败:{detail}",
2703
2858
  "deleteSession.title": "删除会话",
2704
- "deleteSession.desc": "将永久删除会话“{name}”及其子代理和全部记录(对话内容、统计、缓存),此操作不可恢复。",
2859
+ "deleteSession.desc": "将永久删除会话“{name}”及其子代理(含正在运行的)和全部记录(对话内容、统计、缓存),此操作不可恢复。",
2705
2860
  "deleteSession.pending": "正在删除会话…",
2706
2861
  "deleteSession.unknown": "会话已不存在或已被删除。",
2707
2862
  "deleteSession.failed": "删除会话失败:{detail}",
@@ -2785,12 +2940,15 @@ window.__ModuleLoader__.load({
2785
2940
  "archives.timestamp": "{date}, {time}",
2786
2941
  "archives.deleteAll": "Delete all",
2787
2942
  "archives.deleteAllTitle": "Delete all archived chats",
2788
- "archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents, and their records. This cannot be undone.",
2943
+ "archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents (including any that are still running), and their records. This cannot be undone.",
2789
2944
  "archives.deleteAllPending": "Deleting archived chats…",
2790
2945
  "archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
2791
- "archives.unarchiveFailed": "Could not unarchive the session: {detail}",
2946
+ "archives.unarchiveFailed": "Could not unarchive the session: {detail}",
2947
+ "archives.archiveUnknown": "This session no longer exists, so it cannot be archived.",
2948
+ "archives.archiveFailed": "Could not archive the session: {detail}",
2949
+ "archives.forkFailed": "Could not fork the session: {detail}",
2792
2950
  "deleteSession.title": "Delete session",
2793
- "deleteSession.desc": "This permanently deletes session “{name}”, its child agents, and all of its records (conversation, stats, cache). This cannot be undone.",
2951
+ "deleteSession.desc": "This permanently deletes session “{name}”, its child agents (including any that are still running), and all of its records (conversation, stats, cache). This cannot be undone.",
2794
2952
  "deleteSession.pending": "Deleting session…",
2795
2953
  "deleteSession.unknown": "This session no longer exists or was already deleted.",
2796
2954
  "deleteSession.failed": "Could not delete the session: {detail}",
@@ -2919,14 +3077,12 @@ window.__ModuleLoader__.load({
2919
3077
  const result = await session.rename(title);
2920
3078
  if (!result.ok) throw new Error(result.error.message);
2921
3079
  },
2922
- forkSession: (sessionId) => {
2923
- ctx.sessions.fork({
2924
- sessionId,
2925
- increaseTitle: true
2926
- }).then((childId) => {
2927
- ctx.sessions.open(childId);
2928
- }).catch(() => {});
2929
- },
3080
+ forkSession: (sessionId) => ctx.sessions.fork({
3081
+ sessionId,
3082
+ increaseTitle: true
3083
+ }).then((childId) => {
3084
+ ctx.sessions.open(childId);
3085
+ }),
2930
3086
  renameWorkspace: async (workspaceId, title) => {
2931
3087
  await ctx.workspaces.rename(workspaceId, title);
2932
3088
  },
package/lib/projcache.js CHANGED
@@ -23,6 +23,8 @@ import { SessionProjectionCache } from "@deepseek-ai/dsh-session-projection-cach
23
23
  var ArchiveProjectionCache = class extends SessionProjectionCache {
24
24
  /** 已永久删除的会话不会再允许投影缓存写入。 */
25
25
  deletedSessionIds = /* @__PURE__ */ new Set();
26
+ deletedSessionOrder = [];
27
+ deletedSessionTombstoneLimit = 4096;
26
28
  /** Tail of in-flight checkpoint writes (settled promises only). */
27
29
  writeTail = Promise.resolve();
28
30
  constructor(ctx, config) {
@@ -57,7 +59,14 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
57
59
  * @returns resolution after the row is gone.
58
60
  */
59
61
  async delete(id) {
60
- this.deletedSessionIds.add(id);
62
+ if (!this.deletedSessionIds.has(id)) {
63
+ this.deletedSessionIds.add(id);
64
+ this.deletedSessionOrder.push(id);
65
+ }
66
+ while (this.deletedSessionOrder.length > this.deletedSessionTombstoneLimit) {
67
+ const oldest = this.deletedSessionOrder.shift();
68
+ if (oldest !== void 0 && oldest !== id) this.deletedSessionIds.delete(oldest);
69
+ }
61
70
  await this.requireTable().delete(id);
62
71
  }
63
72
  };
package/lib/workspace.js CHANGED
@@ -330,8 +330,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
330
330
  }
331
331
  for (const childId of descendants) {
332
332
  try {
333
+ if (!await this.sessionKnown(childId)) continue;
333
334
  await this.deleteSessionCore(childId);
334
335
  } catch (error) {
336
+ if (error instanceof ArchiveUnknownSessionError) continue;
335
337
  this.ctx.logger.warn(`archive-manager: cascade delete of subagent session "${childId}" (child of "${sessionId}") failed: ${String(error)}`);
336
338
  }
337
339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",