dsh-archived-chats 0.4.0 → 0.5.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/README.md +12 -1
- package/README.zh.md +12 -1
- package/lib/client.js +310 -42
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-archived-chats
|
|
2
2
|
|
|
3
|
+
> ⚡ **Deletion takes effect immediately — no restart.** Even sessions still resident in the background are torn down safely along the official lifecycle and wiped from disk the moment you click delete, instead of being "parked until the next restart".
|
|
4
|
+
|
|
3
5
|
A settings page for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) that brings archived chats back into view.
|
|
4
6
|
|
|
5
7
|
Once a conversation is archived in DeepSeek Harness it disappears from the sidebar, and there is no built-in way to browse it again — only the workspace store (`~/.dsh/storages/workspace.json`) still remembers it. This plugin adds an **Archived Chats** page under Settings where every archived session is visible, searchable, and manageable.
|
|
@@ -15,7 +17,8 @@ Restart DSH once after installing, then open **Settings → Archived Chats**.
|
|
|
15
17
|
## Features
|
|
16
18
|
|
|
17
19
|
- **Complete archived-session list**, grouped by workspace (project) with a per-group count. Every group can be collapsed or expanded, and the state is remembered per browser.
|
|
18
|
-
- **Search** by title,
|
|
20
|
+
- **Search and sort** by title, filter by type (all / regular / subagent) and project, then order results by newest, oldest, or title.
|
|
21
|
+
- **Flexible multi-select**: select individual chats, every visible result, or an entire project. The selection bar can unarchive or permanently delete the chosen chats in one action, while selections hidden by another filter remain intact.
|
|
19
22
|
- **Unarchive** a single chat or a whole project group from the group's `⋯` menu — restored chats reappear in the sidebar immediately.
|
|
20
23
|
- **Delete** one chat, a project group, or everything (**Delete All**), each behind a confirmation dialog. Deletion is thorough: the session log is removed from disk, the session is detached from its workspace record, and the registry's in-memory header index is purged, so the sidebar drops the rows live.
|
|
21
24
|
- Sessions still resident in the background are **deleted in place too**: the plugin disposes the session through the official lifecycle teardown order (cancel → quiesce → flush → fiber teardown → registry detach), the persistence layer releases the write path, and the physical delete completes within the same request — no restart. If the running DSH build does not expose the required internal seams, the plugin falls back to "park permanently + delete on the next start", with parked sessions staying hidden meanwhile.
|
|
@@ -29,6 +32,14 @@ Restart DSH once after installing, then open **Settings → Archived Chats**.
|
|
|
29
32
|
- **Title cache**: resolved titles are memoized per id across list refreshes instead of re-reading every archived log; delete and unarchive invalidate their entries.
|
|
30
33
|
- **Browser half** (`lib/client.js`) registers a `settings.section` slot entry (order 30) and renders the page with React and DSH design tokens.
|
|
31
34
|
|
|
35
|
+
## Development
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm test
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The smoke test uses an isolated temporary DSH home plus mocked host and browser runtimes; it never reads or changes real sessions.
|
|
42
|
+
|
|
32
43
|
## Uninstall
|
|
33
44
|
|
|
34
45
|
```sh
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-archived-chats
|
|
2
2
|
|
|
3
|
+
> ⚡ **删除即生效,无需重启。** 即使会话仍驻留在后台,也会沿官方生命周期当场安全拆除并从磁盘彻底删除——点下删除的那一刻就删干净,而不是"停用后等下次重启"。
|
|
4
|
+
|
|
3
5
|
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 新增一个「已归档的聊天」设置页,把被归档的会话重新找回来。
|
|
4
6
|
|
|
5
7
|
在 DeepSeek Harness 里,聊天一旦归档就会从侧边栏消失,界面中没有任何入口可以再看到它,只有工作区存档(`~/.dsh/storages/workspace.json`)还记得它。这个插件在「设置」中补上一个「已归档的聊天」页面,让所有归档会话都可见、可搜索、可管理。
|
|
@@ -15,7 +17,8 @@ dsh plugin --profile web add dsh-archived-chats
|
|
|
15
17
|
## 功能
|
|
16
18
|
|
|
17
19
|
- **完整归档列表**:按工作区(项目)分组并显示每组数量;每个分组都可折叠/展开,状态按浏览器记忆。
|
|
18
|
-
-
|
|
20
|
+
- **搜索与排序**:按标题搜索,用类型(全部 / 普通会话 / 子代理会话)和项目筛选,并按最新、最早或标题排序。
|
|
21
|
+
- **灵活多选**:逐条选择、选择当前筛选结果或选择整个项目;选中后可一次取消归档或永久删除,隐藏在其他筛选结果中的选择不会丢失。
|
|
19
22
|
- **取消归档**单个聊天,或从分组的 `⋯` 菜单整组取消——恢复的聊天会立刻回到侧边栏。
|
|
20
23
|
- **删除**单个聊天、某个项目分组或全部(**全部删除**),均有确认弹窗。删除是彻底的:会话日志从磁盘移除、从工作区记录中摘除、注册表内存索引同步清理,主侧边栏的条目也会立即消失。
|
|
21
24
|
- 仍驻留后台的会话也**当场删除**:插件按官方生命周期的拆除顺序原地停用并注销会话(取消 → 静默 → 落盘 → 拆纤程 → 摘出注册表),持久层随之释放写入通道,同一次请求内即完成物理删除——无需重启。若当前 DSH 版本不提供所需内部接口,则自动回退为「永久停用 + 下次启动完成删除」,停用期间会话保持隐藏。
|
|
@@ -29,6 +32,14 @@ dsh plugin --profile web add dsh-archived-chats
|
|
|
29
32
|
- **标题缓存**:列表刷新时按 id 记忆已解析的标题,不再每次全量读日志;删除与取消归档会使对应缓存失效。
|
|
30
33
|
- **浏览器半**(`lib/client.js`)注册 `settings.section` 插槽项(order 30),用 React 和 DSH 设计令牌渲染页面。
|
|
31
34
|
|
|
35
|
+
## 开发
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm test
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
测试使用隔离的临时 DSH 主目录和模拟的宿主/浏览器运行时,不会读取或修改真实会话。
|
|
42
|
+
|
|
32
43
|
## 卸载
|
|
33
44
|
|
|
34
45
|
```sh
|
package/lib/client.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// What this plugin is: one settings section — 已归档的聊天 / Archived Chats —
|
|
10
10
|
// modeled 1:1 on the CodeX archived-chats page. The stock DSH sidebar hides
|
|
11
11
|
// archived sessions with no list surface; this page restores one: search,
|
|
12
|
-
// type/project filters,
|
|
13
|
-
//
|
|
12
|
+
// type/project filters, sorting, multi-selection, unarchive, delete, and
|
|
13
|
+
// batch actions, all driven through the host half's /plugins/dsh-archived-chats/*
|
|
14
14
|
// routes (the archive set itself lives in the host's workspace registry).
|
|
15
15
|
window.__ModuleLoader__.load({
|
|
16
16
|
id: "dsh-archived-chats",
|
|
@@ -54,6 +54,15 @@ window.__ModuleLoader__.load({
|
|
|
54
54
|
"filter.normal": "普通会话",
|
|
55
55
|
"filter.subagent": "子代理会话",
|
|
56
56
|
"filter.allProjects": "所有项目",
|
|
57
|
+
"sort.label": "排序方式",
|
|
58
|
+
"sort.newest": "最新优先",
|
|
59
|
+
"sort.oldest": "最早优先",
|
|
60
|
+
"sort.title": "按标题",
|
|
61
|
+
"selection.visible": "选择当前结果",
|
|
62
|
+
"selection.group": "选择此项目",
|
|
63
|
+
"selection.clear": "清除",
|
|
64
|
+
"bulk.unarchive": "取消归档",
|
|
65
|
+
"bulk.delete": "删除",
|
|
57
66
|
"group.noProject": "未分组",
|
|
58
67
|
"chat.untitled": "未命名会话",
|
|
59
68
|
"chat.unarchive": "取消归档",
|
|
@@ -65,6 +74,7 @@ window.__ModuleLoader__.load({
|
|
|
65
74
|
"confirm.deleteOne.body": "这将永久删除已归档聊天",
|
|
66
75
|
"confirm.deleteAll.title": "删除全部已归档聊天?",
|
|
67
76
|
"confirm.deleteGroup.title": "删除该项目下的已归档聊天?",
|
|
77
|
+
"confirm.deleteSelected.title": "删除选中的已归档聊天?",
|
|
68
78
|
"confirm.cancel": "取消",
|
|
69
79
|
"confirm.delete": "删除",
|
|
70
80
|
"state.loading": "加载中…",
|
|
@@ -84,6 +94,15 @@ window.__ModuleLoader__.load({
|
|
|
84
94
|
"filter.normal": "Regular chats",
|
|
85
95
|
"filter.subagent": "Subagent chats",
|
|
86
96
|
"filter.allProjects": "All projects",
|
|
97
|
+
"sort.label": "Sort order",
|
|
98
|
+
"sort.newest": "Newest first",
|
|
99
|
+
"sort.oldest": "Oldest first",
|
|
100
|
+
"sort.title": "Title",
|
|
101
|
+
"selection.visible": "Select visible chats",
|
|
102
|
+
"selection.group": "Select this project",
|
|
103
|
+
"selection.clear": "Clear",
|
|
104
|
+
"bulk.unarchive": "Unarchive",
|
|
105
|
+
"bulk.delete": "Delete",
|
|
87
106
|
"group.noProject": "Ungrouped",
|
|
88
107
|
"chat.untitled": "Untitled chat",
|
|
89
108
|
"chat.unarchive": "Unarchive",
|
|
@@ -95,6 +114,7 @@ window.__ModuleLoader__.load({
|
|
|
95
114
|
"confirm.deleteOne.body": "This permanently deletes the archived chat",
|
|
96
115
|
"confirm.deleteAll.title": "Delete all archived chats?",
|
|
97
116
|
"confirm.deleteGroup.title": "Delete this project's archived chats?",
|
|
117
|
+
"confirm.deleteSelected.title": "Delete selected archived chats?",
|
|
98
118
|
"confirm.cancel": "Cancel",
|
|
99
119
|
"confirm.delete": "Delete",
|
|
100
120
|
"state.loading": "Loading…",
|
|
@@ -115,6 +135,14 @@ window.__ModuleLoader__.load({
|
|
|
115
135
|
const deleteGroupBody = (t, name, n) => isZh(t)
|
|
116
136
|
? `这将永久删除「${name}」下 ${n} 个已归档聊天`
|
|
117
137
|
: `This permanently deletes ${n} archived chat${n === 1 ? "" : "s"} under "${name}"`;
|
|
138
|
+
const deleteSelectedBody = (t, n) => isZh(t)
|
|
139
|
+
? `这将永久删除选中的 ${n} 个已归档聊天`
|
|
140
|
+
: `This permanently deletes the ${n} selected archived chat${n === 1 ? "" : "s"}`;
|
|
141
|
+
const selectedCount = (t, n) => isZh(t)
|
|
142
|
+
? `已选择 ${n} 个聊天`
|
|
143
|
+
: `${n} chat${n === 1 ? "" : "s"} selected`;
|
|
144
|
+
const selectChatLabel = (t, title) => isZh(t) ? `选择 ${title}` : `Select ${title}`;
|
|
145
|
+
const selectProjectLabel = (t, title) => `${t("selection.group")}${isZh(t) ? ":" : ": "}${title}`;
|
|
118
146
|
const failureText = (t, failed) => {
|
|
119
147
|
if (isZh(t)) return `${failed.length} 个会话删除失败:${failed[0]?.message ?? ""}`;
|
|
120
148
|
return `${failed.length} chat${failed.length === 1 ? "" : "s"} failed to delete: ${failed[0]?.message ?? ""}`;
|
|
@@ -148,13 +176,50 @@ window.__ModuleLoader__.load({
|
|
|
148
176
|
return new Date(ms).toLocaleString();
|
|
149
177
|
}
|
|
150
178
|
}
|
|
179
|
+
|
|
180
|
+
function sortArchivedSessions(items, mode, locale) {
|
|
181
|
+
const collator = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
|
|
182
|
+
return items.map((item, index) => ({ item, index })).sort((left, right) => {
|
|
183
|
+
const a = left.item;
|
|
184
|
+
const b = right.item;
|
|
185
|
+
if (mode === "title") {
|
|
186
|
+
const aTitle = typeof a.title === "string" && a.title.trim() !== "" ? a.title.trim() : null;
|
|
187
|
+
const bTitle = typeof b.title === "string" && b.title.trim() !== "" ? b.title.trim() : null;
|
|
188
|
+
if ((aTitle === null) !== (bTitle === null)) return aTitle === null ? 1 : -1;
|
|
189
|
+
if (aTitle !== null && bTitle !== null) {
|
|
190
|
+
const compared = collator.compare(aTitle, bTitle);
|
|
191
|
+
if (compared !== 0) return compared;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
const aDate = typeof a.createdAt === "number" && Number.isFinite(a.createdAt) ? a.createdAt : null;
|
|
195
|
+
const bDate = typeof b.createdAt === "number" && Number.isFinite(b.createdAt) ? b.createdAt : null;
|
|
196
|
+
if ((aDate === null) !== (bDate === null)) return aDate === null ? 1 : -1;
|
|
197
|
+
if (aDate !== null && bDate !== null && aDate !== bDate) return mode === "oldest" ? aDate - bDate : bDate - aDate;
|
|
198
|
+
}
|
|
199
|
+
return left.index - right.index;
|
|
200
|
+
}).map(({ item }) => item);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function setVisibleSelection(selected, visibleIds, checked) {
|
|
204
|
+
const next = new Set(selected);
|
|
205
|
+
for (const id of visibleIds) {
|
|
206
|
+
if (checked) next.add(id);
|
|
207
|
+
else next.delete(id);
|
|
208
|
+
}
|
|
209
|
+
return next;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function reconcileSelection(selected, sessions) {
|
|
213
|
+
const currentIds = new Set(sessions.map((session) => session.id));
|
|
214
|
+
return new Set([...selected].filter((id) => currentIds.has(id)));
|
|
215
|
+
}
|
|
151
216
|
//#endregion
|
|
152
217
|
|
|
153
218
|
//#region styles
|
|
154
219
|
const CSS = `
|
|
155
220
|
.dac-page{position:relative;display:flex;flex-direction:column;gap:14px;padding:4px 0 28px;font-family:inherit}
|
|
156
221
|
.dac-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
157
|
-
.dac-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:500;line-height:28px}
|
|
222
|
+
.dac-title{margin:0;color:var(--dsw-alias-label-primary);font-size:18px;font-weight:500;line-height:28px;outline:none}
|
|
158
223
|
.dac-deleteall{display:inline-flex;align-items:center;gap:6px;border:none;border-radius:999px;padding:6px 14px;background:rgba(229,72,77,.1);color:#e5484d;font:inherit;font-size:13px;line-height:20px;cursor:pointer;transition:background .15s}
|
|
159
224
|
.dac-deleteall:hover:not(:disabled){background:rgba(229,72,77,.18)}
|
|
160
225
|
.dac-deleteall:disabled{opacity:.45;cursor:default}
|
|
@@ -167,8 +232,19 @@ window.__ModuleLoader__.load({
|
|
|
167
232
|
.dac-select{appearance:none;-webkit-appearance:none;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:5px 28px 5px 14px;cursor:pointer;outline:none}
|
|
168
233
|
.dac-select:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
169
234
|
.dac-chevron{position:absolute;right:10px;pointer-events:none;color:var(--dsw-alias-label-tertiary);display:inline-flex}
|
|
235
|
+
.dac-selection-toggle{display:inline-flex;align-items:center;gap:7px;margin-left:auto;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;cursor:pointer;user-select:none}
|
|
236
|
+
.dac-checkbox{width:16px;height:16px;margin:0;accent-color:var(--dsw-alias-interactive-primary,#6e6ef7);cursor:pointer;flex:none}
|
|
237
|
+
.dac-checkbox:disabled{cursor:default;opacity:.45}
|
|
238
|
+
.dac-bulkbar{position:sticky;top:8px;z-index:30;display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 35%,var(--dsw-alias-border-l2));border-radius:12px;background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 8%,var(--dsw-alias-bg-layer-2));padding:9px 12px;box-shadow:0 5px 18px rgba(0,0,0,.08)}
|
|
239
|
+
.dac-bulk-count{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}
|
|
240
|
+
.dac-bulk-actions{display:flex;align-items:center;gap:6px}
|
|
241
|
+
.dac-bulk-btn{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;padding:5px 11px;cursor:pointer}
|
|
242
|
+
.dac-bulk-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
243
|
+
.dac-bulk-btn.dac-danger{border-color:rgba(229,72,77,.28);color:#e5484d;background:rgba(229,72,77,.08)}
|
|
244
|
+
.dac-bulk-btn:disabled{opacity:.45;cursor:default}
|
|
170
245
|
.dac-group{display:flex;flex-direction:column;gap:8px;margin-top:6px}
|
|
171
246
|
.dac-group-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px}
|
|
247
|
+
.dac-group-left{display:flex;align-items:center;gap:8px;min-width:0}
|
|
172
248
|
.dac-group-toggle{display:inline-flex;align-items:center;gap:7px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;line-height:20px;cursor:pointer;padding:3px 6px;margin:-3px -6px;border-radius:7px}
|
|
173
249
|
.dac-group-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
174
250
|
.dac-chev{display:inline-flex;color:var(--dsw-alias-label-tertiary);transform:rotate(-90deg);transition:transform .15s}
|
|
@@ -186,8 +262,10 @@ window.__ModuleLoader__.load({
|
|
|
186
262
|
.dac-menu-item.dac-danger{color:#e5484d}
|
|
187
263
|
.dac-menu-item.dac-danger:hover{background:rgba(229,72,77,.1)}
|
|
188
264
|
.dac-list{display:flex;flex-direction:column;gap:8px}
|
|
189
|
-
.dac-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:12px 16px;transition:border-color .15s}
|
|
265
|
+
.dac-row{display:flex;align-items:center;justify-content:space-between;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:12px 16px;transition:border-color .15s,background .15s}
|
|
190
266
|
.dac-row:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
267
|
+
.dac-row.dac-selected{border-color:color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 45%,var(--dsw-alias-border-l2));background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#6e6ef7) 6%,var(--dsw-alias-bg-layer-1))}
|
|
268
|
+
.dac-row-select{display:flex;align-items:center;gap:11px;min-width:0;flex:1}
|
|
191
269
|
.dac-row-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
192
270
|
.dac-row-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
193
271
|
.dac-row-date{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
@@ -207,7 +285,8 @@ window.__ModuleLoader__.load({
|
|
|
207
285
|
@keyframes dac-toast-in{from{opacity:0;transform:translateX(-50%) translateY(-10px) scale(.96)}to{opacity:1;transform:translateX(-50%) translateY(0) scale(1)}}
|
|
208
286
|
@media (prefers-reduced-motion:reduce){.dac-toast{animation:none}}
|
|
209
287
|
.dac-confirm-overlay{position:fixed;inset:0;z-index:1400;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}
|
|
210
|
-
.dac-confirm{width:min(400px,calc(100vw - 64px));display:flex;flex-direction:column;gap:12px;border-radius:16px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 16px 64px rgba(0,0,0,.3);padding:20px}
|
|
288
|
+
.dac-confirm{width:min(400px,calc(100vw - 64px));display:flex;flex-direction:column;gap:12px;border-radius:16px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 16px 64px rgba(0,0,0,.3);padding:20px;outline:none}
|
|
289
|
+
.dac-confirm:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-interactive-primary,#6e6ef7),0 16px 64px rgba(0,0,0,.3)}
|
|
211
290
|
.dac-confirm-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:500;line-height:24px}
|
|
212
291
|
.dac-confirm-body{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:21px}
|
|
213
292
|
.dac-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
|
|
@@ -218,6 +297,7 @@ window.__ModuleLoader__.load({
|
|
|
218
297
|
.dac-btn-danger:disabled{opacity:.5;cursor:default}
|
|
219
298
|
.dac-spin{display:inline-block;width:14px;height:14px;border:2px solid var(--dsw-alias-label-tertiary);border-top-color:transparent;border-radius:50%;animation:dac-rotate .8s linear infinite}
|
|
220
299
|
@keyframes dac-rotate{to{transform:rotate(360deg)}}
|
|
300
|
+
@media (max-width:640px){.dac-selection-toggle{margin-left:0}.dac-bulkbar{align-items:flex-start;flex-direction:column}.dac-bulk-actions{width:100%;flex-wrap:wrap}.dac-row{align-items:flex-start}.dac-row-actions{gap:4px}.dac-unarchive{padding:5px 10px}}
|
|
221
301
|
`;
|
|
222
302
|
|
|
223
303
|
function ensureStyle() {
|
|
@@ -351,27 +431,85 @@ window.__ModuleLoader__.load({
|
|
|
351
431
|
});
|
|
352
432
|
}
|
|
353
433
|
|
|
354
|
-
function
|
|
434
|
+
function SelectionCheckbox({ checked, indeterminate = false, disabled = false, ariaLabel, onChange }) {
|
|
435
|
+
const inputRef = _react.useRef(null);
|
|
355
436
|
_react.useEffect(() => {
|
|
356
|
-
|
|
437
|
+
if (inputRef.current !== null) inputRef.current.indeterminate = indeterminate;
|
|
438
|
+
}, [indeterminate]);
|
|
439
|
+
return (0, jsx.jsx)("input", {
|
|
440
|
+
ref: inputRef,
|
|
441
|
+
type: "checkbox",
|
|
442
|
+
className: "dac-checkbox",
|
|
443
|
+
checked,
|
|
444
|
+
disabled,
|
|
445
|
+
"aria-label": ariaLabel,
|
|
446
|
+
"aria-checked": indeterminate ? "mixed" : checked,
|
|
447
|
+
onChange: (event) => onChange(event.target.checked)
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function ConfirmDialog({ title, body, confirmLabel, cancelLabel, busy, returnFocus, fallbackFocusRef, onConfirm, onCancel }) {
|
|
452
|
+
const dialogRef = _react.useRef(null);
|
|
453
|
+
const cancelRef = _react.useRef(null);
|
|
454
|
+
_react.useEffect(() => {
|
|
455
|
+
const previousFocus = returnFocus ?? document.activeElement;
|
|
456
|
+
const dialog = dialogRef.current;
|
|
457
|
+
(cancelRef.current ?? dialog)?.focus?.();
|
|
458
|
+
const onKey = (e) => {
|
|
459
|
+
if (e.key === "Escape") {
|
|
460
|
+
e.preventDefault?.();
|
|
461
|
+
onCancel();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (e.key !== "Tab" || dialog === null) return;
|
|
465
|
+
const focusable = [...dialog.querySelectorAll('button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')];
|
|
466
|
+
if (focusable.length === 0) {
|
|
467
|
+
e.preventDefault();
|
|
468
|
+
dialog.focus?.();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const first = focusable[0];
|
|
472
|
+
const last = focusable[focusable.length - 1];
|
|
473
|
+
const active = document.activeElement;
|
|
474
|
+
const focusIsInside = typeof dialog.contains === "function" ? dialog.contains(active) : focusable.includes(active);
|
|
475
|
+
if (e.shiftKey && (active === first || !focusIsInside)) {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
last.focus();
|
|
478
|
+
} else if (!e.shiftKey && (active === last || !focusIsInside)) {
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
first.focus();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
357
483
|
document.addEventListener("keydown", onKey);
|
|
358
|
-
return () =>
|
|
359
|
-
|
|
484
|
+
return () => {
|
|
485
|
+
document.removeEventListener("keydown", onKey);
|
|
486
|
+
const previousIsUsable = previousFocus !== null
|
|
487
|
+
&& previousFocus !== document.body
|
|
488
|
+
&& typeof previousFocus?.focus === "function"
|
|
489
|
+
&& (typeof document.contains !== "function" || document.contains(previousFocus));
|
|
490
|
+
const target = previousIsUsable ? previousFocus : fallbackFocusRef?.current;
|
|
491
|
+
target?.focus?.();
|
|
492
|
+
};
|
|
493
|
+
}, [onCancel, returnFocus, fallbackFocusRef]);
|
|
360
494
|
return (0, jsx.jsx)("div", {
|
|
361
495
|
className: "dac-confirm-overlay",
|
|
362
496
|
onClick: onCancel,
|
|
363
497
|
children: (0, jsx.jsxs)("div", {
|
|
498
|
+
ref: dialogRef,
|
|
364
499
|
className: "dac-confirm",
|
|
365
500
|
role: "alertdialog",
|
|
366
501
|
"aria-modal": "true",
|
|
502
|
+
"aria-labelledby": "dac-confirm-title",
|
|
503
|
+
"aria-describedby": "dac-confirm-body",
|
|
504
|
+
tabIndex: -1,
|
|
367
505
|
onClick: (e) => e.stopPropagation(),
|
|
368
506
|
children: [
|
|
369
|
-
(0, jsx.jsx)("div", { className: "dac-confirm-title", children: title }),
|
|
370
|
-
(0, jsx.jsx)("div", { className: "dac-confirm-body", children: body }),
|
|
507
|
+
(0, jsx.jsx)("div", { id: "dac-confirm-title", className: "dac-confirm-title", children: title }),
|
|
508
|
+
(0, jsx.jsx)("div", { id: "dac-confirm-body", className: "dac-confirm-body", children: body }),
|
|
371
509
|
(0, jsx.jsxs)("div", {
|
|
372
510
|
className: "dac-confirm-actions",
|
|
373
511
|
children: [
|
|
374
|
-
(0, jsx.jsx)("button", { type: "button", className: "dac-btn", onClick: onCancel, children: cancelLabel }),
|
|
512
|
+
(0, jsx.jsx)("button", { ref: cancelRef, type: "button", className: "dac-btn", onClick: onCancel, children: cancelLabel }),
|
|
375
513
|
(0, jsx.jsx)("button", { type: "button", className: "dac-btn-danger", disabled: busy, onClick: onConfirm, children: confirmLabel })
|
|
376
514
|
]
|
|
377
515
|
})
|
|
@@ -380,7 +518,7 @@ window.__ModuleLoader__.load({
|
|
|
380
518
|
});
|
|
381
519
|
}
|
|
382
520
|
|
|
383
|
-
function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, busy }) {
|
|
521
|
+
function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, busy, selected, onToggleSelected }) {
|
|
384
522
|
const wrapRef = _react.useRef(null);
|
|
385
523
|
_react.useEffect(() => {
|
|
386
524
|
if (!menuOpen) return void 0;
|
|
@@ -388,7 +526,11 @@ window.__ModuleLoader__.load({
|
|
|
388
526
|
document.addEventListener("mousedown", onDown);
|
|
389
527
|
return () => document.removeEventListener("mousedown", onDown);
|
|
390
528
|
}, [menuOpen, onToggleMenu]);
|
|
391
|
-
const ids = group.
|
|
529
|
+
const ids = group.selectionIds;
|
|
530
|
+
const selectedInGroup = ids.filter((id) => selected.has(id)).length;
|
|
531
|
+
const allSelected = ids.length > 0 && selectedInGroup === ids.length;
|
|
532
|
+
const someSelected = selectedInGroup > 0 && !allSelected;
|
|
533
|
+
const groupBusy = ids.some((id) => busy[id] === true);
|
|
392
534
|
return (0, jsx.jsxs)("div", {
|
|
393
535
|
className: "dac-group",
|
|
394
536
|
children: [
|
|
@@ -396,16 +538,28 @@ window.__ModuleLoader__.load({
|
|
|
396
538
|
className: "dac-group-head",
|
|
397
539
|
ref: wrapRef,
|
|
398
540
|
children: [
|
|
399
|
-
(0, jsx.jsxs)("
|
|
400
|
-
|
|
401
|
-
className: "dac-group-toggle",
|
|
402
|
-
"aria-expanded": collapsed !== true,
|
|
403
|
-
"aria-label": collapsed === true ? t("group.expand") : t("group.collapse"),
|
|
404
|
-
onClick: () => onToggleCollapsed(group.key),
|
|
541
|
+
(0, jsx.jsxs)("div", {
|
|
542
|
+
className: "dac-group-left",
|
|
405
543
|
children: [
|
|
406
|
-
(0, jsx.jsx)(
|
|
407
|
-
|
|
408
|
-
|
|
544
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
545
|
+
checked: allSelected,
|
|
546
|
+
indeterminate: someSelected,
|
|
547
|
+
disabled: groupBusy,
|
|
548
|
+
ariaLabel: selectProjectLabel(t, group.title),
|
|
549
|
+
onChange: (checked) => onToggleSelected(ids, checked)
|
|
550
|
+
}),
|
|
551
|
+
(0, jsx.jsxs)("button", {
|
|
552
|
+
type: "button",
|
|
553
|
+
className: "dac-group-toggle",
|
|
554
|
+
"aria-expanded": collapsed !== true,
|
|
555
|
+
"aria-label": collapsed === true ? t("group.expand") : t("group.collapse"),
|
|
556
|
+
onClick: () => onToggleCollapsed(group.key),
|
|
557
|
+
children: [
|
|
558
|
+
(0, jsx.jsx)("span", { className: collapsed === true ? "dac-chev" : "dac-chev open", children: (0, jsx.jsx)(IconChevron, {}) }),
|
|
559
|
+
(0, jsx.jsx)(IconFolder, {}),
|
|
560
|
+
(0, jsx.jsx)("span", { children: group.title })
|
|
561
|
+
]
|
|
562
|
+
})
|
|
409
563
|
]
|
|
410
564
|
}),
|
|
411
565
|
(0, jsx.jsxs)("div", {
|
|
@@ -446,15 +600,26 @@ window.__ModuleLoader__.load({
|
|
|
446
600
|
collapsed !== true && (0, jsx.jsx)("div", {
|
|
447
601
|
className: "dac-list",
|
|
448
602
|
children: group.items.map((session) => (0, jsx.jsxs)("div", {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
603
|
+
className: selected.has(session.id) ? "dac-row dac-selected" : "dac-row",
|
|
604
|
+
children: [
|
|
605
|
+
(0, jsx.jsxs)("label", {
|
|
606
|
+
className: "dac-row-select",
|
|
607
|
+
children: [
|
|
608
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
609
|
+
checked: selected.has(session.id),
|
|
610
|
+
disabled: busy[session.id] === true,
|
|
611
|
+
ariaLabel: selectChatLabel(t, session.title ?? t("chat.untitled")),
|
|
612
|
+
onChange: (checked) => onToggleSelected([session.id], checked)
|
|
613
|
+
}),
|
|
614
|
+
(0, jsx.jsxs)("div", {
|
|
615
|
+
className: "dac-row-main",
|
|
616
|
+
children: [
|
|
617
|
+
(0, jsx.jsx)("div", { className: "dac-row-title", children: session.title ?? t("chat.untitled") }),
|
|
618
|
+
(0, jsx.jsx)("div", { className: "dac-row-date", children: formatDate(t, session.createdAt) })
|
|
619
|
+
]
|
|
620
|
+
})
|
|
621
|
+
]
|
|
622
|
+
}),
|
|
458
623
|
(0, jsx.jsxs)("div", {
|
|
459
624
|
className: "dac-row-actions",
|
|
460
625
|
children: [
|
|
@@ -489,11 +654,15 @@ window.__ModuleLoader__.load({
|
|
|
489
654
|
const [query, setQuery] = _react.useState("");
|
|
490
655
|
const [typeFilter, setTypeFilter] = _react.useState("all");
|
|
491
656
|
const [projectFilter, setProjectFilter] = _react.useState("all");
|
|
657
|
+
const [sortMode, setSortMode] = _react.useState("newest");
|
|
492
658
|
const [menuFor, setMenuFor] = _react.useState(null);
|
|
493
659
|
const [confirm, setConfirm] = _react.useState(null);
|
|
494
660
|
const [busy, setBusy] = _react.useState({});
|
|
495
661
|
const [notice, setNotice] = _react.useState(null);
|
|
496
662
|
const [collapsed, setCollapsed] = _react.useState(readCollapsed);
|
|
663
|
+
const [selected, setSelected] = _react.useState(() => new Set());
|
|
664
|
+
const pageHeadingRef = _react.useRef(null);
|
|
665
|
+
const deleteReturnFocusRef = _react.useRef(null);
|
|
497
666
|
|
|
498
667
|
// Success confirmations are transient toasts (CodeX behavior);
|
|
499
668
|
// errors stay until dismissed.
|
|
@@ -524,6 +693,17 @@ window.__ModuleLoader__.load({
|
|
|
524
693
|
}
|
|
525
694
|
}, []);
|
|
526
695
|
_react.useEffect(() => { refresh(); }, [refresh]);
|
|
696
|
+
_react.useEffect(() => {
|
|
697
|
+
if (sessions === null) return;
|
|
698
|
+
setSelected((current) => {
|
|
699
|
+
const next = reconcileSelection(current, sessions);
|
|
700
|
+
return next.size === current.size && [...next].every((id) => current.has(id)) ? current : next;
|
|
701
|
+
});
|
|
702
|
+
}, [sessions]);
|
|
703
|
+
|
|
704
|
+
const toggleSelected = (ids, checked) => {
|
|
705
|
+
setSelected((current) => setVisibleSelection(current, ids, checked));
|
|
706
|
+
};
|
|
527
707
|
|
|
528
708
|
const markBusy = (ids, value) => {
|
|
529
709
|
setBusy((prev) => {
|
|
@@ -543,6 +723,7 @@ window.__ModuleLoader__.load({
|
|
|
543
723
|
try {
|
|
544
724
|
await post("/unarchive-all", { sessionIds: ids });
|
|
545
725
|
setSessions((prev) => (prev ?? []).filter((s) => !ids.includes(s.id)));
|
|
726
|
+
setSelected((current) => setVisibleSelection(current, ids, false));
|
|
546
727
|
} catch (error) {
|
|
547
728
|
setNotice({ kind: "error", text: String(error.message ?? error) });
|
|
548
729
|
} finally {
|
|
@@ -562,10 +743,12 @@ window.__ModuleLoader__.load({
|
|
|
562
743
|
const failed = result.failed ?? [];
|
|
563
744
|
if (deleted.length > 0 || pending.length > 0) {
|
|
564
745
|
changed = true;
|
|
746
|
+
const removed = [...deleted, ...pending];
|
|
565
747
|
// Pending rows leave the list too: the deletion is accepted,
|
|
566
748
|
// the session is parked and excluded from /state until the
|
|
567
749
|
// next-boot sweep physically removes it.
|
|
568
|
-
setSessions((prev) => (prev ?? []).filter((s) => !
|
|
750
|
+
setSessions((prev) => (prev ?? []).filter((s) => !removed.includes(s.id)));
|
|
751
|
+
setSelected((current) => setVisibleSelection(current, removed, false));
|
|
569
752
|
}
|
|
570
753
|
if (failed.length > 0) setNotice({ kind: "error", text: failureText(t, failed) });
|
|
571
754
|
else if (pending.length > 0) setNotice({ kind: "error", text: pendingText(t, pending.length) });
|
|
@@ -576,7 +759,9 @@ window.__ModuleLoader__.load({
|
|
|
576
759
|
const failed = error.body?.failed ?? [];
|
|
577
760
|
if (deleted.length > 0 || pending.length > 0) {
|
|
578
761
|
changed = true;
|
|
579
|
-
|
|
762
|
+
const removed = [...deleted, ...pending];
|
|
763
|
+
setSessions((prev) => (prev ?? []).filter((s) => !removed.includes(s.id)));
|
|
764
|
+
setSelected((current) => setVisibleSelection(current, removed, false));
|
|
580
765
|
}
|
|
581
766
|
setNotice({ kind: "error", text: failed.length > 0 ? failureText(t, failed) : String(error.message ?? error) });
|
|
582
767
|
} finally {
|
|
@@ -590,9 +775,12 @@ window.__ModuleLoader__.load({
|
|
|
590
775
|
}
|
|
591
776
|
};
|
|
592
777
|
|
|
593
|
-
const askDelete = (ids, groupName) => {
|
|
778
|
+
const askDelete = (ids, groupName, selectedScope = false) => {
|
|
594
779
|
if (ids.length === 0) return;
|
|
595
|
-
|
|
780
|
+
deleteReturnFocusRef.current = document.activeElement;
|
|
781
|
+
if (selectedScope) {
|
|
782
|
+
setConfirm({ title: t("confirm.deleteSelected.title"), body: deleteSelectedBody(t, ids.length), ids });
|
|
783
|
+
} else if (ids.length === 1 && groupName === null) {
|
|
596
784
|
setConfirm({ title: t("confirm.deleteOne.title"), body: t("confirm.deleteOne.body"), ids });
|
|
597
785
|
} else if (groupName !== null && groupName !== void 0) {
|
|
598
786
|
setConfirm({ title: t("confirm.deleteGroup.title"), body: deleteGroupBody(t, groupName, ids.length), ids });
|
|
@@ -612,6 +800,12 @@ window.__ModuleLoader__.load({
|
|
|
612
800
|
|
|
613
801
|
const groups = _react.useMemo(() => {
|
|
614
802
|
const q = query.trim().toLowerCase();
|
|
803
|
+
const projectIds = new Map();
|
|
804
|
+
for (const session of sessions ?? []) {
|
|
805
|
+
const key = session.workspaceId ?? "ungrouped";
|
|
806
|
+
if (!projectIds.has(key)) projectIds.set(key, []);
|
|
807
|
+
projectIds.get(key).push(session.id);
|
|
808
|
+
}
|
|
615
809
|
const visible = (sessions ?? []).filter((s) => {
|
|
616
810
|
if (projectFilter !== "all" && (s.workspaceId ?? "ungrouped") !== projectFilter) return false;
|
|
617
811
|
if (typeFilter === "normal" && s.origin === "subagent") return false;
|
|
@@ -622,17 +816,23 @@ window.__ModuleLoader__.load({
|
|
|
622
816
|
const byKey = new Map();
|
|
623
817
|
for (const s of visible) {
|
|
624
818
|
const key = s.workspaceId ?? "ungrouped";
|
|
625
|
-
if (!byKey.has(key)) byKey.set(key, { key, title: s.workspaceTitle ?? t("group.noProject"), items: [] });
|
|
819
|
+
if (!byKey.has(key)) byKey.set(key, { key, title: s.workspaceTitle ?? t("group.noProject"), items: [], selectionIds: projectIds.get(key) ?? [] });
|
|
626
820
|
byKey.get(key).items.push(s);
|
|
627
821
|
}
|
|
628
822
|
const list = [...byKey.values()];
|
|
629
|
-
for (const g of list) g.items
|
|
823
|
+
for (const g of list) g.items = sortArchivedSessions(g.items, sortMode, t("locale.intl"));
|
|
824
|
+
const groupCollator = new Intl.Collator(t("locale.intl"), { numeric: true, sensitivity: "base" });
|
|
630
825
|
list.sort((a, b) => {
|
|
631
826
|
if ((a.key === "ungrouped") !== (b.key === "ungrouped")) return a.key === "ungrouped" ? 1 : -1;
|
|
632
|
-
|
|
827
|
+
if (sortMode === "title") return groupCollator.compare(a.title, b.title);
|
|
828
|
+
const aDate = typeof a.items[0]?.createdAt === "number" ? a.items[0].createdAt : null;
|
|
829
|
+
const bDate = typeof b.items[0]?.createdAt === "number" ? b.items[0].createdAt : null;
|
|
830
|
+
if ((aDate === null) !== (bDate === null)) return aDate === null ? 1 : -1;
|
|
831
|
+
if (aDate !== null && bDate !== null && aDate !== bDate) return sortMode === "oldest" ? aDate - bDate : bDate - aDate;
|
|
832
|
+
return groupCollator.compare(a.title, b.title);
|
|
633
833
|
});
|
|
634
834
|
return list;
|
|
635
|
-
}, [sessions, query, projectFilter, typeFilter, t]);
|
|
835
|
+
}, [sessions, query, projectFilter, typeFilter, sortMode, t]);
|
|
636
836
|
|
|
637
837
|
if (sessions === null && loadError === null) {
|
|
638
838
|
return (0, jsx.jsx)("div", { className: "dac-center", children: [(0, jsx.jsx)("span", { className: "dac-spin" }), (0, jsx.jsx)("span", { children: t("state.loading") })] });
|
|
@@ -648,6 +848,13 @@ window.__ModuleLoader__.load({
|
|
|
648
848
|
}
|
|
649
849
|
|
|
650
850
|
const allIds = (sessions ?? []).map((s) => s.id);
|
|
851
|
+
const visibleIds = groups.flatMap((group) => group.items.map((session) => session.id));
|
|
852
|
+
const selectedIds = allIds.filter((id) => selected.has(id));
|
|
853
|
+
const selectedVisibleCount = visibleIds.filter((id) => selected.has(id)).length;
|
|
854
|
+
const allVisibleSelected = visibleIds.length > 0 && selectedVisibleCount === visibleIds.length;
|
|
855
|
+
const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected;
|
|
856
|
+
const selectedBusy = selectedIds.some((id) => busy[id] === true);
|
|
857
|
+
const visibleBusy = visibleIds.some((id) => busy[id] === true);
|
|
651
858
|
const filtering = query.trim() !== "" || projectFilter !== "all" || typeFilter !== "all";
|
|
652
859
|
|
|
653
860
|
return (0, jsx.jsxs)("div", {
|
|
@@ -656,7 +863,7 @@ window.__ModuleLoader__.load({
|
|
|
656
863
|
(0, jsx.jsxs)("div", {
|
|
657
864
|
className: "dac-head",
|
|
658
865
|
children: [
|
|
659
|
-
(0, jsx.jsx)("
|
|
866
|
+
(0, jsx.jsx)("h2", { ref: pageHeadingRef, tabIndex: -1, className: "dac-title", children: t("page.title") }),
|
|
660
867
|
(0, jsx.jsxs)("button", {
|
|
661
868
|
type: "button",
|
|
662
869
|
className: "dac-deleteall",
|
|
@@ -699,6 +906,62 @@ window.__ModuleLoader__.load({
|
|
|
699
906
|
{ value: "all", label: t("filter.allProjects") },
|
|
700
907
|
...projects.map((p) => ({ value: p.value, label: p.title ?? t("group.noProject") }))
|
|
701
908
|
]
|
|
909
|
+
}),
|
|
910
|
+
(0, jsx.jsx)(Select, {
|
|
911
|
+
value: sortMode,
|
|
912
|
+
onChange: setSortMode,
|
|
913
|
+
ariaLabel: t("sort.label"),
|
|
914
|
+
options: [
|
|
915
|
+
{ value: "newest", label: t("sort.newest") },
|
|
916
|
+
{ value: "oldest", label: t("sort.oldest") },
|
|
917
|
+
{ value: "title", label: t("sort.title") }
|
|
918
|
+
]
|
|
919
|
+
}),
|
|
920
|
+
visibleIds.length > 0 && (0, jsx.jsxs)("label", {
|
|
921
|
+
className: "dac-selection-toggle",
|
|
922
|
+
children: [
|
|
923
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
924
|
+
checked: allVisibleSelected,
|
|
925
|
+
indeterminate: someVisibleSelected,
|
|
926
|
+
disabled: visibleBusy,
|
|
927
|
+
ariaLabel: t("selection.visible"),
|
|
928
|
+
onChange: (checked) => toggleSelected(visibleIds, checked)
|
|
929
|
+
}),
|
|
930
|
+
(0, jsx.jsx)("span", { children: t("selection.visible") })
|
|
931
|
+
]
|
|
932
|
+
})
|
|
933
|
+
]
|
|
934
|
+
}),
|
|
935
|
+
selectedIds.length > 0 && (0, jsx.jsxs)("div", {
|
|
936
|
+
className: "dac-bulkbar",
|
|
937
|
+
role: "region",
|
|
938
|
+
"aria-label": selectedCount(t, selectedIds.length),
|
|
939
|
+
children: [
|
|
940
|
+
(0, jsx.jsx)("span", { className: "dac-bulk-count", children: selectedCount(t, selectedIds.length) }),
|
|
941
|
+
(0, jsx.jsxs)("div", {
|
|
942
|
+
className: "dac-bulk-actions",
|
|
943
|
+
children: [
|
|
944
|
+
(0, jsx.jsx)("button", {
|
|
945
|
+
type: "button",
|
|
946
|
+
className: "dac-bulk-btn",
|
|
947
|
+
disabled: selectedBusy,
|
|
948
|
+
onClick: () => unarchive(selectedIds),
|
|
949
|
+
children: t("bulk.unarchive")
|
|
950
|
+
}),
|
|
951
|
+
(0, jsx.jsx)("button", {
|
|
952
|
+
type: "button",
|
|
953
|
+
className: "dac-bulk-btn dac-danger",
|
|
954
|
+
disabled: selectedBusy,
|
|
955
|
+
onClick: () => askDelete(selectedIds, null, true),
|
|
956
|
+
children: t("bulk.delete")
|
|
957
|
+
}),
|
|
958
|
+
(0, jsx.jsx)("button", {
|
|
959
|
+
type: "button",
|
|
960
|
+
className: "dac-bulk-btn",
|
|
961
|
+
onClick: () => setSelected(new Set()),
|
|
962
|
+
children: t("selection.clear")
|
|
963
|
+
})
|
|
964
|
+
]
|
|
702
965
|
})
|
|
703
966
|
]
|
|
704
967
|
}),
|
|
@@ -736,7 +999,9 @@ window.__ModuleLoader__.load({
|
|
|
736
999
|
onToggleMenu: setMenuFor,
|
|
737
1000
|
onUnarchive: unarchive,
|
|
738
1001
|
onDelete: askDelete,
|
|
739
|
-
busy
|
|
1002
|
+
busy,
|
|
1003
|
+
selected,
|
|
1004
|
+
onToggleSelected: toggleSelected
|
|
740
1005
|
}, group.key)),
|
|
741
1006
|
confirm !== null && (0, jsx.jsx)(ConfirmDialog, {
|
|
742
1007
|
title: confirm.title,
|
|
@@ -744,6 +1009,8 @@ window.__ModuleLoader__.load({
|
|
|
744
1009
|
confirmLabel: t("confirm.delete"),
|
|
745
1010
|
cancelLabel: t("confirm.cancel"),
|
|
746
1011
|
busy: confirm.ids.some((id) => busy[id] === true),
|
|
1012
|
+
returnFocus: deleteReturnFocusRef.current,
|
|
1013
|
+
fallbackFocusRef: pageHeadingRef,
|
|
747
1014
|
onConfirm: () => runDelete(confirm.ids),
|
|
748
1015
|
onCancel: () => setConfirm(null)
|
|
749
1016
|
})
|
|
@@ -782,6 +1049,7 @@ window.__ModuleLoader__.load({
|
|
|
782
1049
|
//#endregion
|
|
783
1050
|
|
|
784
1051
|
exports.SETTINGS_NS = SETTINGS_NS;
|
|
1052
|
+
exports.__test = { sortArchivedSessions, setVisibleSelection, reconcileSelection };
|
|
785
1053
|
exports.apply = apply;
|
|
786
1054
|
exports.inject = inject;
|
|
787
1055
|
return module.exports;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-archived-chats",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DeepSeek Harness
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DeepSeek Harness 已归档会话管理页:搜索、排序、批量选择、取消归档和删除。An Archived Chats settings page for DeepSeek Harness with search, sorting, bulk selection, unarchive, and delete.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ultronen",
|
|
7
7
|
"repository": {
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"web-ui"
|
|
22
22
|
],
|
|
23
23
|
"type": "module",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/smoke.test.mjs"
|
|
26
|
+
},
|
|
24
27
|
"main": "lib/index.js",
|
|
25
28
|
"exports": {
|
|
26
29
|
".": {
|