dsh-archived-chats 0.4.0 → 0.5.1
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 +17 -2
- package/README.zh.md +17 -2
- package/lib/client.js +357 -70
- 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.
|
|
@@ -12,10 +14,15 @@ dsh plugin --profile web add dsh-archived-chats
|
|
|
12
14
|
|
|
13
15
|
Restart DSH once after installing, then open **Settings → Archived Chats**.
|
|
14
16
|
|
|
17
|
+
## Compatibility
|
|
18
|
+
|
|
19
|
+
Version 0.5.1 is tested against DeepSeek Harness `0.1.0-rc.7`. The plugin registers a top-level `settings.section`, so the rc.7 keyed-slot change for `settings.plugin.item` does not apply to it. Future Harness releases should still be checked with the smoke suite and a real-host UI pass before publishing a plugin update, because client slot and design-token contracts may evolve.
|
|
20
|
+
|
|
15
21
|
## Features
|
|
16
22
|
|
|
17
23
|
- **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,
|
|
24
|
+
- **Search and sort** by title, filter by type (all / regular / subagent) and project, then order results by newest, oldest, or title.
|
|
25
|
+
- **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
26
|
- **Unarchive** a single chat or a whole project group from the group's `⋯` menu — restored chats reappear in the sidebar immediately.
|
|
20
27
|
- **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
28
|
- 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.
|
|
@@ -27,7 +34,15 @@ Restart DSH once after installing, then open **Settings → Archived Chats**.
|
|
|
27
34
|
- **In-place live deletion**: deleting a resident session replays the agent factory's own disposer sequence — `cancel({ kind: 'disposed' })` → `whenIdle` → `flush` → `agent.scope.dispose()` → detach of the `agents` and `sessions` store entries. The session detach emits `session/disposed`, the persistence coordinator retires (drains and releases) the write path, and the ordinary cold delete completes in the same request. The store entries are internal surfaces, so every step is feature-detected; anything missing falls back to park-and-defer.
|
|
28
35
|
- **Pending-deletion store** (fallback path and crash bracket): the id is recorded in `$DSH_HOME/plugin-data/archived-chats/pending-deletions.json` while the session stays archived and hidden; the next boot sweeps the queue through the ordinary delete path. In-place deletes are bracketed by the same store (recorded before disposal, cleared once the files are gone), so a crash mid-delete is completed on the next start. Parked sessions are excluded from the listing; unarchiving cancels a pending deletion.
|
|
29
36
|
- **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
|
-
- **Browser half** (`lib/client.js`) registers a `settings.section` slot entry (order 30) and renders the page with React and DSH design tokens.
|
|
37
|
+
- **Browser half** (`lib/client.js`) registers a `settings.section` slot entry (order 30) and renders the page with React and the rc.7 DSH overlay/state design tokens.
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The smoke test uses an isolated temporary DSH home plus mocked host and browser runtimes; it never reads or changes real sessions.
|
|
31
46
|
|
|
32
47
|
## Uninstall
|
|
33
48
|
|
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`)还记得它。这个插件在「设置」中补上一个「已归档的聊天」页面,让所有归档会话都可见、可搜索、可管理。
|
|
@@ -12,10 +14,15 @@ dsh plugin --profile web add dsh-archived-chats
|
|
|
12
14
|
|
|
13
15
|
安装后重启一次 DSH,然后打开 **设置 → 已归档的聊天**。
|
|
14
16
|
|
|
17
|
+
## 兼容性
|
|
18
|
+
|
|
19
|
+
0.5.1 版本以 DeepSeek Harness `0.1.0-rc.7` 为验证基线。插件注册的是顶层 `settings.section`,因此 rc.7 针对 `settings.plugin.item` 的 keyed-slot 变更不影响本插件。以后 Harness 发布新版本时,仍应在发布插件更新前重跑冒烟测试并检查真实宿主页面,因为客户端插槽和设计令牌契约仍可能演进。
|
|
20
|
+
|
|
15
21
|
## 功能
|
|
16
22
|
|
|
17
23
|
- **完整归档列表**:按工作区(项目)分组并显示每组数量;每个分组都可折叠/展开,状态按浏览器记忆。
|
|
18
|
-
-
|
|
24
|
+
- **搜索与排序**:按标题搜索,用类型(全部 / 普通会话 / 子代理会话)和项目筛选,并按最新、最早或标题排序。
|
|
25
|
+
- **灵活多选**:逐条选择、选择当前筛选结果或选择整个项目;选中后可一次取消归档或永久删除,隐藏在其他筛选结果中的选择不会丢失。
|
|
19
26
|
- **取消归档**单个聊天,或从分组的 `⋯` 菜单整组取消——恢复的聊天会立刻回到侧边栏。
|
|
20
27
|
- **删除**单个聊天、某个项目分组或全部(**全部删除**),均有确认弹窗。删除是彻底的:会话日志从磁盘移除、从工作区记录中摘除、注册表内存索引同步清理,主侧边栏的条目也会立即消失。
|
|
21
28
|
- 仍驻留后台的会话也**当场删除**:插件按官方生命周期的拆除顺序原地停用并注销会话(取消 → 静默 → 落盘 → 拆纤程 → 摘出注册表),持久层随之释放写入通道,同一次请求内即完成物理删除——无需重启。若当前 DSH 版本不提供所需内部接口,则自动回退为「永久停用 + 下次启动完成删除」,停用期间会话保持隐藏。
|
|
@@ -27,7 +34,15 @@ dsh plugin --profile web add dsh-archived-chats
|
|
|
27
34
|
- **活会话原地删除**:删除仍驻留后台的会话时,插件复刻 agent 工厂自身 disposer 的顺序——`cancel({ kind: 'disposed' })` → `whenIdle` → `flush` → `agent.scope.dispose()` → 依次 detach `agents` 与 `sessions` 两个 store 条目;session detach 发出 `session/disposed`,持久化协调器随之 retire(排空并释放)该会话的写入通道,之后冷删除路径在同一请求内完成。所涉 store 条目属于内部接口,每一步都做特性探测,探测失败即回退为停用+延后。
|
|
28
35
|
- **待删队列**(回退路径与崩溃兜底):id 记入 `$DSH_HOME/plugin-data/archived-chats/pending-deletions.json`,会话保持归档与隐藏;下次启动时插件清扫该队列,通过常规删除路径完成物理删除。原地删除也用该队列包裹(删除前登记、文件移除后清除),中途崩溃由下次启动补完。已入队的会话不会出现在列表中;取消归档会撤销待删标记。
|
|
29
36
|
- **标题缓存**:列表刷新时按 id 记忆已解析的标题,不再每次全量读日志;删除与取消归档会使对应缓存失效。
|
|
30
|
-
- **浏览器半**(`lib/client.js`)注册 `settings.section` 插槽项(order 30),用 React 和 DSH
|
|
37
|
+
- **浏览器半**(`lib/client.js`)注册 `settings.section` 插槽项(order 30),用 React 和 rc.7 的 DSH 浮层/状态设计令牌渲染页面。
|
|
38
|
+
|
|
39
|
+
## 开发
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm test
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
测试使用隔离的临时 DSH 主目录和模拟的宿主/浏览器运行时,不会读取或修改真实会话。
|
|
31
46
|
|
|
32
47
|
## 卸载
|
|
33
48
|
|
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,15 +176,52 @@ 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}
|
|
158
|
-
.dac-deleteall{display:inline-flex;align-items:center;gap:6px;border:none;border-radius:999px;padding:6px 14px;background:
|
|
159
|
-
.dac-deleteall:hover:not(:disabled){background:
|
|
222
|
+
.dac-title{margin:0;color:var(--dsw-alias-label-primary);font-size:18px;font-weight:500;line-height:28px;outline:none}
|
|
223
|
+
.dac-deleteall{display:inline-flex;align-items:center;gap:6px;border:none;border-radius:999px;padding:6px 14px;background:transparent;color:var(--dsw-alias-state-error-primary);font:inherit;font-size:13px;line-height:20px;cursor:pointer;transition:background .15s}
|
|
224
|
+
.dac-deleteall:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
160
225
|
.dac-deleteall:disabled{opacity:.45;cursor:default}
|
|
161
226
|
.dac-search{display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:8px 12px;color:var(--dsw-alias-label-tertiary);transition:border-color .15s}
|
|
162
227
|
.dac-search:focus-within{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
@@ -167,8 +232,20 @@ 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:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:transparent}
|
|
244
|
+
.dac-bulk-btn.dac-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
245
|
+
.dac-bulk-btn:disabled{opacity:.45;cursor:default}
|
|
170
246
|
.dac-group{display:flex;flex-direction:column;gap:8px;margin-top:6px}
|
|
171
247
|
.dac-group-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px}
|
|
248
|
+
.dac-group-left{display:flex;align-items:center;gap:8px;min-width:0}
|
|
172
249
|
.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
250
|
.dac-group-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
174
251
|
.dac-chev{display:inline-flex;color:var(--dsw-alias-label-tertiary);transform:rotate(-90deg);transition:transform .15s}
|
|
@@ -179,15 +256,17 @@ window.__ModuleLoader__.load({
|
|
|
179
256
|
.dac-iconbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--dsw-alias-label-tertiary);cursor:pointer;transition:background .15s,color .15s;padding:0}
|
|
180
257
|
.dac-iconbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
|
|
181
258
|
.dac-iconbtn:disabled{opacity:.45;cursor:default}
|
|
182
|
-
.dac-iconbtn.dac-danger:hover:not(:disabled){color
|
|
183
|
-
.dac-menu{position:absolute;right:0;top:30px;z-index:40;min-width:150px;display:flex;flex-direction:column;gap:2px;padding:4px;border:1px solid var(--dsw-alias-border-
|
|
259
|
+
.dac-iconbtn.dac-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
260
|
+
.dac-menu{position:absolute;right:0;top:30px;z-index:40;min-width:150px;display:flex;flex-direction:column;gap:2px;padding:4px;border:1px solid var(--dsw-alias-border-inverted);border-radius:10px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3)}
|
|
184
261
|
.dac-menu-item{border:none;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;padding:7px 10px;cursor:pointer}
|
|
185
262
|
.dac-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
186
|
-
.dac-menu-item.dac-danger{color
|
|
187
|
-
.dac-menu-item.dac-danger:hover{background:
|
|
263
|
+
.dac-menu-item.dac-danger{color:var(--dsw-alias-state-error-primary)}
|
|
264
|
+
.dac-menu-item.dac-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
188
265
|
.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}
|
|
266
|
+
.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
267
|
.dac-row:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
268
|
+
.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))}
|
|
269
|
+
.dac-row-select{display:flex;align-items:center;gap:11px;min-width:0;flex:1}
|
|
191
270
|
.dac-row-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
192
271
|
.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
272
|
.dac-row-date{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
@@ -198,26 +277,28 @@ window.__ModuleLoader__.load({
|
|
|
198
277
|
.dac-empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:56px 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}
|
|
199
278
|
.dac-center{display:flex;align-items:center;justify-content:center;gap:8px;padding:48px 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}
|
|
200
279
|
.dac-retry{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:4px 12px;cursor:pointer}
|
|
201
|
-
.dac-notice{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid
|
|
280
|
+
.dac-notice{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:10px;background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;padding:8px 12px}
|
|
202
281
|
.dac-notice button{border:none;background:transparent;color:inherit;cursor:pointer;font:inherit;padding:0;display:inline-flex}
|
|
203
|
-
.dac-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:1200;display:flex;align-items:center;gap:8px;border:1px solid
|
|
282
|
+
.dac-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:1200;display:flex;align-items:center;gap:8px;border:1px solid var(--dsw-alias-state-success-secondary);border-radius:999px;background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary);font-size:13px;font-weight:500;line-height:20px;padding:7px 12px 7px 14px;box-shadow:var(--dsw-shadow-lv3);animation:dac-toast-in .28s cubic-bezier(.21,1.02,.55,1) both}
|
|
204
283
|
.dac-toast svg{flex:none}
|
|
205
284
|
.dac-toast button{border:none;background:transparent;color:inherit;opacity:.55;cursor:pointer;font:inherit;padding:2px;margin-left:2px;display:inline-flex;border-radius:999px}
|
|
206
|
-
.dac-toast button:hover{opacity:1;background:
|
|
285
|
+
.dac-toast button:hover{opacity:1;background:var(--dsw-alias-state-success-secondary)}
|
|
207
286
|
@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
287
|
@media (prefers-reduced-motion:reduce){.dac-toast{animation:none}}
|
|
209
288
|
.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-
|
|
289
|
+
.dac-confirm{width:min(400px,calc(100vw - 64px));display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-border-inverted);border-radius:16px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);padding:20px;outline:none}
|
|
290
|
+
.dac-confirm:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-interactive-primary,#6e6ef7),var(--dsw-shadow-lv3)}
|
|
211
291
|
.dac-confirm-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:500;line-height:24px}
|
|
212
292
|
.dac-confirm-body{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:21px}
|
|
213
293
|
.dac-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
|
|
214
294
|
.dac-btn{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:5px 14px;cursor:pointer}
|
|
215
295
|
.dac-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
216
|
-
.dac-btn-danger{border:
|
|
217
|
-
.dac-btn-danger:hover:not(:disabled){background
|
|
296
|
+
.dac-btn-danger{border:1px solid var(--dsw-alias-state-error-primary);border-radius:9px;background:transparent;color:var(--dsw-alias-state-error-primary);font:inherit;font-size:13px;line-height:20px;padding:5px 14px;cursor:pointer}
|
|
297
|
+
.dac-btn-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}
|
|
218
298
|
.dac-btn-danger:disabled{opacity:.5;cursor:default}
|
|
219
299
|
.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
300
|
@keyframes dac-rotate{to{transform:rotate(360deg)}}
|
|
301
|
+
@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
302
|
`;
|
|
222
303
|
|
|
223
304
|
function ensureStyle() {
|
|
@@ -241,24 +322,35 @@ window.__ModuleLoader__.load({
|
|
|
241
322
|
const NAV_LABELS = ["已归档的聊天", "Archived Chats"];
|
|
242
323
|
const ARCHIVE_ICON_INNER = '<rect x="3" y="4" width="18" height="5" rx="1"></rect><path d="M5 9v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9"></path><path d="M10 13h4"></path>';
|
|
243
324
|
|
|
325
|
+
function safeQueryAll(root, selector) {
|
|
326
|
+
try {
|
|
327
|
+
return typeof root?.querySelectorAll === "function" ? root.querySelectorAll(selector) : [];
|
|
328
|
+
} catch {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
244
333
|
function patchNavIcons() {
|
|
245
334
|
if (typeof document === "undefined" || document.body === null) return;
|
|
246
|
-
const dialogs = document
|
|
335
|
+
const dialogs = safeQueryAll(document, '[role="dialog"]');
|
|
247
336
|
for (const dialog of dialogs) {
|
|
248
|
-
const buttons = dialog
|
|
337
|
+
const buttons = safeQueryAll(dialog, "nav button");
|
|
249
338
|
for (const button of buttons) {
|
|
250
|
-
const text = button.textContent
|
|
339
|
+
const text = typeof button?.textContent === "string" ? button.textContent : "";
|
|
251
340
|
if (!NAV_LABELS.some((label) => text.includes(label))) continue;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
svg.dataset.dacPatched
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
341
|
+
let svg = null;
|
|
342
|
+
try { svg = typeof button.querySelector === "function" ? button.querySelector("svg") : null; } catch { continue; }
|
|
343
|
+
if (svg === null || svg.dataset === void 0 || svg.dataset.dacPatched === "1" || typeof svg.setAttribute !== "function") continue;
|
|
344
|
+
try {
|
|
345
|
+
svg.dataset.dacPatched = "1";
|
|
346
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
347
|
+
svg.setAttribute("fill", "none");
|
|
348
|
+
svg.setAttribute("stroke", "currentColor");
|
|
349
|
+
svg.setAttribute("stroke-width", "1.5");
|
|
350
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
351
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
352
|
+
svg.innerHTML = ARCHIVE_ICON_INNER;
|
|
353
|
+
} catch { /* keep the host-provided fallback icon */ }
|
|
262
354
|
}
|
|
263
355
|
}
|
|
264
356
|
}
|
|
@@ -267,15 +359,22 @@ window.__ModuleLoader__.load({
|
|
|
267
359
|
|
|
268
360
|
function startNavIconPatch() {
|
|
269
361
|
patchNavIcons();
|
|
270
|
-
|
|
362
|
+
let Observer;
|
|
363
|
+
try { Observer = typeof window !== "undefined" ? window.MutationObserver : void 0; } catch { return; }
|
|
271
364
|
if (Observer === void 0 || typeof document === "undefined" || document.body === null) return;
|
|
272
365
|
if (navObserver !== null) return;
|
|
273
|
-
|
|
274
|
-
|
|
366
|
+
let observer = null;
|
|
367
|
+
try {
|
|
368
|
+
observer = new Observer(() => patchNavIcons());
|
|
369
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
370
|
+
navObserver = observer;
|
|
371
|
+
} catch {
|
|
372
|
+
try { observer?.disconnect?.(); } catch { /* best-effort decoration cleanup */ }
|
|
373
|
+
}
|
|
275
374
|
}
|
|
276
375
|
|
|
277
376
|
function stopNavIconPatch() {
|
|
278
|
-
navObserver?.disconnect();
|
|
377
|
+
try { navObserver?.disconnect?.(); } catch { /* best-effort decoration cleanup */ }
|
|
279
378
|
navObserver = null;
|
|
280
379
|
}
|
|
281
380
|
//#endregion
|
|
@@ -351,27 +450,85 @@ window.__ModuleLoader__.load({
|
|
|
351
450
|
});
|
|
352
451
|
}
|
|
353
452
|
|
|
354
|
-
function
|
|
453
|
+
function SelectionCheckbox({ checked, indeterminate = false, disabled = false, ariaLabel, onChange }) {
|
|
454
|
+
const inputRef = _react.useRef(null);
|
|
455
|
+
_react.useEffect(() => {
|
|
456
|
+
if (inputRef.current !== null) inputRef.current.indeterminate = indeterminate;
|
|
457
|
+
}, [indeterminate]);
|
|
458
|
+
return (0, jsx.jsx)("input", {
|
|
459
|
+
ref: inputRef,
|
|
460
|
+
type: "checkbox",
|
|
461
|
+
className: "dac-checkbox",
|
|
462
|
+
checked,
|
|
463
|
+
disabled,
|
|
464
|
+
"aria-label": ariaLabel,
|
|
465
|
+
"aria-checked": indeterminate ? "mixed" : checked,
|
|
466
|
+
onChange: (event) => onChange(event.target.checked)
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function ConfirmDialog({ title, body, confirmLabel, cancelLabel, busy, returnFocus, fallbackFocusRef, onConfirm, onCancel }) {
|
|
471
|
+
const dialogRef = _react.useRef(null);
|
|
472
|
+
const cancelRef = _react.useRef(null);
|
|
355
473
|
_react.useEffect(() => {
|
|
356
|
-
const
|
|
474
|
+
const previousFocus = returnFocus ?? document.activeElement;
|
|
475
|
+
const dialog = dialogRef.current;
|
|
476
|
+
(cancelRef.current ?? dialog)?.focus?.();
|
|
477
|
+
const onKey = (e) => {
|
|
478
|
+
if (e.key === "Escape") {
|
|
479
|
+
e.preventDefault?.();
|
|
480
|
+
onCancel();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (e.key !== "Tab" || dialog === null) return;
|
|
484
|
+
const focusable = [...dialog.querySelectorAll('button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])')];
|
|
485
|
+
if (focusable.length === 0) {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
dialog.focus?.();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const first = focusable[0];
|
|
491
|
+
const last = focusable[focusable.length - 1];
|
|
492
|
+
const active = document.activeElement;
|
|
493
|
+
const focusIsInside = typeof dialog.contains === "function" ? dialog.contains(active) : focusable.includes(active);
|
|
494
|
+
if (e.shiftKey && (active === first || !focusIsInside)) {
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
last.focus();
|
|
497
|
+
} else if (!e.shiftKey && (active === last || !focusIsInside)) {
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
first.focus();
|
|
500
|
+
}
|
|
501
|
+
};
|
|
357
502
|
document.addEventListener("keydown", onKey);
|
|
358
|
-
return () =>
|
|
359
|
-
|
|
503
|
+
return () => {
|
|
504
|
+
document.removeEventListener("keydown", onKey);
|
|
505
|
+
const previousIsUsable = previousFocus !== null
|
|
506
|
+
&& previousFocus !== document.body
|
|
507
|
+
&& typeof previousFocus?.focus === "function"
|
|
508
|
+
&& (typeof document.contains !== "function" || document.contains(previousFocus));
|
|
509
|
+
const target = previousIsUsable ? previousFocus : fallbackFocusRef?.current;
|
|
510
|
+
target?.focus?.();
|
|
511
|
+
};
|
|
512
|
+
}, [onCancel, returnFocus, fallbackFocusRef]);
|
|
360
513
|
return (0, jsx.jsx)("div", {
|
|
361
514
|
className: "dac-confirm-overlay",
|
|
362
515
|
onClick: onCancel,
|
|
363
516
|
children: (0, jsx.jsxs)("div", {
|
|
517
|
+
ref: dialogRef,
|
|
364
518
|
className: "dac-confirm",
|
|
365
519
|
role: "alertdialog",
|
|
366
520
|
"aria-modal": "true",
|
|
521
|
+
"aria-labelledby": "dac-confirm-title",
|
|
522
|
+
"aria-describedby": "dac-confirm-body",
|
|
523
|
+
tabIndex: -1,
|
|
367
524
|
onClick: (e) => e.stopPropagation(),
|
|
368
525
|
children: [
|
|
369
|
-
(0, jsx.jsx)("div", { className: "dac-confirm-title", children: title }),
|
|
370
|
-
(0, jsx.jsx)("div", { className: "dac-confirm-body", children: body }),
|
|
526
|
+
(0, jsx.jsx)("div", { id: "dac-confirm-title", className: "dac-confirm-title", children: title }),
|
|
527
|
+
(0, jsx.jsx)("div", { id: "dac-confirm-body", className: "dac-confirm-body", children: body }),
|
|
371
528
|
(0, jsx.jsxs)("div", {
|
|
372
529
|
className: "dac-confirm-actions",
|
|
373
530
|
children: [
|
|
374
|
-
(0, jsx.jsx)("button", { type: "button", className: "dac-btn", onClick: onCancel, children: cancelLabel }),
|
|
531
|
+
(0, jsx.jsx)("button", { ref: cancelRef, type: "button", className: "dac-btn", onClick: onCancel, children: cancelLabel }),
|
|
375
532
|
(0, jsx.jsx)("button", { type: "button", className: "dac-btn-danger", disabled: busy, onClick: onConfirm, children: confirmLabel })
|
|
376
533
|
]
|
|
377
534
|
})
|
|
@@ -380,7 +537,7 @@ window.__ModuleLoader__.load({
|
|
|
380
537
|
});
|
|
381
538
|
}
|
|
382
539
|
|
|
383
|
-
function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, busy }) {
|
|
540
|
+
function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, busy, selected, onToggleSelected }) {
|
|
384
541
|
const wrapRef = _react.useRef(null);
|
|
385
542
|
_react.useEffect(() => {
|
|
386
543
|
if (!menuOpen) return void 0;
|
|
@@ -388,7 +545,11 @@ window.__ModuleLoader__.load({
|
|
|
388
545
|
document.addEventListener("mousedown", onDown);
|
|
389
546
|
return () => document.removeEventListener("mousedown", onDown);
|
|
390
547
|
}, [menuOpen, onToggleMenu]);
|
|
391
|
-
const ids = group.
|
|
548
|
+
const ids = group.selectionIds;
|
|
549
|
+
const selectedInGroup = ids.filter((id) => selected.has(id)).length;
|
|
550
|
+
const allSelected = ids.length > 0 && selectedInGroup === ids.length;
|
|
551
|
+
const someSelected = selectedInGroup > 0 && !allSelected;
|
|
552
|
+
const groupBusy = ids.some((id) => busy[id] === true);
|
|
392
553
|
return (0, jsx.jsxs)("div", {
|
|
393
554
|
className: "dac-group",
|
|
394
555
|
children: [
|
|
@@ -396,16 +557,28 @@ window.__ModuleLoader__.load({
|
|
|
396
557
|
className: "dac-group-head",
|
|
397
558
|
ref: wrapRef,
|
|
398
559
|
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),
|
|
560
|
+
(0, jsx.jsxs)("div", {
|
|
561
|
+
className: "dac-group-left",
|
|
405
562
|
children: [
|
|
406
|
-
(0, jsx.jsx)(
|
|
407
|
-
|
|
408
|
-
|
|
563
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
564
|
+
checked: allSelected,
|
|
565
|
+
indeterminate: someSelected,
|
|
566
|
+
disabled: groupBusy,
|
|
567
|
+
ariaLabel: selectProjectLabel(t, group.title),
|
|
568
|
+
onChange: (checked) => onToggleSelected(ids, checked)
|
|
569
|
+
}),
|
|
570
|
+
(0, jsx.jsxs)("button", {
|
|
571
|
+
type: "button",
|
|
572
|
+
className: "dac-group-toggle",
|
|
573
|
+
"aria-expanded": collapsed !== true,
|
|
574
|
+
"aria-label": collapsed === true ? t("group.expand") : t("group.collapse"),
|
|
575
|
+
onClick: () => onToggleCollapsed(group.key),
|
|
576
|
+
children: [
|
|
577
|
+
(0, jsx.jsx)("span", { className: collapsed === true ? "dac-chev" : "dac-chev open", children: (0, jsx.jsx)(IconChevron, {}) }),
|
|
578
|
+
(0, jsx.jsx)(IconFolder, {}),
|
|
579
|
+
(0, jsx.jsx)("span", { children: group.title })
|
|
580
|
+
]
|
|
581
|
+
})
|
|
409
582
|
]
|
|
410
583
|
}),
|
|
411
584
|
(0, jsx.jsxs)("div", {
|
|
@@ -446,15 +619,26 @@ window.__ModuleLoader__.load({
|
|
|
446
619
|
collapsed !== true && (0, jsx.jsx)("div", {
|
|
447
620
|
className: "dac-list",
|
|
448
621
|
children: group.items.map((session) => (0, jsx.jsxs)("div", {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
622
|
+
className: selected.has(session.id) ? "dac-row dac-selected" : "dac-row",
|
|
623
|
+
children: [
|
|
624
|
+
(0, jsx.jsxs)("label", {
|
|
625
|
+
className: "dac-row-select",
|
|
626
|
+
children: [
|
|
627
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
628
|
+
checked: selected.has(session.id),
|
|
629
|
+
disabled: busy[session.id] === true,
|
|
630
|
+
ariaLabel: selectChatLabel(t, session.title ?? t("chat.untitled")),
|
|
631
|
+
onChange: (checked) => onToggleSelected([session.id], checked)
|
|
632
|
+
}),
|
|
633
|
+
(0, jsx.jsxs)("div", {
|
|
634
|
+
className: "dac-row-main",
|
|
635
|
+
children: [
|
|
636
|
+
(0, jsx.jsx)("div", { className: "dac-row-title", children: session.title ?? t("chat.untitled") }),
|
|
637
|
+
(0, jsx.jsx)("div", { className: "dac-row-date", children: formatDate(t, session.createdAt) })
|
|
638
|
+
]
|
|
639
|
+
})
|
|
640
|
+
]
|
|
641
|
+
}),
|
|
458
642
|
(0, jsx.jsxs)("div", {
|
|
459
643
|
className: "dac-row-actions",
|
|
460
644
|
children: [
|
|
@@ -489,11 +673,15 @@ window.__ModuleLoader__.load({
|
|
|
489
673
|
const [query, setQuery] = _react.useState("");
|
|
490
674
|
const [typeFilter, setTypeFilter] = _react.useState("all");
|
|
491
675
|
const [projectFilter, setProjectFilter] = _react.useState("all");
|
|
676
|
+
const [sortMode, setSortMode] = _react.useState("newest");
|
|
492
677
|
const [menuFor, setMenuFor] = _react.useState(null);
|
|
493
678
|
const [confirm, setConfirm] = _react.useState(null);
|
|
494
679
|
const [busy, setBusy] = _react.useState({});
|
|
495
680
|
const [notice, setNotice] = _react.useState(null);
|
|
496
681
|
const [collapsed, setCollapsed] = _react.useState(readCollapsed);
|
|
682
|
+
const [selected, setSelected] = _react.useState(() => new Set());
|
|
683
|
+
const pageHeadingRef = _react.useRef(null);
|
|
684
|
+
const deleteReturnFocusRef = _react.useRef(null);
|
|
497
685
|
|
|
498
686
|
// Success confirmations are transient toasts (CodeX behavior);
|
|
499
687
|
// errors stay until dismissed.
|
|
@@ -524,6 +712,17 @@ window.__ModuleLoader__.load({
|
|
|
524
712
|
}
|
|
525
713
|
}, []);
|
|
526
714
|
_react.useEffect(() => { refresh(); }, [refresh]);
|
|
715
|
+
_react.useEffect(() => {
|
|
716
|
+
if (sessions === null) return;
|
|
717
|
+
setSelected((current) => {
|
|
718
|
+
const next = reconcileSelection(current, sessions);
|
|
719
|
+
return next.size === current.size && [...next].every((id) => current.has(id)) ? current : next;
|
|
720
|
+
});
|
|
721
|
+
}, [sessions]);
|
|
722
|
+
|
|
723
|
+
const toggleSelected = (ids, checked) => {
|
|
724
|
+
setSelected((current) => setVisibleSelection(current, ids, checked));
|
|
725
|
+
};
|
|
527
726
|
|
|
528
727
|
const markBusy = (ids, value) => {
|
|
529
728
|
setBusy((prev) => {
|
|
@@ -543,6 +742,7 @@ window.__ModuleLoader__.load({
|
|
|
543
742
|
try {
|
|
544
743
|
await post("/unarchive-all", { sessionIds: ids });
|
|
545
744
|
setSessions((prev) => (prev ?? []).filter((s) => !ids.includes(s.id)));
|
|
745
|
+
setSelected((current) => setVisibleSelection(current, ids, false));
|
|
546
746
|
} catch (error) {
|
|
547
747
|
setNotice({ kind: "error", text: String(error.message ?? error) });
|
|
548
748
|
} finally {
|
|
@@ -562,10 +762,12 @@ window.__ModuleLoader__.load({
|
|
|
562
762
|
const failed = result.failed ?? [];
|
|
563
763
|
if (deleted.length > 0 || pending.length > 0) {
|
|
564
764
|
changed = true;
|
|
765
|
+
const removed = [...deleted, ...pending];
|
|
565
766
|
// Pending rows leave the list too: the deletion is accepted,
|
|
566
767
|
// the session is parked and excluded from /state until the
|
|
567
768
|
// next-boot sweep physically removes it.
|
|
568
|
-
setSessions((prev) => (prev ?? []).filter((s) => !
|
|
769
|
+
setSessions((prev) => (prev ?? []).filter((s) => !removed.includes(s.id)));
|
|
770
|
+
setSelected((current) => setVisibleSelection(current, removed, false));
|
|
569
771
|
}
|
|
570
772
|
if (failed.length > 0) setNotice({ kind: "error", text: failureText(t, failed) });
|
|
571
773
|
else if (pending.length > 0) setNotice({ kind: "error", text: pendingText(t, pending.length) });
|
|
@@ -576,7 +778,9 @@ window.__ModuleLoader__.load({
|
|
|
576
778
|
const failed = error.body?.failed ?? [];
|
|
577
779
|
if (deleted.length > 0 || pending.length > 0) {
|
|
578
780
|
changed = true;
|
|
579
|
-
|
|
781
|
+
const removed = [...deleted, ...pending];
|
|
782
|
+
setSessions((prev) => (prev ?? []).filter((s) => !removed.includes(s.id)));
|
|
783
|
+
setSelected((current) => setVisibleSelection(current, removed, false));
|
|
580
784
|
}
|
|
581
785
|
setNotice({ kind: "error", text: failed.length > 0 ? failureText(t, failed) : String(error.message ?? error) });
|
|
582
786
|
} finally {
|
|
@@ -590,9 +794,12 @@ window.__ModuleLoader__.load({
|
|
|
590
794
|
}
|
|
591
795
|
};
|
|
592
796
|
|
|
593
|
-
const askDelete = (ids, groupName) => {
|
|
797
|
+
const askDelete = (ids, groupName, selectedScope = false) => {
|
|
594
798
|
if (ids.length === 0) return;
|
|
595
|
-
|
|
799
|
+
deleteReturnFocusRef.current = document.activeElement;
|
|
800
|
+
if (selectedScope) {
|
|
801
|
+
setConfirm({ title: t("confirm.deleteSelected.title"), body: deleteSelectedBody(t, ids.length), ids });
|
|
802
|
+
} else if (ids.length === 1 && groupName === null) {
|
|
596
803
|
setConfirm({ title: t("confirm.deleteOne.title"), body: t("confirm.deleteOne.body"), ids });
|
|
597
804
|
} else if (groupName !== null && groupName !== void 0) {
|
|
598
805
|
setConfirm({ title: t("confirm.deleteGroup.title"), body: deleteGroupBody(t, groupName, ids.length), ids });
|
|
@@ -612,6 +819,12 @@ window.__ModuleLoader__.load({
|
|
|
612
819
|
|
|
613
820
|
const groups = _react.useMemo(() => {
|
|
614
821
|
const q = query.trim().toLowerCase();
|
|
822
|
+
const projectIds = new Map();
|
|
823
|
+
for (const session of sessions ?? []) {
|
|
824
|
+
const key = session.workspaceId ?? "ungrouped";
|
|
825
|
+
if (!projectIds.has(key)) projectIds.set(key, []);
|
|
826
|
+
projectIds.get(key).push(session.id);
|
|
827
|
+
}
|
|
615
828
|
const visible = (sessions ?? []).filter((s) => {
|
|
616
829
|
if (projectFilter !== "all" && (s.workspaceId ?? "ungrouped") !== projectFilter) return false;
|
|
617
830
|
if (typeFilter === "normal" && s.origin === "subagent") return false;
|
|
@@ -622,17 +835,23 @@ window.__ModuleLoader__.load({
|
|
|
622
835
|
const byKey = new Map();
|
|
623
836
|
for (const s of visible) {
|
|
624
837
|
const key = s.workspaceId ?? "ungrouped";
|
|
625
|
-
if (!byKey.has(key)) byKey.set(key, { key, title: s.workspaceTitle ?? t("group.noProject"), items: [] });
|
|
838
|
+
if (!byKey.has(key)) byKey.set(key, { key, title: s.workspaceTitle ?? t("group.noProject"), items: [], selectionIds: projectIds.get(key) ?? [] });
|
|
626
839
|
byKey.get(key).items.push(s);
|
|
627
840
|
}
|
|
628
841
|
const list = [...byKey.values()];
|
|
629
|
-
for (const g of list) g.items
|
|
842
|
+
for (const g of list) g.items = sortArchivedSessions(g.items, sortMode, t("locale.intl"));
|
|
843
|
+
const groupCollator = new Intl.Collator(t("locale.intl"), { numeric: true, sensitivity: "base" });
|
|
630
844
|
list.sort((a, b) => {
|
|
631
845
|
if ((a.key === "ungrouped") !== (b.key === "ungrouped")) return a.key === "ungrouped" ? 1 : -1;
|
|
632
|
-
|
|
846
|
+
if (sortMode === "title") return groupCollator.compare(a.title, b.title);
|
|
847
|
+
const aDate = typeof a.items[0]?.createdAt === "number" ? a.items[0].createdAt : null;
|
|
848
|
+
const bDate = typeof b.items[0]?.createdAt === "number" ? b.items[0].createdAt : null;
|
|
849
|
+
if ((aDate === null) !== (bDate === null)) return aDate === null ? 1 : -1;
|
|
850
|
+
if (aDate !== null && bDate !== null && aDate !== bDate) return sortMode === "oldest" ? aDate - bDate : bDate - aDate;
|
|
851
|
+
return groupCollator.compare(a.title, b.title);
|
|
633
852
|
});
|
|
634
853
|
return list;
|
|
635
|
-
}, [sessions, query, projectFilter, typeFilter, t]);
|
|
854
|
+
}, [sessions, query, projectFilter, typeFilter, sortMode, t]);
|
|
636
855
|
|
|
637
856
|
if (sessions === null && loadError === null) {
|
|
638
857
|
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 +867,13 @@ window.__ModuleLoader__.load({
|
|
|
648
867
|
}
|
|
649
868
|
|
|
650
869
|
const allIds = (sessions ?? []).map((s) => s.id);
|
|
870
|
+
const visibleIds = groups.flatMap((group) => group.items.map((session) => session.id));
|
|
871
|
+
const selectedIds = allIds.filter((id) => selected.has(id));
|
|
872
|
+
const selectedVisibleCount = visibleIds.filter((id) => selected.has(id)).length;
|
|
873
|
+
const allVisibleSelected = visibleIds.length > 0 && selectedVisibleCount === visibleIds.length;
|
|
874
|
+
const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected;
|
|
875
|
+
const selectedBusy = selectedIds.some((id) => busy[id] === true);
|
|
876
|
+
const visibleBusy = visibleIds.some((id) => busy[id] === true);
|
|
651
877
|
const filtering = query.trim() !== "" || projectFilter !== "all" || typeFilter !== "all";
|
|
652
878
|
|
|
653
879
|
return (0, jsx.jsxs)("div", {
|
|
@@ -656,7 +882,7 @@ window.__ModuleLoader__.load({
|
|
|
656
882
|
(0, jsx.jsxs)("div", {
|
|
657
883
|
className: "dac-head",
|
|
658
884
|
children: [
|
|
659
|
-
(0, jsx.jsx)("
|
|
885
|
+
(0, jsx.jsx)("h2", { ref: pageHeadingRef, tabIndex: -1, className: "dac-title", children: t("page.title") }),
|
|
660
886
|
(0, jsx.jsxs)("button", {
|
|
661
887
|
type: "button",
|
|
662
888
|
className: "dac-deleteall",
|
|
@@ -699,6 +925,62 @@ window.__ModuleLoader__.load({
|
|
|
699
925
|
{ value: "all", label: t("filter.allProjects") },
|
|
700
926
|
...projects.map((p) => ({ value: p.value, label: p.title ?? t("group.noProject") }))
|
|
701
927
|
]
|
|
928
|
+
}),
|
|
929
|
+
(0, jsx.jsx)(Select, {
|
|
930
|
+
value: sortMode,
|
|
931
|
+
onChange: setSortMode,
|
|
932
|
+
ariaLabel: t("sort.label"),
|
|
933
|
+
options: [
|
|
934
|
+
{ value: "newest", label: t("sort.newest") },
|
|
935
|
+
{ value: "oldest", label: t("sort.oldest") },
|
|
936
|
+
{ value: "title", label: t("sort.title") }
|
|
937
|
+
]
|
|
938
|
+
}),
|
|
939
|
+
visibleIds.length > 0 && (0, jsx.jsxs)("label", {
|
|
940
|
+
className: "dac-selection-toggle",
|
|
941
|
+
children: [
|
|
942
|
+
(0, jsx.jsx)(SelectionCheckbox, {
|
|
943
|
+
checked: allVisibleSelected,
|
|
944
|
+
indeterminate: someVisibleSelected,
|
|
945
|
+
disabled: visibleBusy,
|
|
946
|
+
ariaLabel: t("selection.visible"),
|
|
947
|
+
onChange: (checked) => toggleSelected(visibleIds, checked)
|
|
948
|
+
}),
|
|
949
|
+
(0, jsx.jsx)("span", { children: t("selection.visible") })
|
|
950
|
+
]
|
|
951
|
+
})
|
|
952
|
+
]
|
|
953
|
+
}),
|
|
954
|
+
selectedIds.length > 0 && (0, jsx.jsxs)("div", {
|
|
955
|
+
className: "dac-bulkbar",
|
|
956
|
+
role: "region",
|
|
957
|
+
"aria-label": selectedCount(t, selectedIds.length),
|
|
958
|
+
children: [
|
|
959
|
+
(0, jsx.jsx)("span", { className: "dac-bulk-count", children: selectedCount(t, selectedIds.length) }),
|
|
960
|
+
(0, jsx.jsxs)("div", {
|
|
961
|
+
className: "dac-bulk-actions",
|
|
962
|
+
children: [
|
|
963
|
+
(0, jsx.jsx)("button", {
|
|
964
|
+
type: "button",
|
|
965
|
+
className: "dac-bulk-btn",
|
|
966
|
+
disabled: selectedBusy,
|
|
967
|
+
onClick: () => unarchive(selectedIds),
|
|
968
|
+
children: t("bulk.unarchive")
|
|
969
|
+
}),
|
|
970
|
+
(0, jsx.jsx)("button", {
|
|
971
|
+
type: "button",
|
|
972
|
+
className: "dac-bulk-btn dac-danger",
|
|
973
|
+
disabled: selectedBusy,
|
|
974
|
+
onClick: () => askDelete(selectedIds, null, true),
|
|
975
|
+
children: t("bulk.delete")
|
|
976
|
+
}),
|
|
977
|
+
(0, jsx.jsx)("button", {
|
|
978
|
+
type: "button",
|
|
979
|
+
className: "dac-bulk-btn",
|
|
980
|
+
onClick: () => setSelected(new Set()),
|
|
981
|
+
children: t("selection.clear")
|
|
982
|
+
})
|
|
983
|
+
]
|
|
702
984
|
})
|
|
703
985
|
]
|
|
704
986
|
}),
|
|
@@ -736,7 +1018,9 @@ window.__ModuleLoader__.load({
|
|
|
736
1018
|
onToggleMenu: setMenuFor,
|
|
737
1019
|
onUnarchive: unarchive,
|
|
738
1020
|
onDelete: askDelete,
|
|
739
|
-
busy
|
|
1021
|
+
busy,
|
|
1022
|
+
selected,
|
|
1023
|
+
onToggleSelected: toggleSelected
|
|
740
1024
|
}, group.key)),
|
|
741
1025
|
confirm !== null && (0, jsx.jsx)(ConfirmDialog, {
|
|
742
1026
|
title: confirm.title,
|
|
@@ -744,6 +1028,8 @@ window.__ModuleLoader__.load({
|
|
|
744
1028
|
confirmLabel: t("confirm.delete"),
|
|
745
1029
|
cancelLabel: t("confirm.cancel"),
|
|
746
1030
|
busy: confirm.ids.some((id) => busy[id] === true),
|
|
1031
|
+
returnFocus: deleteReturnFocusRef.current,
|
|
1032
|
+
fallbackFocusRef: pageHeadingRef,
|
|
747
1033
|
onConfirm: () => runDelete(confirm.ids),
|
|
748
1034
|
onCancel: () => setConfirm(null)
|
|
749
1035
|
})
|
|
@@ -782,6 +1068,7 @@ window.__ModuleLoader__.load({
|
|
|
782
1068
|
//#endregion
|
|
783
1069
|
|
|
784
1070
|
exports.SETTINGS_NS = SETTINGS_NS;
|
|
1071
|
+
exports.__test = { sortArchivedSessions, setVisibleSelection, reconcileSelection };
|
|
785
1072
|
exports.apply = apply;
|
|
786
1073
|
exports.inject = inject;
|
|
787
1074
|
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.1",
|
|
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
|
".": {
|