dsh-native-session-delete 1.0.7 → 1.1.0
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/AGENTS.md +17 -12
- package/README.en.md +33 -15
- package/README.md +32 -15
- package/lib/client.js +187 -3
- package/package.json +10 -2
- package/scripts/build-client.mjs +189 -4
- package/scripts/smoke-ui.mjs +7 -2
- package/src/host/archive-manager.mjs +251 -0
- package/src/index.js +47 -7
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ or remove `dsh-native-session-delete` in a selected DeepSeek Harness profile.
|
|
|
6
6
|
## Safety and responsibility boundary
|
|
7
7
|
|
|
8
8
|
- Confirm the target DSH installation and profile. Use `web` only when it is the user's target.
|
|
9
|
-
- Use the fixed v1.0
|
|
9
|
+
- Use the fixed v1.1.0 package below; never substitute a moving branch or an unreviewed source.
|
|
10
10
|
- Do not print session contents, full profile files, transcript paths, credentials, or other private data.
|
|
11
11
|
- Do not start, stop, or restart DSH without explicit permission.
|
|
12
12
|
- Preserve all sessions, unrelated plugins, and user-owned profile changes.
|
|
@@ -22,16 +22,17 @@ requirements. A cancelled confirmation is the safe default and must not send a d
|
|
|
22
22
|
|
|
23
23
|
## Fixed package and standard bundle
|
|
24
24
|
|
|
25
|
-
The v1.0
|
|
25
|
+
The v1.1.0 package is a standard DSH bundle with a `dsh.bundle` profile patch. Its exact package spec is:
|
|
26
26
|
|
|
27
27
|
```text
|
|
28
|
-
dsh-native-session-delete@1.0
|
|
28
|
+
dsh-native-session-delete@1.1.0
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The bundle disables the official workspace row while installed and inserts a uniquely identified native
|
|
32
32
|
workspace row. Removing `dsh-native-session-delete` removes that layer, allowing DSH to restore the official
|
|
33
33
|
workspace row. Do not install the tarball under `@deepseek-ai/dsh-client-ui-workspace`; that old aliasing
|
|
34
|
-
approach is not the v1.0
|
|
34
|
+
approach is not the v1.1.0 contract. The product is shown to users as **DSH Native Session Manager** while
|
|
35
|
+
retaining the existing npm package identity for safe upgrades.
|
|
35
36
|
|
|
36
37
|
## Detect the target DSH
|
|
37
38
|
|
|
@@ -56,13 +57,13 @@ record the selected profile's relevant metadata before invoking it, without prin
|
|
|
56
57
|
With an existing `dsh` command, run exactly:
|
|
57
58
|
|
|
58
59
|
```sh
|
|
59
|
-
dsh plugin --profile web add dsh-native-session-delete@1.0
|
|
60
|
+
dsh plugin --profile web add dsh-native-session-delete@1.1.0
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
With DSH-Portable, run from its root:
|
|
63
64
|
|
|
64
65
|
```powershell
|
|
65
|
-
.\dsh.exe plugin --profile web add dsh-native-session-delete@1.0
|
|
66
|
+
.\dsh.exe plugin --profile web add dsh-native-session-delete@1.1.0
|
|
66
67
|
```
|
|
67
68
|
|
|
68
69
|
Use the same `add` command to update or repair. On Windows, `install.ps1` from the same fixed Release may
|
|
@@ -86,17 +87,21 @@ dsh plugin --profile web list dsh-native-session-delete --depth 0
|
|
|
86
87
|
Use `.\dsh.exe` in place of `dsh` for DSH-Portable. Static acceptance requires:
|
|
87
88
|
|
|
88
89
|
1. The `dsh-native-session-delete` bundle appears exactly once in the requested profile.
|
|
89
|
-
2. Its direct package spec is the fixed `dsh-native-session-delete@1.0
|
|
90
|
+
2. Its direct package spec is the fixed `dsh-native-session-delete@1.1.0` npm version above.
|
|
90
91
|
3. The profile contains the bundle patch and no duplicate official workspace row from this plugin.
|
|
91
92
|
4. No unrelated dependency, profile patch, or session data was changed by the operation.
|
|
92
93
|
|
|
93
94
|
With permission to restart DSH, verify the live UI in dark mode:
|
|
94
95
|
|
|
95
|
-
1. The
|
|
96
|
-
2.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
1. The sidebar header contains an archive action that opens **Archived sessions**.
|
|
97
|
+
2. The archive manager lists archived sessions, filters by name or workspace, and can search archived
|
|
98
|
+
user/assistant conversation content without exposing another session.
|
|
99
|
+
3. Restoring a disposable archived session returns it to its original workspace position without reloading the page.
|
|
100
|
+
4. The selected session's native actions menu contains **Archive session** and the red **Delete session** action.
|
|
101
|
+
5. Opening Delete shows the target session name and a second confirmation.
|
|
102
|
+
6. Selecting **Cancel** closes the dialog, sends no delete request, and leaves the session visible.
|
|
103
|
+
7. A destructive check is allowed only with a disposable test session explicitly selected by the user.
|
|
104
|
+
8. After confirming that disposable session, verify it disappears in place without reloading the whole DSH page.
|
|
100
105
|
|
|
101
106
|
For source-checkout UI acceptance, the non-destructive smoke test is:
|
|
102
107
|
|
package/README.en.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# DSH Native Session
|
|
3
|
+
# DSH Native Session Manager
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Search, restore, and safely clean up sessions from the native DeepSeek Harness sidebar.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Archive manager · Conversation search · One-click restore · Safe permanent deletion
|
|
8
8
|
|
|
9
9
|
[](https://github.com/WSL043/dsh-native-session-delete/releases/latest)
|
|
10
10
|
[](https://github.com/WSL043/dsh-native-session-delete/actions/workflows/ci.yml)
|
|
@@ -17,12 +17,17 @@ Native dark menu · Second confirmation · Permanent delete · In-place list upd
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
20
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/hero.en.png" alt="Native DeepSeek Harness session management and permanent deletion">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
-
|
|
|
23
|
+
| Recover archives | Search conversations | Delete safely |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
|
|
|
25
|
+
| Open the archive manager from the sidebar and restore hidden sessions | Search archived names, workspaces, and user/assistant conversation content | Keep native second confirmation; running work is stopped safely before local records are removed |
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/archive-manager.en.png" width="382" alt="Native DSH archive manager with search, restore, and permanent deletion">
|
|
29
|
+
<br><sub>Native interface on DeepSeek Harness 0.1.1-rc.2</sub>
|
|
30
|
+
</p>
|
|
26
31
|
|
|
27
32
|
## Install
|
|
28
33
|
|
|
@@ -31,7 +36,7 @@ Native dark menu · Second confirmation · Permanent delete · In-place list upd
|
|
|
31
36
|
Open PowerShell and paste one line:
|
|
32
37
|
|
|
33
38
|
```powershell
|
|
34
|
-
irm 'https://github.com/WSL043/dsh-native-session-delete/releases/download/v1.0
|
|
39
|
+
irm 'https://github.com/WSL043/dsh-native-session-delete/releases/download/v1.1.0/install.ps1' | iex
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
The helper checks the current directory, PATH, `DSH_PORTABLE_ROOT`, Downloads/Desktop/Documents, and up to
|
|
@@ -46,7 +51,7 @@ If it still finds nothing, enter the actual DSH-Portable folder and rerun the sa
|
|
|
46
51
|
### Official CLI (macOS, Linux, or direct review)
|
|
47
52
|
|
|
48
53
|
```sh
|
|
49
|
-
dsh plugin --profile web add dsh-native-session-delete@1.0
|
|
54
|
+
dsh plugin --profile web add dsh-native-session-delete@1.1.0
|
|
50
55
|
```
|
|
51
56
|
|
|
52
57
|
The helper and direct command use the same standard bundle mechanism. The helper is only a Windows entry
|
|
@@ -57,19 +62,31 @@ bundle configuration becomes active.
|
|
|
57
62
|
|
|
58
63
|
### Agent installation
|
|
59
64
|
|
|
60
|
-
Use the fixed-version [AGENTS.md](https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
65
|
+
Use the fixed-version [AGENTS.md](https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/AGENTS.md).
|
|
61
66
|
It defines installation, update, acceptance, uninstall, and safety boundaries. Do not use the `main`
|
|
62
67
|
branch document as an installation contract.
|
|
63
68
|
|
|
64
69
|
## Use
|
|
65
70
|
|
|
71
|
+
### Manage archives
|
|
72
|
+
|
|
73
|
+
1. Select the archive icon in the sidebar header to open **Archived sessions**.
|
|
74
|
+
2. Browse every archive or search by session name, workspace, and user/assistant conversation content.
|
|
75
|
+
3. Select **Restore** to return a session to its original workspace position. To remove it completely,
|
|
76
|
+
start the permanent-delete confirmation from the same list.
|
|
77
|
+
|
|
78
|
+
Archive and restore only change DSH's hidden state; they do not delete conversation history. Content search
|
|
79
|
+
is limited to current user and assistant messages inside archived sessions.
|
|
80
|
+
|
|
81
|
+
### Delete permanently
|
|
82
|
+
|
|
66
83
|
1. Open the native actions menu beside the target session in the sidebar.
|
|
67
|
-
2. Choose the red **Delete session
|
|
84
|
+
2. Choose the red **Delete session** action.
|
|
68
85
|
3. Check the session name and select **Delete permanently** in the confirmation dialog, or select
|
|
69
86
|
**Cancel** to leave it unchanged.
|
|
70
87
|
|
|
71
88
|
<p align="center">
|
|
72
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
89
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/confirm-delete.en.png" width="560" alt="English dark-mode permanent deletion confirmation dialog">
|
|
73
90
|
<br><sub>Permanent deletion cannot be undone; the dialog identifies the target session.</sub>
|
|
74
91
|
</p>
|
|
75
92
|
|
|
@@ -104,15 +121,16 @@ without warranty.
|
|
|
104
121
|
Supports DeepSeek Harness: `0.1.0-rc.6`, `0.1.0-rc.7`, `0.1.0-rc.8`, `0.1.1-rc.1`, `0.1.1-rc.2`.
|
|
105
122
|
<!-- /dsh-compatibility -->
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
Archive browsing, restore, and content search use DSH's workspace registry and session-query capabilities.
|
|
125
|
+
Permanent deletion supports DSH's default per-session JSONL storage. Installing replaces the native workspace
|
|
126
|
+
list with the session-management version; uninstalling restores DSH's original list.
|
|
109
127
|
|
|
110
128
|
## Update and uninstall
|
|
111
129
|
|
|
112
|
-
Update by rerunning the quick installer or installing the new npm version. For v1.0
|
|
130
|
+
Update by rerunning the quick installer or installing the new npm version. For v1.1.0:
|
|
113
131
|
|
|
114
132
|
```sh
|
|
115
|
-
dsh plugin --profile web add dsh-native-session-delete@1.0
|
|
133
|
+
dsh plugin --profile web add dsh-native-session-delete@1.1.0
|
|
116
134
|
```
|
|
117
135
|
|
|
118
136
|
Uninstall removes only this plugin's bundle layer and never deletes sessions:
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# DSH Native Session
|
|
3
|
+
# DSH Native Session Manager
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**在 DeepSeek Harness 原生侧边栏中搜索、恢复和安全清理会话。**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
归档管理 · 聊天记录搜索 · 一键恢复 · 安全永久删除
|
|
8
8
|
|
|
9
9
|
[](https://github.com/WSL043/dsh-native-session-delete/releases/latest)
|
|
10
10
|
[](https://github.com/WSL043/dsh-native-session-delete/actions/workflows/ci.yml)
|
|
@@ -17,12 +17,17 @@
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
20
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/hero.png" alt="DeepSeek Harness 原生会话管理与永久删除功能">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
-
|
|
|
23
|
+
| 归档可找回 | 聊天可搜索 | 删除更稳妥 |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
|
|
|
25
|
+
| 从侧边栏打开归档管理器,查看并恢复隐藏的会话 | 按会话名、工作区或用户与助手的聊天内容搜索归档 | 原生菜单保留二次确认;运行中的任务先安全停止,再删除本机会话记录 |
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/archive-manager.png" width="382" alt="DSH 原生归档会话管理器,包含搜索、恢复和永久删除">
|
|
29
|
+
<br><sub>DeepSeek Harness 0.1.1-rc.2 中的原生界面</sub>
|
|
30
|
+
</p>
|
|
26
31
|
|
|
27
32
|
## 安装
|
|
28
33
|
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
打开 PowerShell,复制这一行:
|
|
32
37
|
|
|
33
38
|
```powershell
|
|
34
|
-
irm 'https://github.com/WSL043/dsh-native-session-delete/releases/download/v1.0
|
|
39
|
+
irm 'https://github.com/WSL043/dsh-native-session-delete/releases/download/v1.1.0/install.ps1' | iex
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
助手依次检查当前目录、PATH、`DSH_PORTABLE_ROOT`、下载/桌面/文档目录,以及这些目录和
|
|
@@ -44,7 +49,7 @@ irm 'https://github.com/WSL043/dsh-native-session-delete/releases/download/v1.0.
|
|
|
44
49
|
### 官方 CLI(macOS、Linux 或希望直接审阅命令)
|
|
45
50
|
|
|
46
51
|
```sh
|
|
47
|
-
dsh plugin --profile web add dsh-native-session-delete@1.0
|
|
52
|
+
dsh plugin --profile web add dsh-native-session-delete@1.1.0
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
助手和这条命令使用的是同一个标准 bundle 安装机制;助手只是 Windows 入口,不接管安装事务。
|
|
@@ -53,17 +58,28 @@ dsh plugin --profile web add dsh-native-session-delete@1.0.7
|
|
|
53
58
|
|
|
54
59
|
### 交给 Agent
|
|
55
60
|
|
|
56
|
-
请使用固定版本的 [AGENTS.md](https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
61
|
+
请使用固定版本的 [AGENTS.md](https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/AGENTS.md),
|
|
57
62
|
其中写明了安装、更新、验收、卸载和安全边界。不要把 `main` 分支文档当作安装依据。
|
|
58
63
|
|
|
59
64
|
## 使用
|
|
60
65
|
|
|
66
|
+
### 管理归档
|
|
67
|
+
|
|
68
|
+
1. 点击侧边栏标题区域的归档图标,打开 **归档会话**。
|
|
69
|
+
2. 直接浏览全部归档,或按会话名、工作区和用户/助手聊天内容搜索。
|
|
70
|
+
3. 点击 **恢复** 让会话回到原来的工作区位置;需要彻底清理时,可从同一列表进入永久删除确认。
|
|
71
|
+
|
|
72
|
+
归档和恢复只改变 DSH 的隐藏状态,不删除聊天记录。搜索范围仅限已归档会话中的当前用户与助手消息,
|
|
73
|
+
不会把其他会话或插件数据混入结果。
|
|
74
|
+
|
|
75
|
+
### 永久删除
|
|
76
|
+
|
|
61
77
|
1. 打开侧边栏中目标会话右侧的原生操作菜单。
|
|
62
|
-
2. 选择红色的
|
|
63
|
-
3.
|
|
78
|
+
2. 选择红色的 **删除会话**。
|
|
79
|
+
3. 在确认弹窗中核对会话名称并再次确认 **永久删除**;也可以随时点击 **取消**。
|
|
64
80
|
|
|
65
81
|
<p align="center">
|
|
66
|
-
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.0
|
|
82
|
+
<img src="https://raw.githubusercontent.com/WSL043/dsh-native-session-delete/v1.1.0/docs/assets/confirm-delete.png" width="560" alt="中文深色模式永久删除二次确认弹窗">
|
|
67
83
|
<br><sub>永久删除无法撤销,确认弹窗会明确显示目标会话</sub>
|
|
68
84
|
</p>
|
|
69
85
|
|
|
@@ -94,14 +110,15 @@ dsh plugin --profile web add dsh-native-session-delete@1.0.7
|
|
|
94
110
|
支持 DeepSeek Harness:`0.1.0-rc.6`、`0.1.0-rc.7`、`0.1.0-rc.8`、`0.1.1-rc.1`、`0.1.1-rc.2`。
|
|
95
111
|
<!-- /dsh-compatibility -->
|
|
96
112
|
|
|
97
|
-
|
|
113
|
+
归档浏览、恢复和内容搜索使用 DSH 的工作区注册表与会话查询能力;永久删除适用于 DSH 默认的逐会话
|
|
114
|
+
JSONL 存储。安装后替换为带会话管理功能的原生工作区列表;卸载后恢复 DSH 原有列表。
|
|
98
115
|
|
|
99
116
|
## 更新与卸载
|
|
100
117
|
|
|
101
|
-
更新可重新运行快速安装助手,或安装 npm 上的新版本。v1.0
|
|
118
|
+
更新可重新运行快速安装助手,或安装 npm 上的新版本。v1.1.0 的直接命令是:
|
|
102
119
|
|
|
103
120
|
```sh
|
|
104
|
-
dsh plugin --profile web add dsh-native-session-delete@1.0
|
|
121
|
+
dsh plugin --profile web add dsh-native-session-delete@1.1.0
|
|
105
122
|
```
|
|
106
123
|
|
|
107
124
|
卸载只移除这个插件的 bundle 层,不删除任何会话:
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Modified from @deepseek-ai/dsh-client-ui-workspace 0.1.1-rc.2 by DSH Native Session
|
|
1
|
+
// Modified from @deepseek-ai/dsh-client-ui-workspace 0.1.1-rc.2 by DSH Native Session Manager. See THIRD_PARTY_NOTICES.md.
|
|
2
2
|
window.__ModuleLoader__.load({
|
|
3
3
|
id: "dsh-native-session-delete",
|
|
4
4
|
factory: (require) => {
|
|
@@ -1656,7 +1656,7 @@ window.__ModuleLoader__.load({
|
|
|
1656
1656
|
* @param props - composed slot props (shell owner share + store + injected actions).
|
|
1657
1657
|
* @returns the region element tree.
|
|
1658
1658
|
*/
|
|
1659
|
-
function WorkspaceBrowser({ wide, expandSidebar, useSessions, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostDescription, renderSlot, t }) {
|
|
1659
|
+
function WorkspaceBrowser({ wide, expandSidebar, useSessions, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, deleteSession, restoreSession, searchArchivedSessions, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostDescription, renderSlot, t }) {
|
|
1660
1660
|
const home = useHostDescription((description) => description?.home);
|
|
1661
1661
|
const workspaces = useWorkspaces((state) => state.items);
|
|
1662
1662
|
const workspacePhase = useWorkspaces((state) => state.phase);
|
|
@@ -1858,6 +1858,60 @@ window.__ModuleLoader__.load({
|
|
|
1858
1858
|
console.warn("session archive rejected:", reason);
|
|
1859
1859
|
});
|
|
1860
1860
|
};
|
|
1861
|
+
const archiveSessionList = useSessions((state) => state);
|
|
1862
|
+
const [archiveManagerOpen, setArchiveManagerOpen] = (0, react.useState)(false);
|
|
1863
|
+
const [archiveQuery, setArchiveQuery] = (0, react.useState)("");
|
|
1864
|
+
const [archiveSearch, setArchiveSearch] = (0, react.useState)({ query: "", status: "idle", items: [], hasMore: false });
|
|
1865
|
+
const [archiveBusyId, setArchiveBusyId] = (0, react.useState)(null);
|
|
1866
|
+
const [archiveError, setArchiveError] = (0, react.useState)(null);
|
|
1867
|
+
const normalizedArchiveQuery = archiveQuery.trim();
|
|
1868
|
+
(0, react.useEffect)(() => {
|
|
1869
|
+
if (!archiveManagerOpen || normalizedArchiveQuery === "") {
|
|
1870
|
+
setArchiveSearch({ query: "", status: "idle", items: [], hasMore: false });
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const controller = new AbortController();
|
|
1874
|
+
setArchiveSearch({ query: normalizedArchiveQuery, status: "loading", items: [], hasMore: false });
|
|
1875
|
+
const timer = window.setTimeout(() => {
|
|
1876
|
+
searchArchivedSessions(normalizedArchiveQuery, controller.signal).then((result) => {
|
|
1877
|
+
if (!controller.signal.aborted) setArchiveSearch({ query: normalizedArchiveQuery, status: "ready", items: result.items, hasMore: result.hasMore });
|
|
1878
|
+
}).catch(() => {
|
|
1879
|
+
if (!controller.signal.aborted) setArchiveSearch({ query: normalizedArchiveQuery, status: "error", items: [], hasMore: false });
|
|
1880
|
+
});
|
|
1881
|
+
}, 250);
|
|
1882
|
+
return () => { window.clearTimeout(timer); controller.abort(); };
|
|
1883
|
+
}, [archiveManagerOpen, normalizedArchiveQuery, searchArchivedSessions]);
|
|
1884
|
+
const archiveWorkspaceBySession = (0, react.useMemo)(() => {
|
|
1885
|
+
const result = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!result.has(sessionId)) result.set(sessionId, workspace.title);
|
|
1887
|
+
return result;
|
|
1888
|
+
}, [workspaces]);
|
|
1889
|
+
const archiveSnippets = (0, react.useMemo)(() => new Map(archiveSearch.items.map((item) => [item.sessionId, item.snippet])), [archiveSearch.items]);
|
|
1890
|
+
const archiveRows = (0, react.useMemo)(() => {
|
|
1891
|
+
const query = normalizedArchiveQuery.toLowerCase();
|
|
1892
|
+
const remoteIds = new Set(archiveSearch.items.map((item) => item.sessionId));
|
|
1893
|
+
const rows = archivedSessionIds.map((sessionId) => {
|
|
1894
|
+
const summary = archiveSessionList.byId[sessionId];
|
|
1895
|
+
return {
|
|
1896
|
+
id: sessionId,
|
|
1897
|
+
title: summary === void 0 ? sessionId : sessionTitle(summary),
|
|
1898
|
+
workspace: archiveWorkspaceBySession.get(sessionId) ?? t("group.ungrouped"),
|
|
1899
|
+
updatedAt: summary?.updatedAt ?? 0
|
|
1900
|
+
};
|
|
1901
|
+
});
|
|
1902
|
+
rows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1903
|
+
if (query === "") return rows;
|
|
1904
|
+
return rows.filter((row) => row.title.toLowerCase().includes(query) || row.workspace.toLowerCase().includes(query) || remoteIds.has(row.id));
|
|
1905
|
+
}, [archiveSessionList, archivedSessionIds, archiveSearch.items, archiveWorkspaceBySession, normalizedArchiveQuery, t]);
|
|
1906
|
+
const onArchiveRestore = (sessionId) => {
|
|
1907
|
+
if (archiveBusyId !== null) return;
|
|
1908
|
+
setArchiveBusyId(sessionId);
|
|
1909
|
+
setArchiveError(null);
|
|
1910
|
+
restoreSession(sessionId).then(() => setArchiveBusyId(null)).catch((reason) => {
|
|
1911
|
+
setArchiveBusyId(null);
|
|
1912
|
+
setArchiveError(reason instanceof Error ? reason.message : String(reason));
|
|
1913
|
+
});
|
|
1914
|
+
};
|
|
1861
1915
|
const [sessionDeleteTarget, setSessionDeleteTarget] = (0, react.useState)(null);
|
|
1862
1916
|
const [sessionDeleting, setSessionDeleting] = (0, react.useState)(false);
|
|
1863
1917
|
const [sessionDeleteError, setSessionDeleteError] = (0, react.useState)(null);
|
|
@@ -1982,7 +2036,19 @@ window.__ModuleLoader__.load({
|
|
|
1982
2036
|
}),
|
|
1983
2037
|
(0, react_jsx_runtime.jsxs)("div", {
|
|
1984
2038
|
className: clsx(WorkspaceBrowser_module_css_default.headerActions, wide && searchExpanded && WorkspaceBrowser_module_css_default.headerActionsHidden),
|
|
1985
|
-
children: [
|
|
2039
|
+
children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
2040
|
+
label: t("archive.manager.title"),
|
|
2041
|
+
side: "bottom",
|
|
2042
|
+
delayMs: 500,
|
|
2043
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
2044
|
+
id: "archived-sessions",
|
|
2045
|
+
type: "button",
|
|
2046
|
+
className: WorkspaceBrowser_module_css_default.iconButton,
|
|
2047
|
+
"aria-label": t("archive.manager.title"),
|
|
2048
|
+
onClick: () => { setArchiveError(null); setArchiveManagerOpen(true); },
|
|
2049
|
+
children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: wide ? 16 : 18 })
|
|
2050
|
+
})
|
|
2051
|
+
}), wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
|
|
1986
2052
|
groupBy,
|
|
1987
2053
|
orderBy,
|
|
1988
2054
|
onGroupPick: (mode) => {
|
|
@@ -2210,6 +2276,77 @@ window.__ModuleLoader__.load({
|
|
|
2210
2276
|
children: sessionRenameError
|
|
2211
2277
|
})]
|
|
2212
2278
|
}),
|
|
2279
|
+
(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2280
|
+
open: archiveManagerOpen,
|
|
2281
|
+
onClose: () => { if (archiveBusyId === null) setArchiveManagerOpen(false); },
|
|
2282
|
+
closeLabel: t("close"),
|
|
2283
|
+
title: t("archive.manager.title"),
|
|
2284
|
+
description: t("archive.manager.description", { n: archivedSessionIds.length }),
|
|
2285
|
+
footer: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2286
|
+
variant: "outline",
|
|
2287
|
+
disabled: archiveBusyId !== null,
|
|
2288
|
+
onClick: () => setArchiveManagerOpen(false),
|
|
2289
|
+
children: t("close")
|
|
2290
|
+
}),
|
|
2291
|
+
children: [(0, react_jsx_runtime.jsx)("input", {
|
|
2292
|
+
className: WorkspaceBrowser_module_css_default.renameInput,
|
|
2293
|
+
type: "search",
|
|
2294
|
+
value: archiveQuery,
|
|
2295
|
+
maxLength: SEARCH_QUERY_MAX_CODE_UNITS,
|
|
2296
|
+
placeholder: t("archive.manager.searchPlaceholder"),
|
|
2297
|
+
"aria-label": t("archive.manager.searchPlaceholder"),
|
|
2298
|
+
onChange: (event) => { setArchiveQuery(event.target.value); setArchiveError(null); }
|
|
2299
|
+
}), archiveSearch.status === "loading" && (0, react_jsx_runtime.jsx)("div", {
|
|
2300
|
+
className: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
2301
|
+
role: "status",
|
|
2302
|
+
style: { marginTop: 8 },
|
|
2303
|
+
children: t("archive.manager.searching")
|
|
2304
|
+
}), archiveSearch.status === "error" && (0, react_jsx_runtime.jsx)("div", {
|
|
2305
|
+
className: WorkspaceBrowser_module_css_default.renameError,
|
|
2306
|
+
role: "status",
|
|
2307
|
+
children: t("archive.manager.searchUnavailable")
|
|
2308
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
2309
|
+
style: { display: "flex", flexDirection: "column", gap: 8, maxHeight: "52vh", overflowY: "auto", marginTop: 12 },
|
|
2310
|
+
children: archiveRows.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
2311
|
+
className: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
2312
|
+
children: normalizedArchiveQuery === "" ? t("archive.manager.empty") : t("archive.manager.noMatches")
|
|
2313
|
+
}) : archiveRows.map((row) => (0, react_jsx_runtime.jsxs)("div", {
|
|
2314
|
+
style: { border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 12, padding: 12, display: "flex", alignItems: "center", gap: 12 },
|
|
2315
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
2316
|
+
style: { minWidth: 0, flex: 1 },
|
|
2317
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
2318
|
+
style: { color: "var(--dsw-alias-label-primary)", fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2319
|
+
children: row.title
|
|
2320
|
+
}), (0, react_jsx_runtime.jsx)("div", {
|
|
2321
|
+
style: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, marginTop: 2 },
|
|
2322
|
+
children: row.workspace
|
|
2323
|
+
}), archiveSnippets.has(row.id) && (0, react_jsx_runtime.jsx)("div", {
|
|
2324
|
+
style: { color: "var(--dsw-alias-label-secondary)", fontSize: 12, lineHeight: "18px", marginTop: 6 },
|
|
2325
|
+
children: archiveSnippets.get(row.id)
|
|
2326
|
+
})]
|
|
2327
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2328
|
+
variant: "outline",
|
|
2329
|
+
disabled: archiveBusyId !== null,
|
|
2330
|
+
onClick: () => onArchiveRestore(row.id),
|
|
2331
|
+
children: archiveBusyId === row.id ? t("archive.manager.restoring") : t("archive.manager.restore")
|
|
2332
|
+
}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2333
|
+
variant: "outline",
|
|
2334
|
+
className: WorkspaceBrowser_module_css_default.deleteAction,
|
|
2335
|
+
disabled: archiveBusyId !== null,
|
|
2336
|
+
onClick: () => onSessionDelete(row.id, row.title),
|
|
2337
|
+
children: t("archive.manager.delete")
|
|
2338
|
+
})]
|
|
2339
|
+
}, row.id))
|
|
2340
|
+
}), archiveSearch.hasMore && (0, react_jsx_runtime.jsx)("div", {
|
|
2341
|
+
className: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
2342
|
+
style: { marginTop: 8 },
|
|
2343
|
+
children: t("archive.manager.hasMore")
|
|
2344
|
+
}), archiveError !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
2345
|
+
className: WorkspaceBrowser_module_css_default.renameError,
|
|
2346
|
+
role: "alert",
|
|
2347
|
+
children: archiveError
|
|
2348
|
+
})]
|
|
2349
|
+
}),
|
|
2213
2350
|
(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
2214
2351
|
open: sessionDeleteTarget !== null,
|
|
2215
2352
|
onClose: closeSessionDelete,
|
|
@@ -2317,6 +2454,17 @@ window.__ModuleLoader__.load({
|
|
|
2317
2454
|
"delete.pending": "正在删除工作区…",
|
|
2318
2455
|
"menu.fork": "分叉会话",
|
|
2319
2456
|
"menu.archiveSession": "归档会话",
|
|
2457
|
+
"archive.manager.title": "归档会话",
|
|
2458
|
+
"archive.manager.description": "共 {n} 个归档会话。可按名称、工作区或聊天内容搜索。",
|
|
2459
|
+
"archive.manager.searchPlaceholder": "搜索归档名称、工作区或聊天内容…",
|
|
2460
|
+
"archive.manager.searching": "正在搜索归档聊天记录…",
|
|
2461
|
+
"archive.manager.searchUnavailable": "内容搜索暂不可用,仅显示名称与工作区匹配。",
|
|
2462
|
+
"archive.manager.empty": "暂无归档会话",
|
|
2463
|
+
"archive.manager.noMatches": "没有匹配的归档会话",
|
|
2464
|
+
"archive.manager.hasMore": "仅显示前 20 条内容匹配,请缩小搜索范围。",
|
|
2465
|
+
"archive.manager.restore": "恢复",
|
|
2466
|
+
"archive.manager.restoring": "恢复中…",
|
|
2467
|
+
"archive.manager.delete": "永久删除",
|
|
2320
2468
|
"menu.deleteSession": "删除会话",
|
|
2321
2469
|
"delete.session.title": "永久删除会话?",
|
|
2322
2470
|
"delete.session.desc": "“{name}”的会话记录将从本机永久删除,且无法恢复。正在运行的任务会先安全停止。",
|
|
@@ -2387,6 +2535,17 @@ window.__ModuleLoader__.load({
|
|
|
2387
2535
|
"delete.pending": "Deleting workspace…",
|
|
2388
2536
|
"menu.fork": "Fork session",
|
|
2389
2537
|
"menu.archiveSession": "Archive session",
|
|
2538
|
+
"archive.manager.title": "Archived sessions",
|
|
2539
|
+
"archive.manager.description": "{n} archived sessions. Search by name, workspace, or conversation content.",
|
|
2540
|
+
"archive.manager.searchPlaceholder": "Search archived names, workspaces, or conversation content…",
|
|
2541
|
+
"archive.manager.searching": "Searching archived conversation history…",
|
|
2542
|
+
"archive.manager.searchUnavailable": "Content search is temporarily unavailable. Showing name and workspace matches.",
|
|
2543
|
+
"archive.manager.empty": "No archived sessions",
|
|
2544
|
+
"archive.manager.noMatches": "No matching archived sessions",
|
|
2545
|
+
"archive.manager.hasMore": "Showing the first 20 content matches. Narrow your search.",
|
|
2546
|
+
"archive.manager.restore": "Restore",
|
|
2547
|
+
"archive.manager.restoring": "Restoring…",
|
|
2548
|
+
"archive.manager.delete": "Delete permanently",
|
|
2390
2549
|
"menu.deleteSession": "Delete session",
|
|
2391
2550
|
"delete.session.title": "Permanently delete session?",
|
|
2392
2551
|
"delete.session.desc": "The local record for “{name}” will be permanently deleted and cannot be recovered. Running work will be stopped safely before deletion.",
|
|
@@ -2515,6 +2674,31 @@ window.__ModuleLoader__.load({
|
|
|
2515
2674
|
if (refresh.status === "rejected") console.warn("session deletion succeeded but runtime refresh failed:", refresh.reason);
|
|
2516
2675
|
}
|
|
2517
2676
|
},
|
|
2677
|
+
restoreSession: async (sessionId) => {
|
|
2678
|
+
const response = await fetch("/plugins/dsh-session-delete/restore", {
|
|
2679
|
+
method: "POST",
|
|
2680
|
+
headers: {
|
|
2681
|
+
"content-type": "application/json",
|
|
2682
|
+
"x-dsh-session-manager-action": "restore-session"
|
|
2683
|
+
},
|
|
2684
|
+
body: JSON.stringify({ sessionId })
|
|
2685
|
+
});
|
|
2686
|
+
const payload = await response.json().catch(() => null);
|
|
2687
|
+
if (!response.ok || payload?.ok !== true) throw new Error(payload?.error?.message ?? `Restore failed (HTTP ${response.status})`);
|
|
2688
|
+
const refreshes = await Promise.allSettled([ctx.sessions.refresh(), ctx.workspaces.refresh()]);
|
|
2689
|
+
for (const refresh of refreshes) if (refresh.status === "rejected") console.warn("session restore succeeded but runtime refresh failed:", refresh.reason);
|
|
2690
|
+
},
|
|
2691
|
+
searchArchivedSessions: async (query, signal) => {
|
|
2692
|
+
const response = await fetch("/plugins/dsh-session-delete/archive-search", {
|
|
2693
|
+
method: "POST",
|
|
2694
|
+
headers: { "content-type": "application/json" },
|
|
2695
|
+
body: JSON.stringify({ query }),
|
|
2696
|
+
signal
|
|
2697
|
+
});
|
|
2698
|
+
const payload = await response.json().catch(() => null);
|
|
2699
|
+
if (!response.ok || payload?.ok !== true) throw new Error(payload?.error?.message ?? `Archived search failed (HTTP ${response.status})`);
|
|
2700
|
+
return payload.value;
|
|
2701
|
+
},
|
|
2518
2702
|
insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
|
2519
2703
|
await ctx.workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
|
|
2520
2704
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-native-session-delete",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Native DeepSeek Harness session management: archive search, restore, and safe permanent deletion",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"scripts/build-client.mjs",
|
|
17
17
|
"scripts/smoke-ui.mjs",
|
|
18
18
|
"src/index.js",
|
|
19
|
+
"src/host/archive-manager.mjs",
|
|
19
20
|
"src/host/delete-session.mjs",
|
|
20
21
|
"README.md",
|
|
21
22
|
"README.en.md",
|
|
@@ -43,6 +44,13 @@
|
|
|
43
44
|
"dsh-plugin",
|
|
44
45
|
"deepseek-harness",
|
|
45
46
|
"session",
|
|
47
|
+
"session-manager",
|
|
48
|
+
"session-management",
|
|
49
|
+
"conversation",
|
|
50
|
+
"archive",
|
|
51
|
+
"unarchive",
|
|
52
|
+
"restore",
|
|
53
|
+
"history-search",
|
|
46
54
|
"delete",
|
|
47
55
|
"privacy"
|
|
48
56
|
],
|
package/scripts/build-client.mjs
CHANGED
|
@@ -99,7 +99,7 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
99
99
|
workspaceBrowserSignature,
|
|
100
100
|
workspaceBrowserSignature.replace(
|
|
101
101
|
'deleteWorkspace, insertWorkspaceBefore, archiveSession, insertSessionBefore,',
|
|
102
|
-
'deleteWorkspace, insertWorkspaceBefore, archiveSession, deleteSession, insertSessionBefore,',
|
|
102
|
+
'deleteWorkspace, insertWorkspaceBefore, archiveSession, deleteSession, restoreSession, searchArchivedSessions, insertSessionBefore,',
|
|
103
103
|
),
|
|
104
104
|
'workspace browser delete action prop',
|
|
105
105
|
)
|
|
@@ -108,6 +108,65 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
108
108
|
`\t\t\tconst onSessionArchive = (sessionId) => {\n\t\t\t\tarchiveSession(sessionId).catch((reason) => {\n\t\t\t\t\tconsole.warn("session archive rejected:", reason);\n\t\t\t\t});\n\t\t\t};\n\t\t\tconst [sessionDeleteTarget, setSessionDeleteTarget] = (0, react.useState)(null);\n\t\t\tconst [sessionDeleting, setSessionDeleting] = (0, react.useState)(false);\n\t\t\tconst [sessionDeleteError, setSessionDeleteError] = (0, react.useState)(null);\n\t\t\tconst onSessionDelete = (sessionId, title) => {\n\t\t\t\tsetSessionDeleteTarget({ sessionId, title });\n\t\t\t\tsetSessionDeleteError(null);\n\t\t\t};\n\t\t\tconst closeSessionDelete = () => {\n\t\t\t\tif (sessionDeleting) return;\n\t\t\t\tsetSessionDeleteTarget(null);\n\t\t\t\tsetSessionDeleteError(null);\n\t\t\t};\n\t\t\tconst confirmSessionDelete = () => {\n\t\t\t\tif (sessionDeleting || sessionDeleteTarget === null) return;\n\t\t\t\tsetSessionDeleting(true);\n\t\t\t\tsetSessionDeleteError(null);\n\t\t\t\tdeleteSession(sessionDeleteTarget.sessionId).then(() => {\n\t\t\t\t\tsetSessionDeleting(false);\n\t\t\t\t\tsetSessionDeleteTarget(null);\n\t\t\t\t\tsetSessionDeleteError(null);\n\t\t\t\t}).catch((reason) => {\n\t\t\t\t\tsetSessionDeleting(false);\n\t\t\t\t\tsetSessionDeleteError(reason instanceof Error ? reason.message : String(reason));\n\t\t\t\t});\n\t\t\t};\n`,
|
|
109
109
|
'session delete dialog state',
|
|
110
110
|
)
|
|
111
|
+
patch(
|
|
112
|
+
'const [sessionDeleteTarget, setSessionDeleteTarget] = (0, react.useState)(null);',
|
|
113
|
+
`const archiveSessionList = useSessions((state) => state);
|
|
114
|
+
\t\t\tconst [archiveManagerOpen, setArchiveManagerOpen] = (0, react.useState)(false);
|
|
115
|
+
\t\t\tconst [archiveQuery, setArchiveQuery] = (0, react.useState)("");
|
|
116
|
+
\t\t\tconst [archiveSearch, setArchiveSearch] = (0, react.useState)({ query: "", status: "idle", items: [], hasMore: false });
|
|
117
|
+
\t\t\tconst [archiveBusyId, setArchiveBusyId] = (0, react.useState)(null);
|
|
118
|
+
\t\t\tconst [archiveError, setArchiveError] = (0, react.useState)(null);
|
|
119
|
+
\t\t\tconst normalizedArchiveQuery = archiveQuery.trim();
|
|
120
|
+
\t\t\t(0, react.useEffect)(() => {
|
|
121
|
+
\t\t\t\tif (!archiveManagerOpen || normalizedArchiveQuery === "") {
|
|
122
|
+
\t\t\t\t\tsetArchiveSearch({ query: "", status: "idle", items: [], hasMore: false });
|
|
123
|
+
\t\t\t\t\treturn;
|
|
124
|
+
\t\t\t\t}
|
|
125
|
+
\t\t\t\tconst controller = new AbortController();
|
|
126
|
+
\t\t\t\tsetArchiveSearch({ query: normalizedArchiveQuery, status: "loading", items: [], hasMore: false });
|
|
127
|
+
\t\t\t\tconst timer = window.setTimeout(() => {
|
|
128
|
+
\t\t\t\t\tsearchArchivedSessions(normalizedArchiveQuery, controller.signal).then((result) => {
|
|
129
|
+
\t\t\t\t\t\tif (!controller.signal.aborted) setArchiveSearch({ query: normalizedArchiveQuery, status: "ready", items: result.items, hasMore: result.hasMore });
|
|
130
|
+
\t\t\t\t\t}).catch(() => {
|
|
131
|
+
\t\t\t\t\t\tif (!controller.signal.aborted) setArchiveSearch({ query: normalizedArchiveQuery, status: "error", items: [], hasMore: false });
|
|
132
|
+
\t\t\t\t\t});
|
|
133
|
+
\t\t\t\t}, 250);
|
|
134
|
+
\t\t\t\treturn () => { window.clearTimeout(timer); controller.abort(); };
|
|
135
|
+
\t\t\t}, [archiveManagerOpen, normalizedArchiveQuery, searchArchivedSessions]);
|
|
136
|
+
\t\t\tconst archiveWorkspaceBySession = (0, react.useMemo)(() => {
|
|
137
|
+
\t\t\t\tconst result = /* @__PURE__ */ new Map();
|
|
138
|
+
\t\t\t\tfor (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!result.has(sessionId)) result.set(sessionId, workspace.title);
|
|
139
|
+
\t\t\t\treturn result;
|
|
140
|
+
\t\t\t}, [workspaces]);
|
|
141
|
+
\t\t\tconst archiveSnippets = (0, react.useMemo)(() => new Map(archiveSearch.items.map((item) => [item.sessionId, item.snippet])), [archiveSearch.items]);
|
|
142
|
+
\t\t\tconst archiveRows = (0, react.useMemo)(() => {
|
|
143
|
+
\t\t\t\tconst query = normalizedArchiveQuery.toLowerCase();
|
|
144
|
+
\t\t\t\tconst remoteIds = new Set(archiveSearch.items.map((item) => item.sessionId));
|
|
145
|
+
\t\t\t\tconst rows = archivedSessionIds.map((sessionId) => {
|
|
146
|
+
\t\t\t\t\tconst summary = archiveSessionList.byId[sessionId];
|
|
147
|
+
\t\t\t\t\treturn {
|
|
148
|
+
\t\t\t\t\t\tid: sessionId,
|
|
149
|
+
\t\t\t\t\t\ttitle: summary === void 0 ? sessionId : sessionTitle(summary),
|
|
150
|
+
\t\t\t\t\t\tworkspace: archiveWorkspaceBySession.get(sessionId) ?? t("group.ungrouped"),
|
|
151
|
+
\t\t\t\t\t\tupdatedAt: summary?.updatedAt ?? 0
|
|
152
|
+
\t\t\t\t\t};
|
|
153
|
+
\t\t\t\t});
|
|
154
|
+
\t\t\t\trows.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
155
|
+
\t\t\t\tif (query === "") return rows;
|
|
156
|
+
\t\t\t\treturn rows.filter((row) => row.title.toLowerCase().includes(query) || row.workspace.toLowerCase().includes(query) || remoteIds.has(row.id));
|
|
157
|
+
\t\t\t}, [archiveSessionList, archivedSessionIds, archiveSearch.items, archiveWorkspaceBySession, normalizedArchiveQuery, t]);
|
|
158
|
+
\t\t\tconst onArchiveRestore = (sessionId) => {
|
|
159
|
+
\t\t\t\tif (archiveBusyId !== null) return;
|
|
160
|
+
\t\t\t\tsetArchiveBusyId(sessionId);
|
|
161
|
+
\t\t\t\tsetArchiveError(null);
|
|
162
|
+
\t\t\t\trestoreSession(sessionId).then(() => setArchiveBusyId(null)).catch((reason) => {
|
|
163
|
+
\t\t\t\t\tsetArchiveBusyId(null);
|
|
164
|
+
\t\t\t\t\tsetArchiveError(reason instanceof Error ? reason.message : String(reason));
|
|
165
|
+
\t\t\t\t});
|
|
166
|
+
\t\t\t};
|
|
167
|
+
\t\t\tconst [sessionDeleteTarget, setSessionDeleteTarget] = (0, react.useState)(null);`,
|
|
168
|
+
'archive manager state',
|
|
169
|
+
)
|
|
111
170
|
patch(
|
|
112
171
|
'\t\t\t\t\t\t\tonSessionArchive,\n\t\t\t\t\t\t\tarchivedSessionIds,\n',
|
|
113
172
|
'\t\t\t\t\t\t\tonSessionArchive,\n\t\t\t\t\t\t\tonSessionDelete,\n\t\t\t\t\t\t\tarchivedSessionIds,\n',
|
|
@@ -118,6 +177,101 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
118
177
|
'\t\t\t\t\t\t\tonSessionArchive,\n\t\t\t\t\t\t\tonSessionDelete,\n\t\t\t\t\t\t\tforkSession,\n',
|
|
119
178
|
'session tree delete handler',
|
|
120
179
|
)
|
|
180
|
+
patch(
|
|
181
|
+
'children: [wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {',
|
|
182
|
+
`children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
183
|
+
\t\t\t\t\t\t\t\t\tlabel: t("archive.manager.title"),
|
|
184
|
+
\t\t\t\t\t\t\t\t\tside: "bottom",
|
|
185
|
+
\t\t\t\t\t\t\t\t\tdelayMs: 500,
|
|
186
|
+
\t\t\t\t\t\t\t\t\tchildren: (0, react_jsx_runtime.jsx)("button", {
|
|
187
|
+
\t\t\t\t\t\t\t\t\t\tid: "archived-sessions",
|
|
188
|
+
\t\t\t\t\t\t\t\t\t\ttype: "button",
|
|
189
|
+
\t\t\t\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.iconButton,
|
|
190
|
+
\t\t\t\t\t\t\t\t\t\t"aria-label": t("archive.manager.title"),
|
|
191
|
+
\t\t\t\t\t\t\t\t\t\tonClick: () => { setArchiveError(null); setArchiveManagerOpen(true); },
|
|
192
|
+
\t\t\t\t\t\t\t\t\t\tchildren: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: wide ? 16 : 18 })
|
|
193
|
+
\t\t\t\t\t\t\t\t\t})
|
|
194
|
+
\t\t\t\t\t\t\t\t}), wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {`,
|
|
195
|
+
'archive manager header action',
|
|
196
|
+
)
|
|
197
|
+
patch(
|
|
198
|
+
`\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {\n\t\t\t\t\t\topen: deleteTarget !== null,\n`,
|
|
199
|
+
`\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
200
|
+
\t\t\t\t\t\topen: archiveManagerOpen,
|
|
201
|
+
\t\t\t\t\t\tonClose: () => { if (archiveBusyId === null) setArchiveManagerOpen(false); },
|
|
202
|
+
\t\t\t\t\t\tcloseLabel: t("close"),
|
|
203
|
+
\t\t\t\t\t\ttitle: t("archive.manager.title"),
|
|
204
|
+
\t\t\t\t\t\tdescription: t("archive.manager.description", { n: archivedSessionIds.length }),
|
|
205
|
+
\t\t\t\t\t\tfooter: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
206
|
+
\t\t\t\t\t\t\tvariant: "outline",
|
|
207
|
+
\t\t\t\t\t\t\tdisabled: archiveBusyId !== null,
|
|
208
|
+
\t\t\t\t\t\t\tonClick: () => setArchiveManagerOpen(false),
|
|
209
|
+
\t\t\t\t\t\t\tchildren: t("close")
|
|
210
|
+
\t\t\t\t\t\t}),
|
|
211
|
+
\t\t\t\t\t\tchildren: [(0, react_jsx_runtime.jsx)("input", {
|
|
212
|
+
\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.renameInput,
|
|
213
|
+
\t\t\t\t\t\t\ttype: "search",
|
|
214
|
+
\t\t\t\t\t\t\tvalue: archiveQuery,
|
|
215
|
+
\t\t\t\t\t\t\tmaxLength: SEARCH_QUERY_MAX_CODE_UNITS,
|
|
216
|
+
\t\t\t\t\t\t\tplaceholder: t("archive.manager.searchPlaceholder"),
|
|
217
|
+
\t\t\t\t\t\t\t"aria-label": t("archive.manager.searchPlaceholder"),
|
|
218
|
+
\t\t\t\t\t\t\tonChange: (event) => { setArchiveQuery(event.target.value); setArchiveError(null); }
|
|
219
|
+
\t\t\t\t\t\t}), archiveSearch.status === "loading" && (0, react_jsx_runtime.jsx)("div", {
|
|
220
|
+
\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
221
|
+
\t\t\t\t\t\t\trole: "status",
|
|
222
|
+
\t\t\t\t\t\t\tstyle: { marginTop: 8 },
|
|
223
|
+
\t\t\t\t\t\t\tchildren: t("archive.manager.searching")
|
|
224
|
+
\t\t\t\t\t\t}), archiveSearch.status === "error" && (0, react_jsx_runtime.jsx)("div", {
|
|
225
|
+
\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.renameError,
|
|
226
|
+
\t\t\t\t\t\t\trole: "status",
|
|
227
|
+
\t\t\t\t\t\t\tchildren: t("archive.manager.searchUnavailable")
|
|
228
|
+
\t\t\t\t\t\t}), (0, react_jsx_runtime.jsx)("div", {
|
|
229
|
+
\t\t\t\t\t\t\tstyle: { display: "flex", flexDirection: "column", gap: 8, maxHeight: "52vh", overflowY: "auto", marginTop: 12 },
|
|
230
|
+
\t\t\t\t\t\t\tchildren: archiveRows.length === 0 ? (0, react_jsx_runtime.jsx)("div", {
|
|
231
|
+
\t\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
232
|
+
\t\t\t\t\t\t\t\tchildren: normalizedArchiveQuery === "" ? t("archive.manager.empty") : t("archive.manager.noMatches")
|
|
233
|
+
\t\t\t\t\t\t\t}) : archiveRows.map((row) => (0, react_jsx_runtime.jsxs)("div", {
|
|
234
|
+
\t\t\t\t\t\t\t\tstyle: { border: "1px solid var(--dsw-alias-border-l2)", borderRadius: 12, padding: 12, display: "flex", alignItems: "center", gap: 12 },
|
|
235
|
+
\t\t\t\t\t\t\t\tchildren: [(0, react_jsx_runtime.jsxs)("div", {
|
|
236
|
+
\t\t\t\t\t\t\t\t\tstyle: { minWidth: 0, flex: 1 },
|
|
237
|
+
\t\t\t\t\t\t\t\t\tchildren: [(0, react_jsx_runtime.jsx)("div", {
|
|
238
|
+
\t\t\t\t\t\t\t\t\t\tstyle: { color: "var(--dsw-alias-label-primary)", fontSize: 13, fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
239
|
+
\t\t\t\t\t\t\t\t\t\tchildren: row.title
|
|
240
|
+
\t\t\t\t\t\t\t\t\t}), (0, react_jsx_runtime.jsx)("div", {
|
|
241
|
+
\t\t\t\t\t\t\t\t\t\tstyle: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, marginTop: 2 },
|
|
242
|
+
\t\t\t\t\t\t\t\t\t\tchildren: row.workspace
|
|
243
|
+
\t\t\t\t\t\t\t\t\t}), archiveSnippets.has(row.id) && (0, react_jsx_runtime.jsx)("div", {
|
|
244
|
+
\t\t\t\t\t\t\t\t\t\tstyle: { color: "var(--dsw-alias-label-secondary)", fontSize: 12, lineHeight: "18px", marginTop: 6 },
|
|
245
|
+
\t\t\t\t\t\t\t\t\t\tchildren: archiveSnippets.get(row.id)
|
|
246
|
+
\t\t\t\t\t\t\t\t\t})]
|
|
247
|
+
\t\t\t\t\t\t\t\t}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
248
|
+
\t\t\t\t\t\t\t\t\tvariant: "outline",
|
|
249
|
+
\t\t\t\t\t\t\t\t\tdisabled: archiveBusyId !== null,
|
|
250
|
+
\t\t\t\t\t\t\t\t\tonClick: () => onArchiveRestore(row.id),
|
|
251
|
+
\t\t\t\t\t\t\t\t\tchildren: archiveBusyId === row.id ? t("archive.manager.restoring") : t("archive.manager.restore")
|
|
252
|
+
\t\t\t\t\t\t\t\t}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
253
|
+
\t\t\t\t\t\t\t\t\tvariant: "outline",
|
|
254
|
+
\t\t\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteAction,
|
|
255
|
+
\t\t\t\t\t\t\t\t\tdisabled: archiveBusyId !== null,
|
|
256
|
+
\t\t\t\t\t\t\t\t\tonClick: () => onSessionDelete(row.id, row.title),
|
|
257
|
+
\t\t\t\t\t\t\t\t\tchildren: t("archive.manager.delete")
|
|
258
|
+
\t\t\t\t\t\t\t\t})]
|
|
259
|
+
\t\t\t\t\t\t\t}, row.id))
|
|
260
|
+
\t\t\t\t\t\t}), archiveSearch.hasMore && (0, react_jsx_runtime.jsx)("div", {
|
|
261
|
+
\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteStatus,
|
|
262
|
+
\t\t\t\t\t\t\tstyle: { marginTop: 8 },
|
|
263
|
+
\t\t\t\t\t\t\tchildren: t("archive.manager.hasMore")
|
|
264
|
+
\t\t\t\t\t\t}), archiveError !== null && (0, react_jsx_runtime.jsx)("div", {
|
|
265
|
+
\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.renameError,
|
|
266
|
+
\t\t\t\t\t\t\trole: "alert",
|
|
267
|
+
\t\t\t\t\t\t\tchildren: archiveError
|
|
268
|
+
\t\t\t\t\t\t})]
|
|
269
|
+
\t\t\t\t\t}),
|
|
270
|
+
\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
271
|
+
\t\t\t\t\t\topen: deleteTarget !== null,
|
|
272
|
+
`,
|
|
273
|
+
'archive manager modal',
|
|
274
|
+
)
|
|
121
275
|
patch(
|
|
122
276
|
`\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {\n\t\t\t\t\t\topen: deleteTarget !== null,\n`,
|
|
123
277
|
`\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {\n\t\t\t\t\t\topen: sessionDeleteTarget !== null,\n\t\t\t\t\t\tonClose: closeSessionDelete,\n\t\t\t\t\t\tcloseLabel: t("close"),\n\t\t\t\t\t\ttitle: t("delete.session.title"),\n\t\t\t\t\t\t...sessionDeleteTarget === null ? {} : { description: t("delete.session.desc", { name: sessionDeleteTarget.title }) },\n\t\t\t\t\t\tfooter: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {\n\t\t\t\t\t\t\tvariant: "outline",\n\t\t\t\t\t\t\tdisabled: sessionDeleting,\n\t\t\t\t\t\t\tonClick: closeSessionDelete,\n\t\t\t\t\t\t\tchildren: t("cancel")\n\t\t\t\t\t\t}), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {\n\t\t\t\t\t\t\tvariant: "outline",\n\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteAction,\n\t\t\t\t\t\t\tdisabled: sessionDeleting,\n\t\t\t\t\t\t\tonClick: confirmSessionDelete,\n\t\t\t\t\t\t\tchildren: t("delete.session.confirm")\n\t\t\t\t\t\t})] }),\n\t\t\t\t\t\tchildren: [sessionDeleting && (0, react_jsx_runtime.jsx)("div", {\n\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.deleteStatus,\n\t\t\t\t\t\t\trole: "status",\n\t\t\t\t\t\t\tchildren: t("delete.session.pending")\n\t\t\t\t\t\t}), sessionDeleteError !== null && (0, react_jsx_runtime.jsx)("div", {\n\t\t\t\t\t\t\tclassName: WorkspaceBrowser_module_css_default.renameError,\n\t\t\t\t\t\t\trole: "alert",\n\t\t\t\t\t\t\tchildren: sessionDeleteError\n\t\t\t\t\t\t})]\n\t\t\t\t\t}),\n\t\t\t\t\t(0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {\n\t\t\t\t\t\topen: deleteTarget !== null,\n`,
|
|
@@ -125,12 +279,12 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
125
279
|
)
|
|
126
280
|
patch(
|
|
127
281
|
'\t\t\t"menu.archiveSession": "归档会话",\n',
|
|
128
|
-
'\t\t\t"menu.archiveSession": "归档会话",\n\t\t\t"menu.deleteSession": "删除会话",\n\t\t\t"delete.session.title": "永久删除会话?",\n\t\t\t"delete.session.desc": "“{name}”的会话记录将从本机永久删除,且无法恢复。正在运行的任务会先安全停止。",\n\t\t\t"delete.session.confirm": "永久删除",\n\t\t\t"delete.session.pending": "正在永久删除会话…",\n',
|
|
282
|
+
'\t\t\t"menu.archiveSession": "归档会话",\n\t\t\t"archive.manager.title": "归档会话",\n\t\t\t"archive.manager.description": "共 {n} 个归档会话。可按名称、工作区或聊天内容搜索。",\n\t\t\t"archive.manager.searchPlaceholder": "搜索归档名称、工作区或聊天内容…",\n\t\t\t"archive.manager.searching": "正在搜索归档聊天记录…",\n\t\t\t"archive.manager.searchUnavailable": "内容搜索暂不可用,仅显示名称与工作区匹配。",\n\t\t\t"archive.manager.empty": "暂无归档会话",\n\t\t\t"archive.manager.noMatches": "没有匹配的归档会话",\n\t\t\t"archive.manager.hasMore": "仅显示前 20 条内容匹配,请缩小搜索范围。",\n\t\t\t"archive.manager.restore": "恢复",\n\t\t\t"archive.manager.restoring": "恢复中…",\n\t\t\t"archive.manager.delete": "永久删除",\n\t\t\t"menu.deleteSession": "删除会话",\n\t\t\t"delete.session.title": "永久删除会话?",\n\t\t\t"delete.session.desc": "“{name}”的会话记录将从本机永久删除,且无法恢复。正在运行的任务会先安全停止。",\n\t\t\t"delete.session.confirm": "永久删除",\n\t\t\t"delete.session.pending": "正在永久删除会话…",\n',
|
|
129
283
|
'Chinese delete locale',
|
|
130
284
|
)
|
|
131
285
|
patch(
|
|
132
286
|
'\t\t\t"menu.archiveSession": "Archive session",\n',
|
|
133
|
-
'\t\t\t"menu.archiveSession": "Archive session",\n\t\t\t"menu.deleteSession": "Delete session",\n\t\t\t"delete.session.title": "Permanently delete session?",\n\t\t\t"delete.session.desc": "The local record for “{name}” will be permanently deleted and cannot be recovered. Running work will be stopped safely before deletion.",\n\t\t\t"delete.session.confirm": "Delete permanently",\n\t\t\t"delete.session.pending": "Permanently deleting session…",\n',
|
|
287
|
+
'\t\t\t"menu.archiveSession": "Archive session",\n\t\t\t"archive.manager.title": "Archived sessions",\n\t\t\t"archive.manager.description": "{n} archived sessions. Search by name, workspace, or conversation content.",\n\t\t\t"archive.manager.searchPlaceholder": "Search archived names, workspaces, or conversation content…",\n\t\t\t"archive.manager.searching": "Searching archived conversation history…",\n\t\t\t"archive.manager.searchUnavailable": "Content search is temporarily unavailable. Showing name and workspace matches.",\n\t\t\t"archive.manager.empty": "No archived sessions",\n\t\t\t"archive.manager.noMatches": "No matching archived sessions",\n\t\t\t"archive.manager.hasMore": "Showing the first 20 content matches. Narrow your search.",\n\t\t\t"archive.manager.restore": "Restore",\n\t\t\t"archive.manager.restoring": "Restoring…",\n\t\t\t"archive.manager.delete": "Delete permanently",\n\t\t\t"menu.deleteSession": "Delete session",\n\t\t\t"delete.session.title": "Permanently delete session?",\n\t\t\t"delete.session.desc": "The local record for “{name}” will be permanently deleted and cannot be recovered. Running work will be stopped safely before deletion.",\n\t\t\t"delete.session.confirm": "Delete permanently",\n\t\t\t"delete.session.pending": "Permanently deleting session…",\n',
|
|
134
288
|
'English delete locale',
|
|
135
289
|
)
|
|
136
290
|
patch(
|
|
@@ -138,6 +292,37 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
138
292
|
`\t\t\t\tarchiveSession: async (sessionId) => {\n\t\t\t\t\tawait ctx.workspaces.archiveSession(sessionId);\n\t\t\t\t},\n\t\t\t\tdeleteSession: async (sessionId) => {\n\t\t\t\t\tconst response = await fetch("/plugins/dsh-session-delete/delete", {\n\t\t\t\t\t\tmethod: "POST",\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t"content-type": "application/json",\n\t\t\t\t\t\t\t"x-dsh-session-delete-confirmation": "delete-session"\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({ sessionId })\n\t\t\t\t\t});\n\t\t\t\t\tconst payload = await response.json().catch(() => null);\n\t\t\t\t\tif (!response.ok || payload?.ok !== true) {\n\t\t\t\t\t\tthrow new Error(payload?.error?.message ?? \`Delete failed (HTTP \${response.status})\`);\n\t\t\t\t\t}\n\t\t\t\t\tif (ctx.sessions.list.getSnapshot().current === sessionId) ctx.sessions.clear();\n\t\t\t\t\tconst refreshes = await Promise.allSettled([\n\t\t\t\t\t\tctx.sessions.refresh(),\n\t\t\t\t\t\tctx.workspaces.refresh()\n\t\t\t\t\t]);\n\t\t\t\t\tfor (const refresh of refreshes) {\n\t\t\t\t\t\tif (refresh.status === "rejected") console.warn("session deletion succeeded but runtime refresh failed:", refresh.reason);\n\t\t\t\t\t}\n\t\t\t\t},\n`,
|
|
139
293
|
'browser delete request',
|
|
140
294
|
)
|
|
295
|
+
patch(
|
|
296
|
+
'\t\t\t\tinsertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {\n',
|
|
297
|
+
`\t\t\t\trestoreSession: async (sessionId) => {
|
|
298
|
+
\t\t\t\t\tconst response = await fetch("/plugins/dsh-session-delete/restore", {
|
|
299
|
+
\t\t\t\t\t\tmethod: "POST",
|
|
300
|
+
\t\t\t\t\t\theaders: {
|
|
301
|
+
\t\t\t\t\t\t\t"content-type": "application/json",
|
|
302
|
+
\t\t\t\t\t\t\t"x-dsh-session-manager-action": "restore-session"
|
|
303
|
+
\t\t\t\t\t\t},
|
|
304
|
+
\t\t\t\t\t\tbody: JSON.stringify({ sessionId })
|
|
305
|
+
\t\t\t\t\t});
|
|
306
|
+
\t\t\t\t\tconst payload = await response.json().catch(() => null);
|
|
307
|
+
\t\t\t\t\tif (!response.ok || payload?.ok !== true) throw new Error(payload?.error?.message ?? \`Restore failed (HTTP \${response.status})\`);
|
|
308
|
+
\t\t\t\t\tconst refreshes = await Promise.allSettled([ctx.sessions.refresh(), ctx.workspaces.refresh()]);
|
|
309
|
+
\t\t\t\t\tfor (const refresh of refreshes) if (refresh.status === "rejected") console.warn("session restore succeeded but runtime refresh failed:", refresh.reason);
|
|
310
|
+
\t\t\t\t},
|
|
311
|
+
\t\t\t\tsearchArchivedSessions: async (query, signal) => {
|
|
312
|
+
\t\t\t\t\tconst response = await fetch("/plugins/dsh-session-delete/archive-search", {
|
|
313
|
+
\t\t\t\t\t\tmethod: "POST",
|
|
314
|
+
\t\t\t\t\t\theaders: { "content-type": "application/json" },
|
|
315
|
+
\t\t\t\t\t\tbody: JSON.stringify({ query }),
|
|
316
|
+
\t\t\t\t\t\tsignal
|
|
317
|
+
\t\t\t\t\t});
|
|
318
|
+
\t\t\t\t\tconst payload = await response.json().catch(() => null);
|
|
319
|
+
\t\t\t\t\tif (!response.ok || payload?.ok !== true) throw new Error(payload?.error?.message ?? \`Archived search failed (HTTP \${response.status})\`);
|
|
320
|
+
\t\t\t\t\treturn payload.value;
|
|
321
|
+
\t\t\t\t},
|
|
322
|
+
\t\t\t\tinsertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
|
323
|
+
`,
|
|
324
|
+
'browser archive manager requests',
|
|
325
|
+
)
|
|
141
326
|
const homePathCall = '(0, _deepseek_ai_dsh_client_runtime_client.abbreviateHomePath)(row.cwd, home)'
|
|
142
327
|
if (source.includes(homePathCall)) {
|
|
143
328
|
patch(
|
|
@@ -147,7 +332,7 @@ export function patchWorkspaceClient(upstream, upstreamVersion = LATEST_UPSTREAM
|
|
|
147
332
|
)
|
|
148
333
|
}
|
|
149
334
|
|
|
150
|
-
const notice = `// Modified from @deepseek-ai/dsh-client-ui-workspace ${upstreamVersion} by DSH Native Session
|
|
335
|
+
const notice = `// Modified from @deepseek-ai/dsh-client-ui-workspace ${upstreamVersion} by DSH Native Session Manager. See THIRD_PARTY_NOTICES.md.\n`
|
|
151
336
|
return `${notice}${source}`
|
|
152
337
|
}
|
|
153
338
|
|
package/scripts/smoke-ui.mjs
CHANGED
|
@@ -133,6 +133,11 @@ export async function runSmoke(options) {
|
|
|
133
133
|
})
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
await page.locator('#archived-sessions').click()
|
|
137
|
+
const archiveDialog = page.getByRole('dialog', { name: /^(Archived sessions|归档会话)$/ })
|
|
138
|
+
await archiveDialog.getByRole('searchbox').waitFor()
|
|
139
|
+
await archiveDialog.getByRole('button', { name: /^(Close|关闭)$/ }).filter({ hasText: /^(Close|关闭)$/ }).click()
|
|
140
|
+
await archiveDialog.waitFor({ state: 'hidden' })
|
|
136
141
|
const matchingTitles = page.getByText(options.session, { exact: true })
|
|
137
142
|
await matchingTitles.first().waitFor()
|
|
138
143
|
const rowCount = await matchingTitles.count()
|
|
@@ -193,8 +198,8 @@ export async function runSmoke(options) {
|
|
|
193
198
|
return {
|
|
194
199
|
ok: true,
|
|
195
200
|
checks: options.simulateDeleteSuccess === true
|
|
196
|
-
? ['Archive session', 'red Delete session', 'confirmation dialog', 'successful delete without reload']
|
|
197
|
-
: ['Archive session', 'red Delete session', 'confirmation dialog', 'cancel without request'],
|
|
201
|
+
? ['archive manager', 'Archive session', 'red Delete session', 'confirmation dialog', 'successful delete without reload']
|
|
202
|
+
: ['archive manager', 'Archive session', 'red Delete session', 'confirmation dialog', 'cancel without request'],
|
|
198
203
|
}
|
|
199
204
|
} finally {
|
|
200
205
|
await browser.close()
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const SEARCH_LIMIT = 20
|
|
2
|
+
const MESSAGE_TYPES = new Set(['user/message', 'assistant/message'])
|
|
3
|
+
const MAX_REQUEST_BYTES = 8 * 1024
|
|
4
|
+
|
|
5
|
+
const assertSessionId = sessionId => {
|
|
6
|
+
if (typeof sessionId !== 'string' || sessionId.length === 0 || sessionId.length > 512 || sessionId.includes('\0')) {
|
|
7
|
+
throw new TypeError('invalid archived session id')
|
|
8
|
+
}
|
|
9
|
+
return sessionId
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const archiveIds = workspaceRegistry => {
|
|
13
|
+
const ids = workspaceRegistry?.archivedSessionIds
|
|
14
|
+
if (!Array.isArray(ids) || ids.some(id => typeof id !== 'string')) {
|
|
15
|
+
throw new Error('unsupported workspace registry archive snapshot')
|
|
16
|
+
}
|
|
17
|
+
return [...ids]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const plainSnippet = (text, query) => {
|
|
21
|
+
const compact = text.replace(/\s+/g, ' ').trim()
|
|
22
|
+
if (compact.length <= 240) return compact
|
|
23
|
+
const matchAt = compact.toLowerCase().indexOf(query.toLowerCase())
|
|
24
|
+
const start = Math.max(0, (matchAt === -1 ? 0 : matchAt) - 80)
|
|
25
|
+
const end = Math.min(compact.length, start + 240)
|
|
26
|
+
return `${start > 0 ? '…' : ''}${compact.slice(start, end)}${end < compact.length ? '…' : ''}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const scanArchivedEvents = async (sessionQuery, archivedSessionIds, query, signal) => {
|
|
30
|
+
if (typeof sessionQuery?.filterEvents !== 'function') {
|
|
31
|
+
throw new Error('archived history scan is unavailable')
|
|
32
|
+
}
|
|
33
|
+
const filters = [
|
|
34
|
+
{ kind: 'type', values: ['user/message', 'assistant/message'] },
|
|
35
|
+
{ kind: 'surface', values: ['current'] },
|
|
36
|
+
{ kind: 'text', text: query },
|
|
37
|
+
]
|
|
38
|
+
const items = []
|
|
39
|
+
for (let offset = 0; offset < archivedSessionIds.length && items.length <= SEARCH_LIMIT; offset += 4) {
|
|
40
|
+
signal?.throwIfAborted()
|
|
41
|
+
const batch = archivedSessionIds.slice(offset, offset + 4)
|
|
42
|
+
const matches = await Promise.all(batch.map(sessionId => sessionQuery.filterEvents(sessionId, filters)))
|
|
43
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
44
|
+
const match = Array.isArray(matches[index])
|
|
45
|
+
? matches[index].find(event => typeof event?.text === 'string' && event.text.trim().length > 0)
|
|
46
|
+
: undefined
|
|
47
|
+
if (match !== undefined) items.push({ sessionId: batch[index], snippet: plainSnippet(match.text, query) })
|
|
48
|
+
if (items.length > SEARCH_LIMIT) break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { items: items.slice(0, SEARCH_LIMIT), hasMore: items.length > SEARCH_LIMIT }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Remove one id from DSH's registry-global archive set without touching the
|
|
56
|
+
* session log or its workspace accounting position. DSH does not expose a
|
|
57
|
+
* public unarchive method yet, so every private seam is checked before use.
|
|
58
|
+
*/
|
|
59
|
+
export async function restoreArchivedSession(workspaceRegistry, sessionId) {
|
|
60
|
+
const id = assertSessionId(sessionId)
|
|
61
|
+
if (
|
|
62
|
+
typeof workspaceRegistry?.enqueueOperation !== 'function'
|
|
63
|
+
|| typeof workspaceRegistry?.requireState !== 'function'
|
|
64
|
+
|| typeof workspaceRegistry?.setState !== 'function'
|
|
65
|
+
) {
|
|
66
|
+
throw new Error('unsupported workspace registry restore seam')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return workspaceRegistry.enqueueOperation(async () => {
|
|
70
|
+
const state = workspaceRegistry.requireState()
|
|
71
|
+
if (!Array.isArray(state?.archivedSessionIds)) {
|
|
72
|
+
throw new Error('unsupported workspace registry restore state')
|
|
73
|
+
}
|
|
74
|
+
if (!state.archivedSessionIds.includes(id)) {
|
|
75
|
+
return { restored: false, archivedSessionIds: [...state.archivedSessionIds] }
|
|
76
|
+
}
|
|
77
|
+
const archivedSessionIds = state.archivedSessionIds.filter(candidate => candidate !== id)
|
|
78
|
+
await workspaceRegistry.setState({ ...state, archivedSessionIds })
|
|
79
|
+
return { restored: true, archivedSessionIds }
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Keep DSH's archive registry free of ids whose storage was permanently removed. */
|
|
84
|
+
export async function deleteSessionAndReconcileArchive({ workspaceRegistry, deleteSession, warn = console.warn }, sessionId) {
|
|
85
|
+
const result = await deleteSession(sessionId)
|
|
86
|
+
if (result?.ok !== true) return result
|
|
87
|
+
try {
|
|
88
|
+
await restoreArchivedSession(workspaceRegistry, sessionId)
|
|
89
|
+
return { ok: true, value: { ...result.value, archiveReconciled: true } }
|
|
90
|
+
} catch (error) {
|
|
91
|
+
warn('session storage was deleted but its archive marker could not be reconciled:', error)
|
|
92
|
+
return { ok: true, value: { ...result.value, archiveReconciled: false } }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Search current user/assistant message history inside the archive set only. */
|
|
97
|
+
export async function searchArchivedSessions({ workspaceRegistry, sessionQuery }, query, signal) {
|
|
98
|
+
const normalized = typeof query === 'string' ? query.trim() : ''
|
|
99
|
+
if (normalized.length === 0 || normalized.length > 500 || normalized.includes('\0')) {
|
|
100
|
+
throw new TypeError('invalid archive search query')
|
|
101
|
+
}
|
|
102
|
+
if (typeof sessionQuery?.searchSessions !== 'function') {
|
|
103
|
+
throw new Error('archived history search is unavailable')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const archivedSessionIds = archiveIds(workspaceRegistry)
|
|
107
|
+
if (archivedSessionIds.length === 0) return { items: [], hasMore: false }
|
|
108
|
+
|
|
109
|
+
let page
|
|
110
|
+
try {
|
|
111
|
+
page = await sessionQuery.searchSessions({
|
|
112
|
+
query: normalized,
|
|
113
|
+
sessionFilters: [{ kind: 'id', values: archivedSessionIds }],
|
|
114
|
+
eventFilters: [
|
|
115
|
+
{ kind: 'type', values: ['user/message', 'assistant/message'] },
|
|
116
|
+
{ kind: 'surface', values: ['current'] },
|
|
117
|
+
],
|
|
118
|
+
limit: SEARCH_LIMIT,
|
|
119
|
+
}, { signal })
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code !== 'SESSION_QUERY_SEARCH_DISABLED') throw error
|
|
122
|
+
return scanArchivedEvents(sessionQuery, archivedSessionIds, normalized, signal)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const archived = new Set(archivedSessionIds)
|
|
126
|
+
const items = []
|
|
127
|
+
const included = new Set()
|
|
128
|
+
for (const hit of Array.isArray(page?.items) ? page.items : []) {
|
|
129
|
+
const sessionId = hit?.header?.id
|
|
130
|
+
const match = hit?.bestMatch
|
|
131
|
+
if (
|
|
132
|
+
typeof sessionId !== 'string'
|
|
133
|
+
|| !archived.has(sessionId)
|
|
134
|
+
|| included.has(sessionId)
|
|
135
|
+
|| match?.sessionId !== sessionId
|
|
136
|
+
|| match?.surface !== 'current'
|
|
137
|
+
|| !MESSAGE_TYPES.has(match?.type)
|
|
138
|
+
|| typeof match?.snippet !== 'string'
|
|
139
|
+
) continue
|
|
140
|
+
included.add(sessionId)
|
|
141
|
+
items.push({ sessionId, snippet: match.snippet })
|
|
142
|
+
if (items.length >= SEARCH_LIMIT) break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { items, hasMore: page?.nextCursor !== undefined }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sendJson = (res, status, payload) => {
|
|
149
|
+
res.writeHead(status, {
|
|
150
|
+
'content-type': 'application/json; charset=utf-8',
|
|
151
|
+
'cache-control': 'no-store',
|
|
152
|
+
'x-content-type-options': 'nosniff',
|
|
153
|
+
})
|
|
154
|
+
res.end(JSON.stringify(payload))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const failure = (code, message) => ({ ok: false, error: { code, message } })
|
|
158
|
+
|
|
159
|
+
const readJsonBody = async req => {
|
|
160
|
+
const chunks = []
|
|
161
|
+
let size = 0
|
|
162
|
+
for await (const chunk of req) {
|
|
163
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
164
|
+
size += buffer.length
|
|
165
|
+
if (size > MAX_REQUEST_BYTES) throw new Error('request-too-large')
|
|
166
|
+
chunks.push(buffer)
|
|
167
|
+
}
|
|
168
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const acceptJsonPost = (req, res) => {
|
|
172
|
+
if (req.method !== 'POST') {
|
|
173
|
+
sendJson(res, 405, failure('method-not-allowed', '只允许使用 POST 管理归档会话。'))
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
const host = req.headers.host
|
|
177
|
+
if (typeof host !== 'string' || req.headers.origin !== `http://${host}`) {
|
|
178
|
+
sendJson(res, 403, failure('forbidden', '归档管理请求未通过同源校验。'))
|
|
179
|
+
return false
|
|
180
|
+
}
|
|
181
|
+
const contentType = req.headers['content-type']
|
|
182
|
+
if (typeof contentType !== 'string' || !contentType.toLowerCase().startsWith('application/json')) {
|
|
183
|
+
sendJson(res, 415, failure('unsupported-media-type', '归档管理请求必须使用 JSON。'))
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const parseBody = async (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
return await readJsonBody(req)
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const tooLarge = error instanceof Error && error.message === 'request-too-large'
|
|
194
|
+
sendJson(
|
|
195
|
+
res,
|
|
196
|
+
tooLarge ? 413 : 400,
|
|
197
|
+
failure(tooLarge ? 'request-too-large' : 'invalid-json', tooLarge ? '归档管理请求过大。' : '归档管理请求不是有效 JSON。'),
|
|
198
|
+
)
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Same-origin HTTP boundary consumed by the native archive manager UI. */
|
|
204
|
+
export function createArchiveRequestHandlers({ restore, search, warn = console.warn }) {
|
|
205
|
+
return {
|
|
206
|
+
restore: async (req, res) => {
|
|
207
|
+
if (!acceptJsonPost(req, res)) return
|
|
208
|
+
if (req.headers['x-dsh-session-manager-action'] !== 'restore-session') {
|
|
209
|
+
sendJson(res, 403, failure('forbidden', '恢复请求缺少明确的操作标记。'))
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const body = await parseBody(req, res)
|
|
213
|
+
if (body === undefined) return
|
|
214
|
+
try {
|
|
215
|
+
const sessionId = assertSessionId(body?.sessionId)
|
|
216
|
+
sendJson(res, 200, { ok: true, value: await restore(sessionId) })
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (error instanceof TypeError) {
|
|
219
|
+
sendJson(res, 400, failure('invalid-session-id', '会话 ID 无效。'))
|
|
220
|
+
} else {
|
|
221
|
+
sendJson(res, 500, failure('restore-failed', '取消归档失败,归档状态未确认改变。'))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
search: async (req, res) => {
|
|
226
|
+
if (!acceptJsonPost(req, res)) return
|
|
227
|
+
const body = await parseBody(req, res)
|
|
228
|
+
if (body === undefined) return
|
|
229
|
+
const query = typeof body?.query === 'string' ? body.query.trim() : ''
|
|
230
|
+
if (query.length === 0 || query.length > 500 || query.includes('\0')) {
|
|
231
|
+
sendJson(res, 400, failure('invalid-query', '搜索内容无效。'))
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
const controller = new AbortController()
|
|
235
|
+
const abort = () => controller.abort(new Error('archive search request closed'))
|
|
236
|
+
req.once('aborted', abort)
|
|
237
|
+
if (typeof res.once === 'function') res.once('close', abort)
|
|
238
|
+
try {
|
|
239
|
+
const value = await search(query, controller.signal)
|
|
240
|
+
sendJson(res, 200, { ok: true, value })
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (controller.signal.aborted) return
|
|
243
|
+
warn('archived history search failed:', error)
|
|
244
|
+
sendJson(res, 503, failure('search-unavailable', '归档内容搜索暂不可用。'))
|
|
245
|
+
} finally {
|
|
246
|
+
req.off('aborted', abort)
|
|
247
|
+
if (typeof res.off === 'function') res.off('close', abort)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,9 +3,15 @@ import {
|
|
|
3
3
|
deleteSessionSafely,
|
|
4
4
|
installAgentHandleTracker,
|
|
5
5
|
} from './host/delete-session.mjs'
|
|
6
|
+
import {
|
|
7
|
+
createArchiveRequestHandlers,
|
|
8
|
+
deleteSessionAndReconcileArchive,
|
|
9
|
+
restoreArchivedSession,
|
|
10
|
+
searchArchivedSessions,
|
|
11
|
+
} from './host/archive-manager.mjs'
|
|
6
12
|
|
|
7
13
|
export const name = 'dsh-session-delete'
|
|
8
|
-
export const inject = ['webServer', 'sessionPersistence', 'sessions', 'agents']
|
|
14
|
+
export const inject = ['webServer', 'sessionPersistence', 'sessions', 'agents', 'workspaceRegistry']
|
|
9
15
|
|
|
10
16
|
export function apply(ctx) {
|
|
11
17
|
const sessionRoot = ctx.sessionPersistence?.root
|
|
@@ -16,13 +22,24 @@ export function apply(ctx) {
|
|
|
16
22
|
const agentHandles = installAgentHandleTracker(ctx.agents, ctx.sessions)
|
|
17
23
|
ctx.effect(() => () => agentHandles.release(), 'dsh-session-delete: agent lifecycle tracking')
|
|
18
24
|
|
|
25
|
+
const deleteSession = sessionId => deleteSessionSafely({
|
|
26
|
+
sessions: ctx.sessions,
|
|
27
|
+
agents: ctx.agents,
|
|
28
|
+
agentHandles,
|
|
29
|
+
sessionPersistence: ctx.sessionPersistence,
|
|
30
|
+
}, { sessionRoot, sessionId })
|
|
19
31
|
const handler = createDeleteRequestHandler({
|
|
20
|
-
deleteSession: sessionId =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
deleteSession: sessionId => deleteSessionAndReconcileArchive({
|
|
33
|
+
workspaceRegistry: ctx.workspaceRegistry,
|
|
34
|
+
deleteSession,
|
|
35
|
+
}, sessionId),
|
|
36
|
+
})
|
|
37
|
+
const archiveHandlers = createArchiveRequestHandlers({
|
|
38
|
+
restore: sessionId => restoreArchivedSession(ctx.workspaceRegistry, sessionId),
|
|
39
|
+
search: (query, signal) => searchArchivedSessions({
|
|
40
|
+
workspaceRegistry: ctx.workspaceRegistry,
|
|
41
|
+
sessionQuery: ctx.get('sessionQuery'),
|
|
42
|
+
}, query, signal),
|
|
26
43
|
})
|
|
27
44
|
|
|
28
45
|
ctx.effect(
|
|
@@ -33,6 +50,22 @@ export function apply(ctx) {
|
|
|
33
50
|
}),
|
|
34
51
|
'dsh-session-delete: confirmed permanent deletion route',
|
|
35
52
|
)
|
|
53
|
+
ctx.effect(
|
|
54
|
+
() => ctx.webServer.register({
|
|
55
|
+
kind: 'exact',
|
|
56
|
+
path: '/plugins/dsh-session-delete/restore',
|
|
57
|
+
handler: archiveHandlers.restore,
|
|
58
|
+
}),
|
|
59
|
+
'dsh-session-delete: archived session restore route',
|
|
60
|
+
)
|
|
61
|
+
ctx.effect(
|
|
62
|
+
() => ctx.webServer.register({
|
|
63
|
+
kind: 'exact',
|
|
64
|
+
path: '/plugins/dsh-session-delete/archive-search',
|
|
65
|
+
handler: archiveHandlers.search,
|
|
66
|
+
}),
|
|
67
|
+
'dsh-session-delete: archived history search route',
|
|
68
|
+
)
|
|
36
69
|
}
|
|
37
70
|
|
|
38
71
|
export {
|
|
@@ -41,3 +74,10 @@ export {
|
|
|
41
74
|
deleteSessionSafely,
|
|
42
75
|
installAgentHandleTracker,
|
|
43
76
|
} from './host/delete-session.mjs'
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
createArchiveRequestHandlers,
|
|
80
|
+
deleteSessionAndReconcileArchive,
|
|
81
|
+
restoreArchivedSession,
|
|
82
|
+
searchArchivedSessions,
|
|
83
|
+
} from './host/archive-manager.mjs'
|