dsh-session-remover 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.en.md +64 -0
- package/README.md +63 -2
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1019 -0
- package/lib/index.js +613 -0
- package/package.json +75 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-session-remover",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
"use strict";
|
|
5
|
+
var module = { exports: {} };
|
|
6
|
+
var exports = module.exports;
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
|
+
|
|
9
|
+
var react = require("react");
|
|
10
|
+
|
|
11
|
+
var primitives = null;
|
|
12
|
+
try {
|
|
13
|
+
primitives = require("@deepseek-ai/dsh-client-ui-primitives") || null;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
primitives = null;
|
|
16
|
+
}
|
|
17
|
+
var MenuItemButton = primitives && primitives.MenuItemButton ? primitives.MenuItemButton : null;
|
|
18
|
+
var IconTrash = primitives && primitives.IconTrashOutlineRegular ? primitives.IconTrashOutlineRegular : null;
|
|
19
|
+
var IconCheck = primitives && primitives.IconCheckOutlineRegular ? primitives.IconCheckOutlineRegular : null;
|
|
20
|
+
var Modal = primitives && primitives.Modal ? primitives.Modal : null;
|
|
21
|
+
var Checkbox = primitives && primitives.Checkbox ? primitives.Checkbox : null;
|
|
22
|
+
var Button = primitives && primitives.Button ? primitives.Button : null;
|
|
23
|
+
|
|
24
|
+
var reactDomClient = null;
|
|
25
|
+
try {
|
|
26
|
+
reactDomClient = require("react-dom/client") || null;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
reactDomClient = null;
|
|
29
|
+
}
|
|
30
|
+
var canMountOwnRoot = !!(reactDomClient && typeof reactDomClient.createRoot === "function");
|
|
31
|
+
|
|
32
|
+
var SLOT = "sidebar.workspaces.session.menu.item";
|
|
33
|
+
var ENTRY_ID = "dsh-session-remover/delete";
|
|
34
|
+
var ENTRY_ORDER = 500;
|
|
35
|
+
|
|
36
|
+
var BATCH_ENTRY_ID = "dsh-session-remover/batch-delete";
|
|
37
|
+
var BATCH_ENTRY_ORDER = 600;
|
|
38
|
+
var BATCH_ENDPOINT = "/api/session.delete";
|
|
39
|
+
var STYLE_ID = "dsh-session-remover/style";
|
|
40
|
+
|
|
41
|
+
var NS = "dsh-session-remover";
|
|
42
|
+
var DICT = {
|
|
43
|
+
zh: {
|
|
44
|
+
"menu.delete": "删除对话",
|
|
45
|
+
"menu.batch": "批量删除…",
|
|
46
|
+
"batch.title": "批量删除对话",
|
|
47
|
+
"batch.description": "勾选要删除的对话。删除后移入系统回收站,其子代理会话一并移入。",
|
|
48
|
+
"batch.ack": "删除后移入系统回收站,其子代理会话一并移入。",
|
|
49
|
+
"batch.selected": "已选 {selected} / 共 {total} 个",
|
|
50
|
+
"batch.selectAll": "全选",
|
|
51
|
+
"batch.selectNone": "全不选",
|
|
52
|
+
"batch.noSource": "无法读取会话清单,未列出任何对话。单条删除仍可使用。",
|
|
53
|
+
"batch.empty": "没有可批量删除的对话。子代理会话、空白会话、已归档会话不在此列。",
|
|
54
|
+
"batch.forkNote": "列表含 {count} 个「分支」对话,由其他对话派生。删除分支不影响源对话,删除源对话亦不连带删除分支;如需删除请单独勾选。",
|
|
55
|
+
"batch.currentNote": "所选包含当前打开的对话。删除前将先停止其任务,完成后页面自动刷新。",
|
|
56
|
+
"batch.deleting": "正在删除 {index}/{total}:{title}",
|
|
57
|
+
"batch.deletingShort": "正在删除…",
|
|
58
|
+
"batch.continue": "继续删除剩余 {count} 个",
|
|
59
|
+
"batch.confirmDelete": "删除选中的 {count} 个",
|
|
60
|
+
"batch.report.ok": "已移入回收站 {count} 个{list}",
|
|
61
|
+
"batch.report.okColon": ":",
|
|
62
|
+
"fmt.quote": "「{title}」",
|
|
63
|
+
"batch.report.failed": "已中止,未删除 {count} 个。失败项「{title}」:{reason}",
|
|
64
|
+
"batch.report.rest": "其后 {count} 个未处理。排除故障后可继续删除。",
|
|
65
|
+
"batch.report.hint": "侧栏可能仍显示已删除的对话,刷新页面后与磁盘一致。已移入系统回收站的对话可从回收站还原。",
|
|
66
|
+
"batch.report.warn": "会话已移入回收站,但有 {count} 条残留记录未能清理:{list}",
|
|
67
|
+
"confirm.delete": "确认删除对话「{title}」?\n该对话及其子代理会话将移入系统回收站,可从回收站还原。",
|
|
68
|
+
"confirm.batch": "确认删除选中的 {count} 个对话?\n这些对话及其子代理会话将移入系统回收站,可从回收站还原。",
|
|
69
|
+
"error.noSessionId": "删除失败:未获取到会话编号,请刷新页面后重试。",
|
|
70
|
+
"error.noSessionIdShort": "未获取到会话编号",
|
|
71
|
+
"error.http": "删除失败(HTTP {status})",
|
|
72
|
+
"error.leftover": "会话已移入回收站,但有残留记录未能清理:\n{list}",
|
|
73
|
+
"error.request": "删除请求失败:{reason}",
|
|
74
|
+
"error.batchUnavailable": "批量删除不可用:缺少所需运行组件,请重启 DSH 后重试。",
|
|
75
|
+
"error.batchUnavailableShort": "批量删除不可用:{reason}",
|
|
76
|
+
"tag.fork": "分支",
|
|
77
|
+
"tag.current": "当前打开",
|
|
78
|
+
"common.untitled": "(无标题)",
|
|
79
|
+
"common.close": "关闭",
|
|
80
|
+
"common.cancel": "取消",
|
|
81
|
+
"common.reload": "刷新页面",
|
|
82
|
+
"join.list": "、",
|
|
83
|
+
"join.warn": ";",
|
|
84
|
+
"err.E_SESSION_ROOT_UNREADABLE": "无法读取会话根目录:{reason}",
|
|
85
|
+
"err.E_WORKSPACE_DIR_UNREADABLE": "无法读取工作区目录 {name}:{reason}",
|
|
86
|
+
"err.E_REGISTRY_UNAVAILABLE": "工作区登记表不可用",
|
|
87
|
+
"err.E_SESSION_IDS_NOT_ARRAY": "工作区登记表中的会话名单不是数组({type})",
|
|
88
|
+
"err.E_ARCHIVED_NOT_ARRAY": "归档名单不是数组({type})",
|
|
89
|
+
"err.E_REGISTRY_READ_FAILED": "读取工作区登记表失败:{reason}",
|
|
90
|
+
"err.E_PROTECTED_LIST_UNREADABLE": "无法读取受保护的会话名单({reason});为避免误删,已放弃连带删除子代理会话",
|
|
91
|
+
"err.E_INDEX_UNREADABLE": "{reason};为避免误删,已放弃连带删除子代理会话",
|
|
92
|
+
"err.E_CSCRIPT_MISSING": "未找到系统脚本宿主 cscript.exe",
|
|
93
|
+
"err.E_DIR_OUTSIDE_ROOT": "已拒绝:待删除的目录不在会话根目录内",
|
|
94
|
+
"err.E_RECYCLE_SPAWN_FAILED": "调用回收站失败:{reason}",
|
|
95
|
+
"err.E_RECYCLE_NO_RESULT": "回收站未返回结果,脚本未执行完",
|
|
96
|
+
"err.E_RECYCLE_DISABLED": "本机已关闭回收站(已勾选「不将文件移到回收站中,直接删除」),插件拒绝删除,以免数据无法还原",
|
|
97
|
+
"err.E_RECYCLE_UNRECOGNIZED": "无法识别回收站返回的结果",
|
|
98
|
+
"err.E_RECYCLE_LEFT": "共 {total} 个会话目录,已移入回收站 {moved} 个,仍有 {left} 个未能移入",
|
|
99
|
+
"err.E_RECYCLE_LEFT_REASON": "共 {total} 个会话目录,已移入回收站 {moved} 个,仍有 {left} 个未能移入:{reason}",
|
|
100
|
+
"err.E_RECYCLE_MISMATCH": "回收站中仅找到 {matched}/{total} 个会话,可能有目录已被永久删除,请立即检查回收站",
|
|
101
|
+
"err.E_WS_DETACH_FAILED": "从工作区摘除失败:{reason}",
|
|
102
|
+
"err.E_DETACH_FAILED": "会话 {id} 未能从工作区摘除:{reason}",
|
|
103
|
+
"err.E_ABORTED_NO_CHANGE": "{reason};已放弃删除,未改动任何数据",
|
|
104
|
+
"err.E_NOT_FOUND": "未找到该会话",
|
|
105
|
+
"err.E_MOVE_ABORTED": "{reason};已中止,存储域与登记表保持原样",
|
|
106
|
+
"err.E_LOOPBACK_ONLY": "已拒绝:该接口仅接受本机(回环)请求",
|
|
107
|
+
"err.E_POST_ONLY": "仅接受 POST 请求",
|
|
108
|
+
"err.E_BODY_TOO_LARGE": "请求体过大",
|
|
109
|
+
"err.E_BODY_READ_FAILED": "读取请求体失败",
|
|
110
|
+
"err.E_SESSION_ID_REQUIRED": "缺少会话编号",
|
|
111
|
+
"err.E_SESSION_ID_INVALID": "会话编号格式不正确:{id}",
|
|
112
|
+
"err.E_INTERNAL": "服务端内部错误:{reason}",
|
|
113
|
+
"leftover.L_SESSION_DIR": "会话目录",
|
|
114
|
+
"leftover.L_PROJ_CACHE": "投影缓存",
|
|
115
|
+
"leftover.L_WORKSPACE_REGISTRY": "工作区记账",
|
|
116
|
+
"leftover.L_ARCHIVE_SET": "归档集",
|
|
117
|
+
"leftover.L_SESSION_LEFTOVER": "会话 {id}:{what}"
|
|
118
|
+
},
|
|
119
|
+
en: {
|
|
120
|
+
"menu.delete": "Delete conversation",
|
|
121
|
+
"menu.batch": "Batch delete…",
|
|
122
|
+
"batch.title": "Batch delete conversations",
|
|
123
|
+
"batch.description": "Tick the conversations to delete. They are moved to the system recycle bin, together with their subagent sessions.",
|
|
124
|
+
"batch.ack": "After deletion they go to the system recycle bin, together with their subagent sessions.",
|
|
125
|
+
"batch.selected": "{selected} of {total} selected",
|
|
126
|
+
"batch.selectAll": "Select all",
|
|
127
|
+
"batch.selectNone": "Select none",
|
|
128
|
+
"batch.noSource": "Could not read the conversation list, so none are listed. Single delete still works.",
|
|
129
|
+
"batch.empty": "No conversations to batch delete. Subagent, blank and archived sessions are not listed here.",
|
|
130
|
+
"batch.forkNote": "The list contains {count} forked conversations, derived from other conversations. Deleting a fork does not affect its source, and deleting a source does not delete its forks; tick them individually if you want them gone.",
|
|
131
|
+
"batch.currentNote": "The selection includes the conversation you have open. Its running task will be stopped first, and the page reloads when done.",
|
|
132
|
+
"batch.deleting": "Deleting {index}/{total}: {title}",
|
|
133
|
+
"batch.deletingShort": "Deleting…",
|
|
134
|
+
"batch.continue": "Delete the remaining {count}",
|
|
135
|
+
"batch.confirmDelete": "Delete {count} selected",
|
|
136
|
+
"batch.report.ok": "{count} moved to the recycle bin{list}",
|
|
137
|
+
"batch.report.okColon": ": ",
|
|
138
|
+
"fmt.quote": "\"{title}\"",
|
|
139
|
+
"batch.report.failed": "Stopped; {count} not deleted. Failed on \"{title}\": {reason}",
|
|
140
|
+
"batch.report.rest": "The remaining {count} were not processed. Fix the problem, then continue.",
|
|
141
|
+
"batch.report.hint": "The sidebar may still show deleted conversations; reload the page to match the disk. Conversations in the system recycle bin can be restored from it.",
|
|
142
|
+
"batch.report.warn": "The conversations were moved to the recycle bin, but {count} leftover records could not be cleaned: {list}",
|
|
143
|
+
"confirm.delete": "Delete conversation \"{title}\"?\nIt and its subagent sessions will be moved to the system recycle bin, from which they can be restored.",
|
|
144
|
+
"confirm.batch": "Delete the {count} selected conversations?\nThey and their subagent sessions will be moved to the system recycle bin, from which they can be restored.",
|
|
145
|
+
"error.noSessionId": "Delete failed: no session id was available. Reload the page and try again.",
|
|
146
|
+
"error.noSessionIdShort": "no session id was available",
|
|
147
|
+
"error.http": "Delete failed (HTTP {status})",
|
|
148
|
+
"error.leftover": "The conversation was moved to the recycle bin, but some leftover records could not be cleaned:\n{list}",
|
|
149
|
+
"error.request": "Delete request failed: {reason}",
|
|
150
|
+
"error.batchUnavailable": "Batch delete is unavailable: a required runtime module is missing. Restart DSH and try again.",
|
|
151
|
+
"error.batchUnavailableShort": "Batch delete is unavailable: {reason}",
|
|
152
|
+
"tag.fork": "fork",
|
|
153
|
+
"tag.current": "open",
|
|
154
|
+
"common.untitled": "(untitled)",
|
|
155
|
+
"common.close": "Close",
|
|
156
|
+
"common.cancel": "Cancel",
|
|
157
|
+
"common.reload": "Reload page",
|
|
158
|
+
"join.list": ", ",
|
|
159
|
+
"join.warn": "; ",
|
|
160
|
+
"err.E_SESSION_ROOT_UNREADABLE": "Cannot read the sessions root directory: {reason}",
|
|
161
|
+
"err.E_WORKSPACE_DIR_UNREADABLE": "Cannot read the workspace directory {name}: {reason}",
|
|
162
|
+
"err.E_REGISTRY_UNAVAILABLE": "The workspace registry is unavailable",
|
|
163
|
+
"err.E_SESSION_IDS_NOT_ARRAY": "The session list in the workspace registry is not an array ({type})",
|
|
164
|
+
"err.E_ARCHIVED_NOT_ARRAY": "The archived list is not an array ({type})",
|
|
165
|
+
"err.E_REGISTRY_READ_FAILED": "Failed to read the workspace registry: {reason}",
|
|
166
|
+
"err.E_PROTECTED_LIST_UNREADABLE": "Cannot read the protected session list ({reason}); cascading deletion of subagent sessions was abandoned to avoid deleting the wrong sessions",
|
|
167
|
+
"err.E_INDEX_UNREADABLE": "{reason}; cascading deletion of subagent sessions was abandoned to avoid deleting the wrong sessions",
|
|
168
|
+
"err.E_CSCRIPT_MISSING": "The system script host cscript.exe was not found",
|
|
169
|
+
"err.E_DIR_OUTSIDE_ROOT": "Refused: a directory to delete is outside the sessions root",
|
|
170
|
+
"err.E_RECYCLE_SPAWN_FAILED": "Failed to invoke the recycle bin: {reason}",
|
|
171
|
+
"err.E_RECYCLE_NO_RESULT": "The recycle bin returned no result; the script did not finish",
|
|
172
|
+
"err.E_RECYCLE_DISABLED": "The recycle bin is disabled on this machine (\"Don't move files to the Recycle Bin. Remove files immediately when deleted\" is selected), so the plugin refuses to delete rather than leave the data unrestorable",
|
|
173
|
+
"err.E_RECYCLE_UNRECOGNIZED": "The result returned by the recycle bin could not be recognised",
|
|
174
|
+
"err.E_RECYCLE_LEFT": "Of {total} session directories, {moved} were moved to the recycle bin and {left} could not be moved",
|
|
175
|
+
"err.E_RECYCLE_LEFT_REASON": "Of {total} session directories, {moved} were moved to the recycle bin and {left} could not be moved: {reason}",
|
|
176
|
+
"err.E_RECYCLE_MISMATCH": "Only {matched}/{total} sessions were found in the recycle bin; some directories may have been permanently deleted, so check the recycle bin immediately",
|
|
177
|
+
"err.E_WS_DETACH_FAILED": "Failed to detach from the workspace: {reason}",
|
|
178
|
+
"err.E_DETACH_FAILED": "Session {id} could not be detached from the workspace: {reason}",
|
|
179
|
+
"err.E_ABORTED_NO_CHANGE": "{reason}; deletion was abandoned and no data was changed",
|
|
180
|
+
"err.E_NOT_FOUND": "Session not found",
|
|
181
|
+
"err.E_MOVE_ABORTED": "{reason}; the operation was stopped and no records were deleted",
|
|
182
|
+
"err.E_LOOPBACK_ONLY": "Refused: this endpoint accepts loopback requests only",
|
|
183
|
+
"err.E_POST_ONLY": "POST only",
|
|
184
|
+
"err.E_BODY_TOO_LARGE": "Request body too large",
|
|
185
|
+
"err.E_BODY_READ_FAILED": "Failed to read the request body",
|
|
186
|
+
"err.E_SESSION_ID_REQUIRED": "Missing session id",
|
|
187
|
+
"err.E_SESSION_ID_INVALID": "Invalid session id format: {id}",
|
|
188
|
+
"err.E_INTERNAL": "Internal server error: {reason}",
|
|
189
|
+
"leftover.L_SESSION_DIR": "session directory",
|
|
190
|
+
"leftover.L_PROJ_CACHE": "projection cache",
|
|
191
|
+
"leftover.L_WORKSPACE_REGISTRY": "workspace registry",
|
|
192
|
+
"leftover.L_ARCHIVE_SET": "archived set",
|
|
193
|
+
"leftover.L_SESSION_LEFTOVER": "Session {id}: {what}"
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// 文案跟随 DSH 界面语言(官方 locale 服务);服务缺失时退回中文,不因取不到而报错。
|
|
198
|
+
var i18n = { service: null, active: "zh", revision: 0, listeners: [] };
|
|
199
|
+
|
|
200
|
+
function i18nNotify() {
|
|
201
|
+
i18n.revision += 1;
|
|
202
|
+
var list = i18n.listeners.slice();
|
|
203
|
+
for (var i = 0; i < list.length; i++) {
|
|
204
|
+
try { list[i](); } catch (e) { }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function i18nSubscribe(fn) {
|
|
209
|
+
i18n.listeners.push(fn);
|
|
210
|
+
return function () {
|
|
211
|
+
var at = i18n.listeners.indexOf(fn);
|
|
212
|
+
if (at >= 0) i18n.listeners.splice(at, 1);
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function i18nReadActive() {
|
|
217
|
+
var service = i18n.service;
|
|
218
|
+
if (!service) return "zh";
|
|
219
|
+
try {
|
|
220
|
+
var snapshot = typeof service.getLocale === "function" ? service.getLocale() : null;
|
|
221
|
+
var active = snapshot && snapshot.active;
|
|
222
|
+
if (typeof active === "string" && active !== "") {
|
|
223
|
+
if (DICT[active]) return active;
|
|
224
|
+
if (active.indexOf("zh") === 0) return "zh";
|
|
225
|
+
return "en";
|
|
226
|
+
}
|
|
227
|
+
} catch (e) { }
|
|
228
|
+
return "zh";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function i18nSync() {
|
|
232
|
+
var next = i18nReadActive();
|
|
233
|
+
if (next === i18n.active) return;
|
|
234
|
+
i18n.active = next;
|
|
235
|
+
i18nNotify();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function t(key, params) {
|
|
239
|
+
var table = DICT[i18n.active] || DICT.zh;
|
|
240
|
+
// 词表是普通对象,键可能撞上原型链上的名字(如 constructor),故一律走自有属性判定。
|
|
241
|
+
var text = Object.prototype.hasOwnProperty.call(table, key) ? table[key] : undefined;
|
|
242
|
+
if (text === undefined) {
|
|
243
|
+
text = Object.prototype.hasOwnProperty.call(DICT.zh, key) ? DICT.zh[key] : undefined;
|
|
244
|
+
}
|
|
245
|
+
if (text === undefined) return key;
|
|
246
|
+
if (params) {
|
|
247
|
+
text = text.replace(/\{(\w+)\}/g, function (whole, name) {
|
|
248
|
+
var v = params[name];
|
|
249
|
+
return v === undefined || v === null ? whole : String(v);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return text;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function hasKey(key) {
|
|
256
|
+
var table = DICT[i18n.active] || DICT.zh;
|
|
257
|
+
return Object.prototype.hasOwnProperty.call(table, key) || Object.prototype.hasOwnProperty.call(DICT.zh, key);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 服务端的失败是「错误码 + 参数」;嵌套原因(reason)本身也可能是失败对象,故递归展开。
|
|
261
|
+
function renderFailure(value) {
|
|
262
|
+
if (value === null || value === undefined) return "";
|
|
263
|
+
if (typeof value === "string") {
|
|
264
|
+
// 参数里也可能直接嵌一个裸码(而非对象),认得出来就一并取词。
|
|
265
|
+
var bare = /^(E|L)_[A-Z0-9_]+$/.test(value) ? (value.indexOf("L_") === 0 ? "leftover." : "err.") + value : null;
|
|
266
|
+
return bare !== null && hasKey(bare) ? t(bare) : value;
|
|
267
|
+
}
|
|
268
|
+
if (typeof value !== "object") return String(value);
|
|
269
|
+
var code = value.code;
|
|
270
|
+
if (typeof code === "string" && code !== "") {
|
|
271
|
+
var key = (code.indexOf("L_") === 0 ? "leftover." : "err.") + code;
|
|
272
|
+
if (!hasKey(key)) return typeof value.message === "string" ? value.message : code;
|
|
273
|
+
var src = value.params || {};
|
|
274
|
+
var params = {};
|
|
275
|
+
for (var name in src) {
|
|
276
|
+
if (Object.prototype.hasOwnProperty.call(src, name)) params[name] = renderFailure(src[name]);
|
|
277
|
+
}
|
|
278
|
+
return t(key, params);
|
|
279
|
+
}
|
|
280
|
+
if (typeof value.error === "string" && value.error !== "") return value.error;
|
|
281
|
+
return "";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 浮层挂在自己另起的 React 根上(不经官方插槽),故自行订阅语言变化后重渲染。
|
|
285
|
+
function useI18nRevision() {
|
|
286
|
+
var state = react.useState(i18n.revision);
|
|
287
|
+
var setValue = state[1];
|
|
288
|
+
react.useEffect(function () {
|
|
289
|
+
return i18nSubscribe(function () { setValue(i18n.revision); });
|
|
290
|
+
}, []);
|
|
291
|
+
return state[0];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var CSS_TEXT = [
|
|
295
|
+
".dsh-session-remover-dialog{width:min(460px,100%)}",
|
|
296
|
+
".dsh-session-remover-toolbar{display:flex;align-items:center;gap:8px;",
|
|
297
|
+
"font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#5f6570)}",
|
|
298
|
+
".dsh-session-remover-list{margin-top:10px;max-height:min(46vh,320px);overflow:auto;",
|
|
299
|
+
"border:0.5px solid var(--dsw-alias-border-l2,rgba(0,0,0,.12));border-radius:10px;padding:4px}",
|
|
300
|
+
".dsh-session-remover-row{display:flex;align-items:flex-start;border-radius:8px;padding:4px 6px}",
|
|
301
|
+
".dsh-session-remover-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.04))}",
|
|
302
|
+
".dsh-session-remover-row>label{width:100%;align-items:flex-start}",
|
|
303
|
+
".dsh-session-remover-rowmain{display:flex;flex-direction:column;min-width:0}",
|
|
304
|
+
".dsh-session-remover-title{font-size:13px;line-height:20px;overflow-wrap:anywhere}",
|
|
305
|
+
".dsh-session-remover-meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center;",
|
|
306
|
+
"font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8b9098)}",
|
|
307
|
+
".dsh-session-remover-tag{border:0.5px solid var(--dsw-alias-border-l3,rgba(0,0,0,.2));",
|
|
308
|
+
"border-radius:4px;padding:0 4px}",
|
|
309
|
+
".dsh-session-remover-note{margin:10px 0 0;font-size:12px;line-height:18px;",
|
|
310
|
+
"color:var(--dsw-alias-state-warn-label,#8a6d00)}",
|
|
311
|
+
".dsh-session-remover-empty{margin:12px 0;font-size:13px;line-height:20px;",
|
|
312
|
+
"color:var(--dsw-alias-label-tertiary,#8b9098)}",
|
|
313
|
+
".dsh-session-remover-report{margin-top:8px;font-size:12px;line-height:18px}",
|
|
314
|
+
".dsh-session-remover-report p{margin:0 0 6px}",
|
|
315
|
+
".dsh-session-remover-ok{color:var(--dsw-alias-state-success-primary,#1a7f37)}",
|
|
316
|
+
".dsh-session-remover-bad{color:var(--dsw-alias-state-error-primary,#e5484d)}",
|
|
317
|
+
".dsh-session-remover-progress{font-size:13px;line-height:20px;overflow-wrap:anywhere}",
|
|
318
|
+
".dsh-session-remover-danger{background:var(--dsw-alias-state-error-primary,#e5484d)!important;",
|
|
319
|
+
"color:#fff!important;border-color:transparent!important}",
|
|
320
|
+
".dsh-session-remover-fallback-mask{position:fixed;inset:0;z-index:1000;display:flex;",
|
|
321
|
+
"align-items:center;justify-content:center;padding:24px;",
|
|
322
|
+
"background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.35))}",
|
|
323
|
+
".dsh-session-remover-fallback-card{box-sizing:border-box;width:min(460px,100%);",
|
|
324
|
+
"max-height:80vh;overflow:auto;padding:20px 24px;border-radius:16px;",
|
|
325
|
+
"background:var(--dsw-alias-bg-layer-2,#fff);box-shadow:var(--dsw-elevation-prominent,0 8px 30px rgba(0,0,0,.2))}",
|
|
326
|
+
".dsh-session-remover-fallback-card h2{margin:0 0 10px;font-size:16px;font-weight:500}",
|
|
327
|
+
".dsh-session-remover-fallback-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}",
|
|
328
|
+
""
|
|
329
|
+
].join("");
|
|
330
|
+
|
|
331
|
+
function warn(message, error) {
|
|
332
|
+
try {
|
|
333
|
+
console.warn("[dsh-session-remover] " + message, error === undefined ? "" : error);
|
|
334
|
+
} catch (e) { }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 确认框不可用时按「不删」处理:不可逆操作的安全阀失效方向必须是拒绝。
|
|
338
|
+
function confirmDelete(message) {
|
|
339
|
+
try { return window.confirm(message) === true; } catch (e) { return false; }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function requestDelete(sessionId, displayTitle) {
|
|
343
|
+
var shown = displayTitle || sessionId || t("common.untitled");
|
|
344
|
+
if (!confirmDelete(t("confirm.delete", { title: shown }))) return;
|
|
345
|
+
if (!sessionId) {
|
|
346
|
+
window.alert(t("error.noSessionId"));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
var response = await fetch("/api/session.delete", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "content-type": "application/json" },
|
|
353
|
+
body: JSON.stringify({ sessionId: sessionId })
|
|
354
|
+
});
|
|
355
|
+
var result = await response.json().catch(() => ({}));
|
|
356
|
+
if (!response.ok || result.ok !== true) {
|
|
357
|
+
window.alert(renderFailure(result) || t("error.http", { status: response.status }));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// 残留(leftover)与警告都要报出来,只报 warnings 会让「有记录没清掉」静默消失。
|
|
361
|
+
var notes = collectNotes(result);
|
|
362
|
+
if (notes.length > 0) {
|
|
363
|
+
window.alert(t("error.leftover", { list: notes.map(renderFailure).join("\n") }));
|
|
364
|
+
}
|
|
365
|
+
window.location.reload();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
window.alert(t("error.request", { reason: (error && error.message) || error }));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 残留与警告合并成一串:两者都表示「目录已移走,但本地还有东西没清干净」。
|
|
372
|
+
function collectNotes(result) {
|
|
373
|
+
var notes = [];
|
|
374
|
+
if (result && Array.isArray(result.warnings)) notes = notes.concat(result.warnings);
|
|
375
|
+
if (result && Array.isArray(result.leftover)) notes = notes.concat(result.leftover);
|
|
376
|
+
return notes;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function postSessionDelete(sessionId) {
|
|
380
|
+
if (!sessionId) throw new Error(t("error.noSessionIdShort"));
|
|
381
|
+
var response = await fetch(BATCH_ENDPOINT, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "content-type": "application/json" },
|
|
384
|
+
body: JSON.stringify({ sessionId: sessionId })
|
|
385
|
+
});
|
|
386
|
+
var result = await response.json().catch(() => ({}));
|
|
387
|
+
if (!response.ok || result.ok !== true) {
|
|
388
|
+
throw new Error(renderFailure(result) || t("error.http", { status: response.status }));
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function readBatchSources(ctx) {
|
|
394
|
+
var sessions = null;
|
|
395
|
+
var workspaces = null;
|
|
396
|
+
try { sessions = ctx.get("sessions") || null; } catch (e) { sessions = null; }
|
|
397
|
+
try { workspaces = ctx.get("workspaces") || null; } catch (e) { workspaces = null; }
|
|
398
|
+
return {
|
|
399
|
+
sessionList: sessions && sessions.list ? sessions.list : null,
|
|
400
|
+
workspaceList: workspaces && workspaces.list ? workspaces.list : null
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function useSnapshotSource(source, fallback) {
|
|
405
|
+
var state = react.useState(function () {
|
|
406
|
+
try { return source ? source.getSnapshot() : fallback; } catch (e) { return fallback; }
|
|
407
|
+
});
|
|
408
|
+
var value = state[0];
|
|
409
|
+
var setValue = state[1];
|
|
410
|
+
var lastRef = react.useRef(value);
|
|
411
|
+
react.useEffect(function () {
|
|
412
|
+
if (!source || typeof source.subscribe !== "function") return undefined;
|
|
413
|
+
var read = function () {
|
|
414
|
+
var next;
|
|
415
|
+
try { next = source.getSnapshot(); } catch (e) { return; }
|
|
416
|
+
if (next === lastRef.current) return;
|
|
417
|
+
lastRef.current = next;
|
|
418
|
+
setValue(next);
|
|
419
|
+
};
|
|
420
|
+
read();
|
|
421
|
+
var unsubscribe = source.subscribe(read);
|
|
422
|
+
return function () {
|
|
423
|
+
if (typeof unsubscribe === "function") {
|
|
424
|
+
try { unsubscribe(); } catch (e) { }
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}, [source]);
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// origin === "subagent" 过滤是安全关键:漏了会把子代理会话列给用户。
|
|
432
|
+
function selectBatchRows(listState, workspaceState, currentId) {
|
|
433
|
+
var rows = [];
|
|
434
|
+
if (!listState || !listState.byId) return rows;
|
|
435
|
+
var archived = {};
|
|
436
|
+
var archivedIds = (workspaceState && workspaceState.archivedSessionIds) || [];
|
|
437
|
+
for (var a = 0; a < archivedIds.length; a++) archived[archivedIds[a]] = true;
|
|
438
|
+
|
|
439
|
+
var workspaceTitle = {};
|
|
440
|
+
var workspaceOrder = [];
|
|
441
|
+
var items = (workspaceState && workspaceState.items) || [];
|
|
442
|
+
for (var i = 0; i < items.length; i++) {
|
|
443
|
+
var item = items[i];
|
|
444
|
+
var memberIds = (item && item.sessionIds) || [];
|
|
445
|
+
for (var j = 0; j < memberIds.length; j++) {
|
|
446
|
+
var memberId = memberIds[j];
|
|
447
|
+
if (workspaceTitle[memberId] === undefined) {
|
|
448
|
+
workspaceTitle[memberId] = (item && item.title) || "";
|
|
449
|
+
workspaceOrder.push(memberId);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
var seen = {};
|
|
455
|
+
function pushId(id) {
|
|
456
|
+
if (id === undefined || id === null || seen[id] === true) return;
|
|
457
|
+
seen[id] = true;
|
|
458
|
+
var summary = listState.byId[id];
|
|
459
|
+
if (!summary) return;
|
|
460
|
+
if (summary.origin === "subagent") return;
|
|
461
|
+
if (summary.blank === true) return;
|
|
462
|
+
if (archived[id] === true) return;
|
|
463
|
+
var title = summary.displayTitle || summary.title || id;
|
|
464
|
+
rows.push({
|
|
465
|
+
id: id,
|
|
466
|
+
title: title,
|
|
467
|
+
workspace: workspaceTitle[id] || "",
|
|
468
|
+
fork: typeof summary.parentId === "string" && summary.parentId.length > 0,
|
|
469
|
+
current: id === currentId
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for (var k = 0; k < workspaceOrder.length; k++) pushId(workspaceOrder[k]);
|
|
474
|
+
var ids = Array.isArray(listState.ids) ? listState.ids : Object.keys(listState.byId);
|
|
475
|
+
for (var m = 0; m < ids.length; m++) pushId(ids[m]);
|
|
476
|
+
return rows;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function currentSessionId(listState) {
|
|
480
|
+
if (!listState || !listState.byId) return undefined;
|
|
481
|
+
var values = Object.keys(listState.byId).map(function (key) { return listState.byId[key]; });
|
|
482
|
+
for (var i = 0; i < values.length; i++) {
|
|
483
|
+
var summary = values[i];
|
|
484
|
+
if (summary && summary.retainedBy && (summary.retainedBy.mainView || 0) > 0) return summary.id;
|
|
485
|
+
}
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 官方组件(Button/Checkbox/Modal)取不到时有朴素兜底,键盘与语义仍可用。
|
|
490
|
+
function uiButton(props) {
|
|
491
|
+
if (Button) return react.createElement(Button, props);
|
|
492
|
+
return react.createElement("button", {
|
|
493
|
+
key: props.key,
|
|
494
|
+
type: "button",
|
|
495
|
+
disabled: props.disabled === true,
|
|
496
|
+
onClick: props.onClick,
|
|
497
|
+
className: props.className,
|
|
498
|
+
style: {
|
|
499
|
+
height: 28, padding: "0 12px", borderRadius: 14, cursor: props.disabled ? "default" : "pointer",
|
|
500
|
+
border: "0.5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.2))",
|
|
501
|
+
background: "transparent", color: "inherit", font: "inherit", fontSize: 13
|
|
502
|
+
}
|
|
503
|
+
}, props.children);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function uiCheckbox(props) {
|
|
507
|
+
if (Checkbox) return react.createElement(Checkbox, props);
|
|
508
|
+
return react.createElement("label", { style: { display: "flex", alignItems: "flex-start", gap: 8, cursor: "pointer" } }, [
|
|
509
|
+
react.createElement("input", {
|
|
510
|
+
key: "input",
|
|
511
|
+
type: "checkbox",
|
|
512
|
+
checked: props.checked === true,
|
|
513
|
+
disabled: props.disabled === true,
|
|
514
|
+
onChange: function (event) { props.onChange(event.target.checked); }
|
|
515
|
+
}),
|
|
516
|
+
react.createElement("span", { key: "label", style: { minWidth: 0 } }, props.label)
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function uiModal(props) {
|
|
521
|
+
if (Modal) {
|
|
522
|
+
return react.createElement(Modal, {
|
|
523
|
+
open: true,
|
|
524
|
+
onClose: props.onClose,
|
|
525
|
+
title: props.title,
|
|
526
|
+
closeLabel: props.closeLabel,
|
|
527
|
+
description: props.description,
|
|
528
|
+
className: "dsh-session-remover-dialog",
|
|
529
|
+
children: props.children,
|
|
530
|
+
footer: props.footer
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
return react.createElement("div", {
|
|
534
|
+
className: "dsh-session-remover-fallback-mask",
|
|
535
|
+
onClick: function () { props.onClose(); }
|
|
536
|
+
}, react.createElement("div", {
|
|
537
|
+
role: "dialog",
|
|
538
|
+
"aria-modal": "true",
|
|
539
|
+
"aria-label": props.title,
|
|
540
|
+
className: "dsh-session-remover-fallback-card",
|
|
541
|
+
onClick: function (event) { event.stopPropagation(); }
|
|
542
|
+
}, [
|
|
543
|
+
react.createElement("h2", { key: "title" }, props.title),
|
|
544
|
+
props.description ? react.createElement("p", { key: "desc", style: { margin: "0 0 8px", fontSize: 13 } }, props.description) : null,
|
|
545
|
+
react.createElement("div", { key: "body" }, props.children),
|
|
546
|
+
react.createElement("div", { key: "footer", className: "dsh-session-remover-fallback-actions" }, props.footer)
|
|
547
|
+
]));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function BatchDeleteDialog(props) {
|
|
551
|
+
var ctx = props.ctx;
|
|
552
|
+
var onClose = props.onClose;
|
|
553
|
+
useI18nRevision();
|
|
554
|
+
var sources = readBatchSources(ctx);
|
|
555
|
+
var listState = useSnapshotSource(sources.sessionList, null);
|
|
556
|
+
var workspaceState = useSnapshotSource(sources.workspaceList, null);
|
|
557
|
+
|
|
558
|
+
var currentId = react.useMemo(function () { return currentSessionId(listState); }, [listState]);
|
|
559
|
+
var rows = react.useMemo(
|
|
560
|
+
function () { return selectBatchRows(listState, workspaceState, currentId); },
|
|
561
|
+
[listState, workspaceState, currentId]
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
var selectedState = react.useState(function () {
|
|
565
|
+
var initial = {};
|
|
566
|
+
if (props.initialId) {
|
|
567
|
+
for (var i = 0; i < rows.length; i++) {
|
|
568
|
+
if (rows[i].id === props.initialId) { initial[props.initialId] = true; break; }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return initial;
|
|
572
|
+
});
|
|
573
|
+
var selected = selectedState[0];
|
|
574
|
+
var setSelected = selectedState[1];
|
|
575
|
+
var ackState = react.useState(false);
|
|
576
|
+
var acknowledged = ackState[0];
|
|
577
|
+
var setAcknowledged = ackState[1];
|
|
578
|
+
var busyState = react.useState(false);
|
|
579
|
+
var busy = busyState[0];
|
|
580
|
+
var setBusy = busyState[1];
|
|
581
|
+
var progressState = react.useState(null);
|
|
582
|
+
var progress = progressState[0];
|
|
583
|
+
var setProgress = progressState[1];
|
|
584
|
+
var reportState = react.useState(null);
|
|
585
|
+
var report = reportState[0];
|
|
586
|
+
var setReport = reportState[1];
|
|
587
|
+
var bodyRef = react.useRef(null);
|
|
588
|
+
|
|
589
|
+
var selectedIds = Object.keys(selected);
|
|
590
|
+
var selectedCount = selectedIds.length;
|
|
591
|
+
var forkCount = 0;
|
|
592
|
+
for (var f = 0; f < rows.length; f++) if (rows[f].fork) forkCount++;
|
|
593
|
+
var currentPicked = currentId !== undefined && selected[currentId] === true;
|
|
594
|
+
|
|
595
|
+
function toggle(id) {
|
|
596
|
+
if (busy) return;
|
|
597
|
+
setSelected(function (previous) {
|
|
598
|
+
var next = {};
|
|
599
|
+
var keys = Object.keys(previous || {});
|
|
600
|
+
for (var i = 0; i < keys.length; i++) next[keys[i]] = true;
|
|
601
|
+
if (next[id] === true) delete next[id];
|
|
602
|
+
else next[id] = true;
|
|
603
|
+
return next;
|
|
604
|
+
});
|
|
605
|
+
setAcknowledged(false);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function selectAll() {
|
|
609
|
+
if (busy) return;
|
|
610
|
+
setSelected(function () {
|
|
611
|
+
var next = {};
|
|
612
|
+
for (var i = 0; i < rows.length; i++) next[rows[i].id] = true;
|
|
613
|
+
return next;
|
|
614
|
+
});
|
|
615
|
+
setAcknowledged(false);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function deselectAll() {
|
|
619
|
+
if (busy) return;
|
|
620
|
+
setSelected({});
|
|
621
|
+
setAcknowledged(false);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 关闭闸门:删除进行中(busy)时不响应关闭,等跑完。
|
|
625
|
+
function requestClose() {
|
|
626
|
+
if (busy) return;
|
|
627
|
+
onClose();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
react.useEffect(function () {
|
|
631
|
+
var onKeyDown = function (event) {
|
|
632
|
+
if (event.key === "Escape") requestClose();
|
|
633
|
+
};
|
|
634
|
+
document.addEventListener("keydown", onKeyDown);
|
|
635
|
+
return function () { document.removeEventListener("keydown", onKeyDown); };
|
|
636
|
+
}, [onClose, busy]);
|
|
637
|
+
|
|
638
|
+
react.useEffect(function () {
|
|
639
|
+
if (busy || report) return;
|
|
640
|
+
var node = bodyRef.current;
|
|
641
|
+
if (!node || typeof node.querySelector !== "function") return;
|
|
642
|
+
var input = node.querySelector('input[type="checkbox"]');
|
|
643
|
+
if (input && typeof input.focus === "function") {
|
|
644
|
+
try { input.focus(); } catch (e) { }
|
|
645
|
+
}
|
|
646
|
+
}, [busy, report]);
|
|
647
|
+
|
|
648
|
+
// 删除串行逐个请求,不并发。
|
|
649
|
+
async function runDelete(targets) {
|
|
650
|
+
if (busy || targets.length === 0) return;
|
|
651
|
+
if (!confirmDelete(t("confirm.batch", { count: targets.length }))) return;
|
|
652
|
+
|
|
653
|
+
setReport(null);
|
|
654
|
+
setBusy(true);
|
|
655
|
+
var deleted = [];
|
|
656
|
+
var failed = null;
|
|
657
|
+
var remaining = [];
|
|
658
|
+
var warnings = [];
|
|
659
|
+
for (var i = 0; i < targets.length; i++) {
|
|
660
|
+
var target = targets[i];
|
|
661
|
+
setProgress({ index: i + 1, total: targets.length, title: target.title });
|
|
662
|
+
try {
|
|
663
|
+
var done = await postSessionDelete(target.id);
|
|
664
|
+
deleted.push(target);
|
|
665
|
+
warnings = warnings.concat(collectNotes(done));
|
|
666
|
+
} catch (error) {
|
|
667
|
+
failed = { id: target.id, title: target.title, error: (error && error.message) || String(error) };
|
|
668
|
+
remaining = targets.slice(i);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
setProgress(null);
|
|
673
|
+
setBusy(false);
|
|
674
|
+
if (failed === null && deleted.length === targets.length && warnings.length === 0) {
|
|
675
|
+
window.location.reload();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
setReport({ deleted: deleted, failed: failed, remaining: remaining, warnings: warnings });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function onConfirm() {
|
|
682
|
+
var targets = [];
|
|
683
|
+
for (var i = 0; i < rows.length; i++) if (selected[rows[i].id] === true) targets.push(rows[i]);
|
|
684
|
+
void runDelete(targets);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function onContinue() {
|
|
688
|
+
if (!report) return;
|
|
689
|
+
var remaining = report.remaining;
|
|
690
|
+
var next = {};
|
|
691
|
+
for (var i = 0; i < remaining.length; i++) next[remaining[i].id] = true;
|
|
692
|
+
setSelected(next);
|
|
693
|
+
setReport(null);
|
|
694
|
+
void runDelete(remaining);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
var children = [];
|
|
698
|
+
if (busy) {
|
|
699
|
+
children.push(react.createElement("p", { key: "busy", className: "dsh-session-remover-progress" },
|
|
700
|
+
progress
|
|
701
|
+
? t("batch.deleting", { index: progress.index, total: progress.total, title: progress.title })
|
|
702
|
+
: t("batch.deletingShort")));
|
|
703
|
+
} else if (report) {
|
|
704
|
+
var reportLines = [];
|
|
705
|
+
var okList = report.deleted.length > 0
|
|
706
|
+
? t("batch.report.okColon") + report.deleted.map(function (row) { return t("fmt.quote", { title: row.title }); }).join(t("join.list"))
|
|
707
|
+
: "";
|
|
708
|
+
reportLines.push(react.createElement("p", { key: "ok", className: "dsh-session-remover-ok" },
|
|
709
|
+
t("batch.report.ok", { count: report.deleted.length, list: okList })));
|
|
710
|
+
if (report.failed) {
|
|
711
|
+
reportLines.push(react.createElement("p", { key: "bad", className: "dsh-session-remover-bad" },
|
|
712
|
+
t("batch.report.failed", { count: report.remaining.length, title: report.failed.title, reason: report.failed.error })));
|
|
713
|
+
if (report.remaining.length > 1) {
|
|
714
|
+
reportLines.push(react.createElement("p", { key: "rest", className: "dsh-session-remover-meta" },
|
|
715
|
+
t("batch.report.rest", { count: report.remaining.length - 1 })));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
reportLines.push(react.createElement("p", { key: "hint", className: "dsh-session-remover-meta" },
|
|
719
|
+
t("batch.report.hint")));
|
|
720
|
+
if (report.warnings && report.warnings.length > 0) {
|
|
721
|
+
reportLines.push(react.createElement("p", { key: "warn", className: "dsh-session-remover-note" },
|
|
722
|
+
t("batch.report.warn", { count: report.warnings.length, list: report.warnings.map(renderFailure).join(t("join.warn")) })));
|
|
723
|
+
}
|
|
724
|
+
children.push(react.createElement("div", { key: "report", className: "dsh-session-remover-report" }, reportLines));
|
|
725
|
+
} else {
|
|
726
|
+
children.push(react.createElement("div", { key: "toolbar", className: "dsh-session-remover-toolbar" }, [
|
|
727
|
+
react.createElement("span", { key: "count" }, t("batch.selected", { selected: selectedCount, total: rows.length })),
|
|
728
|
+
react.createElement("span", { key: "spacer", style: { marginLeft: "auto" } }),
|
|
729
|
+
uiButton({ key: "all", variant: "ghost", size: "sm", disabled: busy || rows.length === 0, onClick: selectAll, children: t("batch.selectAll") }),
|
|
730
|
+
uiButton({ key: "none", variant: "ghost", size: "sm", disabled: busy || selectedCount === 0, onClick: deselectAll, children: t("batch.selectNone") })
|
|
731
|
+
]));
|
|
732
|
+
children.push(react.createElement("div", { key: "list", className: "dsh-session-remover-list", ref: bodyRef },
|
|
733
|
+
!sources.sessionList
|
|
734
|
+
? react.createElement("p", { className: "dsh-session-remover-empty" },
|
|
735
|
+
t("batch.noSource"))
|
|
736
|
+
: rows.length === 0
|
|
737
|
+
? react.createElement("p", { className: "dsh-session-remover-empty" },
|
|
738
|
+
t("batch.empty"))
|
|
739
|
+
: rows.map(function (row) {
|
|
740
|
+
var meta = [];
|
|
741
|
+
if (row.workspace) meta.push(react.createElement("span", { key: "ws" }, row.workspace));
|
|
742
|
+
if (row.fork) meta.push(react.createElement("span", { key: "fork", className: "dsh-session-remover-tag" }, t("tag.fork")));
|
|
743
|
+
if (row.current) meta.push(react.createElement("span", { key: "cur", className: "dsh-session-remover-tag" }, t("tag.current")));
|
|
744
|
+
// label 传元素而非字符串:官方 Checkbox 的运行时是 jsx("span", { children: label }),
|
|
745
|
+
// 元素能正常渲染,只有这样才显示得出「标题 + 分支/当前打开」这一整行。
|
|
746
|
+
// 官方类型声明写的是 string,将来若给本插件加类型检查会在此报错,属预期差异。
|
|
747
|
+
var label = react.createElement("span", { className: "dsh-session-remover-rowmain" }, [
|
|
748
|
+
react.createElement("span", { key: "t", className: "dsh-session-remover-title" }, row.title),
|
|
749
|
+
meta.length > 0 ? react.createElement("span", { key: "m", className: "dsh-session-remover-meta" }, meta) : null
|
|
750
|
+
]);
|
|
751
|
+
return react.createElement("div", { key: row.id, className: "dsh-session-remover-row" },
|
|
752
|
+
uiCheckbox({
|
|
753
|
+
checked: selected[row.id] === true,
|
|
754
|
+
disabled: busy,
|
|
755
|
+
label: label,
|
|
756
|
+
onChange: function () { toggle(row.id); }
|
|
757
|
+
}));
|
|
758
|
+
})));
|
|
759
|
+
if (forkCount > 0) {
|
|
760
|
+
children.push(react.createElement("p", { key: "forknote", className: "dsh-session-remover-note" },
|
|
761
|
+
t("batch.forkNote", { count: forkCount })));
|
|
762
|
+
}
|
|
763
|
+
if (currentPicked) {
|
|
764
|
+
children.push(react.createElement("p", { key: "curnote", className: "dsh-session-remover-note" },
|
|
765
|
+
t("batch.currentNote")));
|
|
766
|
+
}
|
|
767
|
+
children.push(react.createElement("div", { key: "ack", style: { marginTop: 12 } },
|
|
768
|
+
uiCheckbox({
|
|
769
|
+
checked: acknowledged,
|
|
770
|
+
disabled: busy,
|
|
771
|
+
label: t("batch.ack"),
|
|
772
|
+
onChange: function (next) { setAcknowledged(next === true); }
|
|
773
|
+
})));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
var footer;
|
|
777
|
+
if (busy) {
|
|
778
|
+
footer = [uiButton({ key: "busy", variant: "outline", disabled: true, children: t("batch.deletingShort") })];
|
|
779
|
+
} else if (report) {
|
|
780
|
+
footer = [
|
|
781
|
+
uiButton({ key: "close", variant: "ghost", onClick: requestClose, children: t("common.close") }),
|
|
782
|
+
uiButton({ key: "reload", variant: "outline", onClick: function () { window.location.reload(); }, children: t("common.reload") }),
|
|
783
|
+
uiButton({
|
|
784
|
+
key: "continue",
|
|
785
|
+
variant: "primary",
|
|
786
|
+
className: "dsh-session-remover-danger",
|
|
787
|
+
disabled: report.remaining.length === 0,
|
|
788
|
+
onClick: onContinue,
|
|
789
|
+
children: t("batch.continue", { count: report.remaining.length })
|
|
790
|
+
})
|
|
791
|
+
];
|
|
792
|
+
} else {
|
|
793
|
+
footer = [
|
|
794
|
+
uiButton({ key: "cancel", variant: "ghost", onClick: requestClose, children: t("common.cancel") }),
|
|
795
|
+
uiButton({
|
|
796
|
+
key: "confirm",
|
|
797
|
+
variant: "primary",
|
|
798
|
+
className: "dsh-session-remover-danger",
|
|
799
|
+
disabled: !acknowledged || selectedCount === 0,
|
|
800
|
+
onClick: onConfirm,
|
|
801
|
+
children: t("batch.confirmDelete", { count: selectedCount })
|
|
802
|
+
})
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
var description = report
|
|
807
|
+
? undefined
|
|
808
|
+
: t("batch.description");
|
|
809
|
+
return uiModal({
|
|
810
|
+
title: t("batch.title"),
|
|
811
|
+
closeLabel: t("common.close"),
|
|
812
|
+
description: description,
|
|
813
|
+
onClose: requestClose,
|
|
814
|
+
children: children,
|
|
815
|
+
footer: footer
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// 本插件往官方侧栏插槽注册菜单项;批量浮层是自己另起的 React 根(官方没有多选插槽)。
|
|
820
|
+
var host = { ctx: null, container: null, root: null, seq: 0 };
|
|
821
|
+
|
|
822
|
+
function ensureHost() {
|
|
823
|
+
if (host.root) return host.root;
|
|
824
|
+
var container = document.createElement("div");
|
|
825
|
+
container.setAttribute("data-dsh-session-remover", "batch-dialog");
|
|
826
|
+
container.style.fontFamily = "var(--dsw-font-family, system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', sans-serif)";
|
|
827
|
+
container.style.color = "var(--dsw-alias-label-primary, #1f2329)";
|
|
828
|
+
container.style.fontSize = "14px";
|
|
829
|
+
document.body.appendChild(container);
|
|
830
|
+
host.container = container;
|
|
831
|
+
host.root = reactDomClient.createRoot(container);
|
|
832
|
+
return host.root;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function closeBatchDialog() {
|
|
836
|
+
if (!host.root) return;
|
|
837
|
+
try { host.root.render(null); } catch (e) { warn("关闭批量浮层失败", e); }
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function openBatchDialog(initialId) {
|
|
841
|
+
if (!canMountOwnRoot) {
|
|
842
|
+
window.alert(t("error.batchUnavailable"));
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
ensureHost();
|
|
847
|
+
host.seq += 1;
|
|
848
|
+
host.root.render(react.createElement(BatchDeleteDialog, {
|
|
849
|
+
key: "batch-" + host.seq,
|
|
850
|
+
ctx: host.ctx,
|
|
851
|
+
initialId: initialId,
|
|
852
|
+
onClose: closeBatchDialog
|
|
853
|
+
}));
|
|
854
|
+
} catch (e) {
|
|
855
|
+
warn("打开批量浮层失败", e);
|
|
856
|
+
window.alert(t("error.batchUnavailableShort", { reason: (e && e.message) || e }));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function disposeHost() {
|
|
861
|
+
if (host.root) {
|
|
862
|
+
try { host.root.unmount(); } catch (e) { }
|
|
863
|
+
host.root = null;
|
|
864
|
+
}
|
|
865
|
+
if (host.container && host.container.parentNode) host.container.parentNode.removeChild(host.container);
|
|
866
|
+
host.container = null;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// 插槽注册声明了 locale 命名空间,官方框架会把 t 随界面语言变化重新派发;
|
|
870
|
+
// 万一没给 t,退回模块内的同一套词表。
|
|
871
|
+
// 官方 t 查不到词条时会把 key 原样返回,故必须把「等于 key」也当作没取到,否则菜单会露出裸码。
|
|
872
|
+
function menuText(props, key) {
|
|
873
|
+
if (props && typeof props.t === "function") {
|
|
874
|
+
try {
|
|
875
|
+
var text = props.t(key);
|
|
876
|
+
if (typeof text === "string" && text !== "" && text !== key) return text;
|
|
877
|
+
} catch (e) { }
|
|
878
|
+
}
|
|
879
|
+
return t(key);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function DeleteSessionMenuItem(props) {
|
|
883
|
+
var sessionId = props.sessionId;
|
|
884
|
+
var displayTitle = props.displayTitle;
|
|
885
|
+
var useMenuOpenState = props.useMenuOpenState;
|
|
886
|
+
var label = menuText(props, "menu.delete");
|
|
887
|
+
var setMenuOpen = null;
|
|
888
|
+
if (typeof useMenuOpenState === "function") {
|
|
889
|
+
var pair = useMenuOpenState();
|
|
890
|
+
setMenuOpen = pair && pair[1];
|
|
891
|
+
}
|
|
892
|
+
var onSelect = function () {
|
|
893
|
+
if (typeof setMenuOpen === "function") {
|
|
894
|
+
try { setMenuOpen(false); } catch (e) { }
|
|
895
|
+
}
|
|
896
|
+
void requestDelete(sessionId, displayTitle);
|
|
897
|
+
};
|
|
898
|
+
if (MenuItemButton) {
|
|
899
|
+
return react.createElement(MenuItemButton, {
|
|
900
|
+
icon: IconTrash ? react.createElement(IconTrash, { size: 14 }) : null,
|
|
901
|
+
danger: true,
|
|
902
|
+
onSelect: onSelect,
|
|
903
|
+
children: label
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return react.createElement(
|
|
907
|
+
"button",
|
|
908
|
+
{ type: "button", role: "menuitem", onClick: onSelect, style: { display: "block", width: "100%", textAlign: "left", background: "transparent", border: "none", cursor: "pointer", color: "inherit", font: "inherit", padding: "6px 10px" } },
|
|
909
|
+
label
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function BatchDeleteMenuItem(props) {
|
|
914
|
+
var sessionId = props.sessionId;
|
|
915
|
+
var useMenuOpenState = props.useMenuOpenState;
|
|
916
|
+
var label = menuText(props, "menu.batch");
|
|
917
|
+
var setMenuOpen = null;
|
|
918
|
+
if (typeof useMenuOpenState === "function") {
|
|
919
|
+
var pair = useMenuOpenState();
|
|
920
|
+
setMenuOpen = pair && pair[1];
|
|
921
|
+
}
|
|
922
|
+
var onSelect = function () {
|
|
923
|
+
if (typeof setMenuOpen === "function") {
|
|
924
|
+
try { setMenuOpen(false); } catch (e) { }
|
|
925
|
+
}
|
|
926
|
+
openBatchDialog(sessionId);
|
|
927
|
+
};
|
|
928
|
+
if (MenuItemButton) {
|
|
929
|
+
return react.createElement(MenuItemButton, {
|
|
930
|
+
icon: IconCheck ? react.createElement(IconCheck, { size: 14 }) : null,
|
|
931
|
+
danger: true,
|
|
932
|
+
onSelect: onSelect,
|
|
933
|
+
children: label
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return react.createElement(
|
|
937
|
+
"button",
|
|
938
|
+
{ type: "button", role: "menuitem", onClick: onSelect, style: { display: "block", width: "100%", textAlign: "left", background: "transparent", border: "none", cursor: "pointer", color: "inherit", font: "inherit", padding: "6px 10px" } },
|
|
939
|
+
label
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function apply(ctx) {
|
|
944
|
+
var slots = null;
|
|
945
|
+
try { slots = ctx.slots || ctx.get("slots"); } catch (e) { slots = null; }
|
|
946
|
+
if (!slots || typeof slots.inject !== "function") {
|
|
947
|
+
console.warn("[dsh-session-remover] 拿不到 slots 服务,插件不启用(不影响官方功能)");
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
host.ctx = ctx;
|
|
951
|
+
|
|
952
|
+
// locale 服务软取:取不到就维持中文,不阻塞插件启用。
|
|
953
|
+
// 注册与订阅各自独立降级——订阅失败不该把已注册成功的词典一并废掉。
|
|
954
|
+
var localeService = null;
|
|
955
|
+
try { localeService = ctx.get("locale") || ctx.locale || null; } catch (e) { localeService = null; }
|
|
956
|
+
var localeReady = false;
|
|
957
|
+
if (localeService && typeof localeService.register === "function") {
|
|
958
|
+
try {
|
|
959
|
+
ctx.effect(function () { return localeService.register(NS, { zh: DICT.zh, en: DICT.en }); });
|
|
960
|
+
i18n.service = localeService;
|
|
961
|
+
i18n.active = i18nReadActive();
|
|
962
|
+
localeReady = true;
|
|
963
|
+
} catch (e) {
|
|
964
|
+
i18n.service = null;
|
|
965
|
+
warn("locale 词表注册失败,文案退回中文", e);
|
|
966
|
+
}
|
|
967
|
+
if (localeReady && typeof localeService.subscribe === "function") {
|
|
968
|
+
try {
|
|
969
|
+
ctx.effect(function () { return localeService.subscribe(i18nSync); });
|
|
970
|
+
} catch (e) {
|
|
971
|
+
warn("locale 语言变化订阅失败,浮层文案不随语言切换", e);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
// 只有 locale 服务确实就位时才声明 locale:官方在没有 locale face 时渲染该条目会抛错,
|
|
976
|
+
// 恒带 locale 会把「软取、拿不到就退回中文」的设计变成整条侧栏报错。
|
|
977
|
+
function entryOptions(id, order) {
|
|
978
|
+
return localeReady
|
|
979
|
+
? { name: SLOT, id: id, order: order, locale: NS }
|
|
980
|
+
: { name: SLOT, id: id, order: order };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (canMountOwnRoot) {
|
|
984
|
+
ctx.effect(function () {
|
|
985
|
+
var styleEl = document.createElement("style");
|
|
986
|
+
styleEl.id = STYLE_ID;
|
|
987
|
+
styleEl.textContent = CSS_TEXT;
|
|
988
|
+
document.head.appendChild(styleEl);
|
|
989
|
+
return function () {
|
|
990
|
+
if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl);
|
|
991
|
+
disposeHost();
|
|
992
|
+
};
|
|
993
|
+
});
|
|
994
|
+
} else {
|
|
995
|
+
warn("拿不到 react-dom/client 平台模块,多选批量删除不注册(单条删除照常可用)");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
ctx.effect(() => slots.inject(
|
|
1000
|
+
SLOT,
|
|
1001
|
+
() => slots.register(entryOptions(ENTRY_ID, ENTRY_ORDER), DeleteSessionMenuItem)
|
|
1002
|
+
));
|
|
1003
|
+
if (canMountOwnRoot) {
|
|
1004
|
+
ctx.effect(() => slots.inject(
|
|
1005
|
+
SLOT,
|
|
1006
|
+
() => slots.register(entryOptions(BATCH_ENTRY_ID, BATCH_ENTRY_ORDER), BatchDeleteMenuItem)
|
|
1007
|
+
));
|
|
1008
|
+
}
|
|
1009
|
+
} catch (e) {
|
|
1010
|
+
console.warn("[dsh-session-remover] 插槽注册失败(自动退化,不影响官方功能):", (e && e.message) || e);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
exports.name = "dsh-session-remover";
|
|
1015
|
+
exports.inject = ["slots"];
|
|
1016
|
+
exports.apply = apply;
|
|
1017
|
+
return module.exports;
|
|
1018
|
+
}
|
|
1019
|
+
});
|