dsh-archived-chats 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/README.zh.md +40 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +746 -0
- package/lib/index.js +463 -0
- package/lib/types/client/index.d.ts +3 -0
- package/lib/types/index.d.ts +3 -0
- package/package.json +59 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
// dsh-archived-chats — browser half (client plugin bundle).
|
|
2
|
+
//
|
|
3
|
+
// Loaded by dsh-client-modules at /plugins/dsh-archived-chats/client.js and
|
|
4
|
+
// executed through the vendored cordis Loader's lazy-CJS module table
|
|
5
|
+
// (window.__ModuleLoader__.load). The factory body is plain CJS with require()
|
|
6
|
+
// resolved against the shell's module table — the same shape the shipped ui-*
|
|
7
|
+
// packages emit.
|
|
8
|
+
//
|
|
9
|
+
// What this plugin is: one settings section — 已归档的聊天 / Archived Chats —
|
|
10
|
+
// modeled 1:1 on the CodeX archived-chats page. The stock DSH sidebar hides
|
|
11
|
+
// archived sessions with no list surface; this page restores one: search,
|
|
12
|
+
// type/project filters, per-workspace grouping, unarchive, delete, and
|
|
13
|
+
// delete-all, all driven through the host half's /plugins/dsh-archived-chats/*
|
|
14
|
+
// routes (the archive set itself lives in the host's workspace registry).
|
|
15
|
+
window.__ModuleLoader__.load({
|
|
16
|
+
id: "dsh-archived-chats",
|
|
17
|
+
factory: (require) => {
|
|
18
|
+
var module = { exports: {} };
|
|
19
|
+
var exports = module.exports;
|
|
20
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
21
|
+
let jsx = require("react/jsx-runtime");
|
|
22
|
+
let _react = require("react");
|
|
23
|
+
|
|
24
|
+
//#region constants
|
|
25
|
+
const SETTINGS_NS = "settings.archived-chats";
|
|
26
|
+
const API_BASE = "/plugins/dsh-archived-chats";
|
|
27
|
+
const GUARD_HEADER = "x-dsh-archived-chats";
|
|
28
|
+
const STYLE_ID = "dsh-archived-chats-css";
|
|
29
|
+
const KEY_COLLAPSED = "dsh-archived-chats:collapsed";
|
|
30
|
+
//#endregion
|
|
31
|
+
|
|
32
|
+
//#region collapsed-group persistence (browser-local, like every visual preference)
|
|
33
|
+
function readCollapsed() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = window.localStorage.getItem(KEY_COLLAPSED);
|
|
36
|
+
if (raw === null) return {};
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
39
|
+
} catch { return {}; }
|
|
40
|
+
}
|
|
41
|
+
function writeCollapsed(map) {
|
|
42
|
+
try { window.localStorage.setItem(KEY_COLLAPSED, JSON.stringify(map)); } catch { /* quota — ignore */ }
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
|
|
46
|
+
//#region locale
|
|
47
|
+
const zh = {
|
|
48
|
+
"locale.intl": "zh-CN",
|
|
49
|
+
"nav": "已归档的聊天",
|
|
50
|
+
"page.title": "已归档的聊天",
|
|
51
|
+
"delete.all": "全部删除",
|
|
52
|
+
"search.placeholder": "搜索已归档聊天",
|
|
53
|
+
"filter.allChats": "全部聊天",
|
|
54
|
+
"filter.normal": "普通会话",
|
|
55
|
+
"filter.subagent": "子代理会话",
|
|
56
|
+
"filter.allProjects": "所有项目",
|
|
57
|
+
"group.noProject": "未分组",
|
|
58
|
+
"chat.untitled": "未命名会话",
|
|
59
|
+
"chat.unarchive": "取消归档",
|
|
60
|
+
"menu.unarchiveAll": "全部取消归档",
|
|
61
|
+
"menu.deleteAll": "全部删除",
|
|
62
|
+
"group.collapse": "折叠",
|
|
63
|
+
"group.expand": "展开",
|
|
64
|
+
"confirm.deleteOne.title": "删除已归档聊天?",
|
|
65
|
+
"confirm.deleteOne.body": "这将永久删除已归档聊天",
|
|
66
|
+
"confirm.deleteAll.title": "删除全部已归档聊天?",
|
|
67
|
+
"confirm.deleteGroup.title": "删除该项目下的已归档聊天?",
|
|
68
|
+
"confirm.cancel": "取消",
|
|
69
|
+
"confirm.delete": "删除",
|
|
70
|
+
"state.loading": "加载中…",
|
|
71
|
+
"state.empty": "暂无已归档的聊天",
|
|
72
|
+
"state.emptyFiltered": "没有匹配的已归档聊天",
|
|
73
|
+
"state.error": "加载失败",
|
|
74
|
+
"state.retry": "重试",
|
|
75
|
+
"notice.dismiss": "关闭"
|
|
76
|
+
};
|
|
77
|
+
const en = {
|
|
78
|
+
"locale.intl": "en-US",
|
|
79
|
+
"nav": "Archived Chats",
|
|
80
|
+
"page.title": "Archived Chats",
|
|
81
|
+
"delete.all": "Delete All",
|
|
82
|
+
"search.placeholder": "Search archived chats",
|
|
83
|
+
"filter.allChats": "All chats",
|
|
84
|
+
"filter.normal": "Regular chats",
|
|
85
|
+
"filter.subagent": "Subagent chats",
|
|
86
|
+
"filter.allProjects": "All projects",
|
|
87
|
+
"group.noProject": "Ungrouped",
|
|
88
|
+
"chat.untitled": "Untitled chat",
|
|
89
|
+
"chat.unarchive": "Unarchive",
|
|
90
|
+
"menu.unarchiveAll": "Unarchive all",
|
|
91
|
+
"menu.deleteAll": "Delete all",
|
|
92
|
+
"group.collapse": "Collapse",
|
|
93
|
+
"group.expand": "Expand",
|
|
94
|
+
"confirm.deleteOne.title": "Delete archived chat?",
|
|
95
|
+
"confirm.deleteOne.body": "This permanently deletes the archived chat",
|
|
96
|
+
"confirm.deleteAll.title": "Delete all archived chats?",
|
|
97
|
+
"confirm.deleteGroup.title": "Delete this project's archived chats?",
|
|
98
|
+
"confirm.cancel": "Cancel",
|
|
99
|
+
"confirm.delete": "Delete",
|
|
100
|
+
"state.loading": "Loading…",
|
|
101
|
+
"state.empty": "No archived chats",
|
|
102
|
+
"state.emptyFiltered": "No archived chats match your filters",
|
|
103
|
+
"state.error": "Failed to load",
|
|
104
|
+
"state.retry": "Retry",
|
|
105
|
+
"notice.dismiss": "Dismiss"
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
|
|
109
|
+
//#region localized compositors (interpolation stays in JS)
|
|
110
|
+
const isZh = (t) => t("locale.intl").startsWith("zh");
|
|
111
|
+
const chatsCount = (t, n) => isZh(t) ? `${n} 个聊天` : n === 1 ? "1 chat" : `${n} chats`;
|
|
112
|
+
const deleteAllBody = (t, n) => isZh(t)
|
|
113
|
+
? `这将永久删除全部 ${n} 个已归档聊天`
|
|
114
|
+
: `This permanently deletes all ${n} archived chat${n === 1 ? "" : "s"}`;
|
|
115
|
+
const deleteGroupBody = (t, name, n) => isZh(t)
|
|
116
|
+
? `这将永久删除「${name}」下 ${n} 个已归档聊天`
|
|
117
|
+
: `This permanently deletes ${n} archived chat${n === 1 ? "" : "s"} under "${name}"`;
|
|
118
|
+
const failureText = (t, failed) => {
|
|
119
|
+
if (isZh(t)) return `${failed.length} 个会话删除失败:${failed[0]?.message ?? ""}`;
|
|
120
|
+
return `${failed.length} chat${failed.length === 1 ? "" : "s"} failed to delete: ${failed[0]?.message ?? ""}`;
|
|
121
|
+
};
|
|
122
|
+
const pendingText = (t, n) => isZh(t)
|
|
123
|
+
? `${n} 个会话已停用,将在重启 DSH 后彻底删除`
|
|
124
|
+
: n === 1
|
|
125
|
+
? "1 chat is parked and will be permanently deleted after DSH restarts"
|
|
126
|
+
: `${n} chats are parked and will be permanently deleted after DSH restarts`;
|
|
127
|
+
function formatDate(t, ms) {
|
|
128
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return "";
|
|
129
|
+
try {
|
|
130
|
+
return new Intl.DateTimeFormat(t("locale.intl"), {
|
|
131
|
+
year: "numeric", month: "long", day: "numeric",
|
|
132
|
+
hour: "2-digit", minute: "2-digit", hour12: false
|
|
133
|
+
}).format(new Date(ms));
|
|
134
|
+
} catch {
|
|
135
|
+
return new Date(ms).toLocaleString();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
|
|
140
|
+
//#region styles
|
|
141
|
+
const CSS = `
|
|
142
|
+
.dac-page{display:flex;flex-direction:column;gap:14px;padding:4px 0 28px;font-family:inherit}
|
|
143
|
+
.dac-head{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
144
|
+
.dac-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:500;line-height:28px}
|
|
145
|
+
.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}
|
|
146
|
+
.dac-deleteall:hover:not(:disabled){background:rgba(229,72,77,.18)}
|
|
147
|
+
.dac-deleteall:disabled{opacity:.45;cursor:default}
|
|
148
|
+
.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}
|
|
149
|
+
.dac-search:focus-within{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
150
|
+
.dac-search input{flex:1;min-width:0;border:none;outline:none;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:0}
|
|
151
|
+
.dac-search input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
152
|
+
.dac-filters{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
153
|
+
.dac-select-wrap{position:relative;display:inline-flex;align-items:center}
|
|
154
|
+
.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}
|
|
155
|
+
.dac-select:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
156
|
+
.dac-chevron{position:absolute;right:10px;pointer-events:none;color:var(--dsw-alias-label-tertiary);display:inline-flex}
|
|
157
|
+
.dac-group{display:flex;flex-direction:column;gap:8px;margin-top:6px}
|
|
158
|
+
.dac-group-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:2px}
|
|
159
|
+
.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}
|
|
160
|
+
.dac-group-toggle:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
161
|
+
.dac-chev{display:inline-flex;color:var(--dsw-alias-label-tertiary);transform:rotate(-90deg);transition:transform .15s}
|
|
162
|
+
.dac-chev.open{transform:rotate(0)}
|
|
163
|
+
.dac-group-name{display:flex;align-items:center;gap:7px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
|
|
164
|
+
.dac-group-side{display:flex;align-items:center;gap:4px}
|
|
165
|
+
.dac-count{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin-right:2px}
|
|
166
|
+
.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}
|
|
167
|
+
.dac-iconbtn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary)}
|
|
168
|
+
.dac-iconbtn:disabled{opacity:.45;cursor:default}
|
|
169
|
+
.dac-iconbtn.dac-danger:hover:not(:disabled){color:#e5484d;background:rgba(229,72,77,.1)}
|
|
170
|
+
.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-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 8px 28px rgba(0,0,0,.18)}
|
|
171
|
+
.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}
|
|
172
|
+
.dac-menu-item:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
173
|
+
.dac-menu-item.dac-danger{color:#e5484d}
|
|
174
|
+
.dac-menu-item.dac-danger:hover{background:rgba(229,72,77,.1)}
|
|
175
|
+
.dac-list{display:flex;flex-direction:column;gap:8px}
|
|
176
|
+
.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}
|
|
177
|
+
.dac-row:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-border-l2))}
|
|
178
|
+
.dac-row-main{min-width:0;display:flex;flex-direction:column;gap:4px}
|
|
179
|
+
.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}
|
|
180
|
+
.dac-row-date{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
|
|
181
|
+
.dac-row-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}
|
|
182
|
+
.dac-unarchive{border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;padding:5px 14px;cursor:pointer;transition:background .15s,border-color .15s}
|
|
183
|
+
.dac-unarchive:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
184
|
+
.dac-unarchive:disabled{opacity:.45;cursor:default}
|
|
185
|
+
.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}
|
|
186
|
+
.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}
|
|
187
|
+
.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}
|
|
188
|
+
.dac-notice{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid rgba(229,72,77,.45);border-radius:10px;background:rgba(229,72,77,.08);color:#e5484d;font-size:12px;line-height:18px;padding:8px 12px}
|
|
189
|
+
.dac-notice button{border:none;background:transparent;color:inherit;cursor:pointer;font:inherit;padding:0;display:inline-flex}
|
|
190
|
+
.dac-confirm-overlay{position:fixed;inset:0;z-index:1400;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4)}
|
|
191
|
+
.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}
|
|
192
|
+
.dac-confirm-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:500;line-height:24px}
|
|
193
|
+
.dac-confirm-body{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:21px}
|
|
194
|
+
.dac-confirm-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:4px}
|
|
195
|
+
.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}
|
|
196
|
+
.dac-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
|
|
197
|
+
.dac-btn-danger{border:none;border-radius:9px;background:#e5484d;color:#fff;font:inherit;font-size:13px;line-height:20px;padding:6px 14px;cursor:pointer}
|
|
198
|
+
.dac-btn-danger:hover:not(:disabled){background:#d13438}
|
|
199
|
+
.dac-btn-danger:disabled{opacity:.5;cursor:default}
|
|
200
|
+
.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}
|
|
201
|
+
@keyframes dac-rotate{to{transform:rotate(360deg)}}
|
|
202
|
+
`;
|
|
203
|
+
|
|
204
|
+
function ensureStyle() {
|
|
205
|
+
if (typeof document === "undefined") return;
|
|
206
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
207
|
+
const style = document.createElement("style");
|
|
208
|
+
style.id = STYLE_ID;
|
|
209
|
+
style.setAttribute("data-plugin-css", "dsh-archived-chats");
|
|
210
|
+
style.textContent = CSS;
|
|
211
|
+
document.head.appendChild(style);
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
|
|
215
|
+
//#region settings-nav icon patch
|
|
216
|
+
// The settings shell hardcodes nav icons by section id (models /
|
|
217
|
+
// agent-presets / plugins, gear for everything else). A registrant cannot
|
|
218
|
+
// supply its own glyph, so after the shell renders we rewrite OUR nav
|
|
219
|
+
// button's icon in place — mutating the existing <svg> (never replacing
|
|
220
|
+
// React-managed nodes) and re-marking through dataset, so locale
|
|
221
|
+
// re-renders and dialog reopens simply get re-patched by the observer.
|
|
222
|
+
const NAV_LABELS = ["已归档的聊天", "Archived Chats"];
|
|
223
|
+
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>';
|
|
224
|
+
|
|
225
|
+
function patchNavIcons() {
|
|
226
|
+
if (typeof document === "undefined" || document.body === null) return;
|
|
227
|
+
const dialogs = document.querySelectorAll('[role="dialog"]');
|
|
228
|
+
for (const dialog of dialogs) {
|
|
229
|
+
const buttons = dialog.querySelectorAll("nav button");
|
|
230
|
+
for (const button of buttons) {
|
|
231
|
+
const text = button.textContent ?? "";
|
|
232
|
+
if (!NAV_LABELS.some((label) => text.includes(label))) continue;
|
|
233
|
+
const svg = button.querySelector("svg");
|
|
234
|
+
if (svg === null || svg.dataset.dacPatched === "1") continue;
|
|
235
|
+
svg.dataset.dacPatched = "1";
|
|
236
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
237
|
+
svg.setAttribute("fill", "none");
|
|
238
|
+
svg.setAttribute("stroke", "currentColor");
|
|
239
|
+
svg.setAttribute("stroke-width", "1.5");
|
|
240
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
241
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
242
|
+
svg.innerHTML = ARCHIVE_ICON_INNER;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let navObserver = null;
|
|
248
|
+
|
|
249
|
+
function startNavIconPatch() {
|
|
250
|
+
patchNavIcons();
|
|
251
|
+
const Observer = typeof window !== "undefined" ? window.MutationObserver : void 0;
|
|
252
|
+
if (Observer === void 0 || typeof document === "undefined" || document.body === null) return;
|
|
253
|
+
if (navObserver !== null) return;
|
|
254
|
+
navObserver = new Observer(() => patchNavIcons());
|
|
255
|
+
navObserver.observe(document.body, { childList: true, subtree: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function stopNavIconPatch() {
|
|
259
|
+
navObserver?.disconnect();
|
|
260
|
+
navObserver = null;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
263
|
+
|
|
264
|
+
//#region api
|
|
265
|
+
async function fetchState() {
|
|
266
|
+
const res = await fetch(`${API_BASE}/state`, { cache: "no-store" });
|
|
267
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
268
|
+
const body = await res.json();
|
|
269
|
+
return Array.isArray(body.sessions) ? body.sessions : [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function post(path, body) {
|
|
273
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: { "content-type": "application/json", [GUARD_HEADER]: "1" },
|
|
276
|
+
body: JSON.stringify(body)
|
|
277
|
+
});
|
|
278
|
+
const parsed = await res.json().catch(() => ({}));
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
const error = new Error(parsed.message || `HTTP ${res.status}`);
|
|
281
|
+
error.body = parsed;
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
return parsed;
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
|
|
288
|
+
//#region icons (inline SVG, stroke = currentColor)
|
|
289
|
+
function svgProps(size) {
|
|
290
|
+
return { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true" };
|
|
291
|
+
}
|
|
292
|
+
function IconSearch({ size = 15 }) {
|
|
293
|
+
return (0, jsx.jsxs)("svg", { ...svgProps(size), children: [(0, jsx.jsx)("circle", { cx: 11, cy: 11, r: 7 }), (0, jsx.jsx)("path", { d: "m20 20-3.8-3.8" })] });
|
|
294
|
+
}
|
|
295
|
+
function IconFolder({ size = 15 }) {
|
|
296
|
+
return (0, jsx.jsx)("svg", { ...svgProps(size), children: (0, jsx.jsx)("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" }) });
|
|
297
|
+
}
|
|
298
|
+
function IconTrash({ size = 14 }) {
|
|
299
|
+
return (0, jsx.jsxs)("svg", { ...svgProps(size), children: [(0, jsx.jsx)("path", { d: "M4 7h16" }), (0, jsx.jsx)("path", { d: "M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" }), (0, jsx.jsx)("path", { d: "M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12" }), (0, jsx.jsx)("path", { d: "M10 11v6M14 11v6" })] });
|
|
300
|
+
}
|
|
301
|
+
function IconDots({ size = 15 }) {
|
|
302
|
+
return (0, jsx.jsxs)("svg", { ...svgProps(size), fill: "currentColor", stroke: "none", children: [(0, jsx.jsx)("circle", { cx: 5, cy: 12, r: 1.6 }), (0, jsx.jsx)("circle", { cx: 12, cy: 12, r: 1.6 }), (0, jsx.jsx)("circle", { cx: 19, cy: 12, r: 1.6 })] });
|
|
303
|
+
}
|
|
304
|
+
function IconChevron({ size = 13 }) {
|
|
305
|
+
return (0, jsx.jsx)("svg", { ...svgProps(size), children: (0, jsx.jsx)("path", { d: "m6 9 6 6 6-6" }) });
|
|
306
|
+
}
|
|
307
|
+
function IconArchive({ size = 40 }) {
|
|
308
|
+
return (0, jsx.jsxs)("svg", { ...svgProps(size), strokeWidth: 1.2, children: [(0, jsx.jsx)("rect", { x: 3, y: 4, width: 18, height: 5, rx: 1 }), (0, jsx.jsx)("path", { d: "M5 9v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9" }), (0, jsx.jsx)("path", { d: "M10 13h4" })] });
|
|
309
|
+
}
|
|
310
|
+
function IconClose({ size = 12 }) {
|
|
311
|
+
return (0, jsx.jsx)("svg", { ...svgProps(size), children: (0, jsx.jsx)("path", { d: "M6 6l12 12M18 6 6 18" }) });
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
|
|
315
|
+
//#region components
|
|
316
|
+
function Select({ value, onChange, options, ariaLabel }) {
|
|
317
|
+
return (0, jsx.jsxs)("span", {
|
|
318
|
+
className: "dac-select-wrap",
|
|
319
|
+
children: [
|
|
320
|
+
(0, jsx.jsx)("select", {
|
|
321
|
+
className: "dac-select",
|
|
322
|
+
value,
|
|
323
|
+
"aria-label": ariaLabel,
|
|
324
|
+
onChange: (e) => onChange(e.target.value),
|
|
325
|
+
children: options.map((o) => (0, jsx.jsx)("option", { value: o.value, children: o.label }, o.value))
|
|
326
|
+
}),
|
|
327
|
+
(0, jsx.jsx)("span", { className: "dac-chevron", children: (0, jsx.jsx)(IconChevron, {}) })
|
|
328
|
+
]
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function ConfirmDialog({ title, body, confirmLabel, cancelLabel, busy, onConfirm, onCancel }) {
|
|
333
|
+
_react.useEffect(() => {
|
|
334
|
+
const onKey = (e) => { if (e.key === "Escape") onCancel(); };
|
|
335
|
+
document.addEventListener("keydown", onKey);
|
|
336
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
337
|
+
}, [onCancel]);
|
|
338
|
+
return (0, jsx.jsx)("div", {
|
|
339
|
+
className: "dac-confirm-overlay",
|
|
340
|
+
onClick: onCancel,
|
|
341
|
+
children: (0, jsx.jsxs)("div", {
|
|
342
|
+
className: "dac-confirm",
|
|
343
|
+
role: "alertdialog",
|
|
344
|
+
"aria-modal": "true",
|
|
345
|
+
onClick: (e) => e.stopPropagation(),
|
|
346
|
+
children: [
|
|
347
|
+
(0, jsx.jsx)("div", { className: "dac-confirm-title", children: title }),
|
|
348
|
+
(0, jsx.jsx)("div", { className: "dac-confirm-body", children: body }),
|
|
349
|
+
(0, jsx.jsxs)("div", {
|
|
350
|
+
className: "dac-confirm-actions",
|
|
351
|
+
children: [
|
|
352
|
+
(0, jsx.jsx)("button", { type: "button", className: "dac-btn", onClick: onCancel, children: cancelLabel }),
|
|
353
|
+
(0, jsx.jsx)("button", { type: "button", className: "dac-btn-danger", disabled: busy, onClick: onConfirm, children: confirmLabel })
|
|
354
|
+
]
|
|
355
|
+
})
|
|
356
|
+
]
|
|
357
|
+
})
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function GroupSection({ group, t, collapsed, onToggleCollapsed, menuOpen, onToggleMenu, onUnarchive, onDelete, busy }) {
|
|
362
|
+
const wrapRef = _react.useRef(null);
|
|
363
|
+
_react.useEffect(() => {
|
|
364
|
+
if (!menuOpen) return void 0;
|
|
365
|
+
const onDown = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) onToggleMenu(null); };
|
|
366
|
+
document.addEventListener("mousedown", onDown);
|
|
367
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
368
|
+
}, [menuOpen, onToggleMenu]);
|
|
369
|
+
const ids = group.items.map((s) => s.id);
|
|
370
|
+
return (0, jsx.jsxs)("div", {
|
|
371
|
+
className: "dac-group",
|
|
372
|
+
children: [
|
|
373
|
+
(0, jsx.jsxs)("div", {
|
|
374
|
+
className: "dac-group-head",
|
|
375
|
+
ref: wrapRef,
|
|
376
|
+
children: [
|
|
377
|
+
(0, jsx.jsxs)("button", {
|
|
378
|
+
type: "button",
|
|
379
|
+
className: "dac-group-toggle",
|
|
380
|
+
"aria-expanded": collapsed !== true,
|
|
381
|
+
"aria-label": collapsed === true ? t("group.expand") : t("group.collapse"),
|
|
382
|
+
onClick: () => onToggleCollapsed(group.key),
|
|
383
|
+
children: [
|
|
384
|
+
(0, jsx.jsx)("span", { className: collapsed === true ? "dac-chev" : "dac-chev open", children: (0, jsx.jsx)(IconChevron, {}) }),
|
|
385
|
+
(0, jsx.jsx)(IconFolder, {}),
|
|
386
|
+
(0, jsx.jsx)("span", { children: group.title })
|
|
387
|
+
]
|
|
388
|
+
}),
|
|
389
|
+
(0, jsx.jsxs)("div", {
|
|
390
|
+
className: "dac-group-side",
|
|
391
|
+
children: [
|
|
392
|
+
(0, jsx.jsx)("span", { className: "dac-count", children: chatsCount(t, group.items.length) }),
|
|
393
|
+
(0, jsx.jsx)("button", {
|
|
394
|
+
type: "button",
|
|
395
|
+
className: "dac-iconbtn",
|
|
396
|
+
"aria-label": "…",
|
|
397
|
+
onClick: () => onToggleMenu(menuOpen ? null : group.key),
|
|
398
|
+
children: (0, jsx.jsx)(IconDots, {})
|
|
399
|
+
})
|
|
400
|
+
]
|
|
401
|
+
}),
|
|
402
|
+
menuOpen && (0, jsx.jsxs)("div", {
|
|
403
|
+
className: "dac-menu",
|
|
404
|
+
role: "menu",
|
|
405
|
+
children: [
|
|
406
|
+
(0, jsx.jsx)("button", {
|
|
407
|
+
type: "button",
|
|
408
|
+
className: "dac-menu-item",
|
|
409
|
+
role: "menuitem",
|
|
410
|
+
onClick: () => { onToggleMenu(null); onUnarchive(ids); },
|
|
411
|
+
children: t("menu.unarchiveAll")
|
|
412
|
+
}),
|
|
413
|
+
(0, jsx.jsx)("button", {
|
|
414
|
+
type: "button",
|
|
415
|
+
className: "dac-menu-item dac-danger",
|
|
416
|
+
role: "menuitem",
|
|
417
|
+
onClick: () => { onToggleMenu(null); onDelete(ids, group.title); },
|
|
418
|
+
children: t("menu.deleteAll")
|
|
419
|
+
})
|
|
420
|
+
]
|
|
421
|
+
})
|
|
422
|
+
]
|
|
423
|
+
}),
|
|
424
|
+
collapsed !== true && (0, jsx.jsx)("div", {
|
|
425
|
+
className: "dac-list",
|
|
426
|
+
children: group.items.map((session) => (0, jsx.jsxs)("div", {
|
|
427
|
+
className: "dac-row",
|
|
428
|
+
children: [
|
|
429
|
+
(0, jsx.jsxs)("div", {
|
|
430
|
+
className: "dac-row-main",
|
|
431
|
+
children: [
|
|
432
|
+
(0, jsx.jsx)("div", { className: "dac-row-title", children: session.title ?? t("chat.untitled") }),
|
|
433
|
+
(0, jsx.jsx)("div", { className: "dac-row-date", children: formatDate(t, session.createdAt) })
|
|
434
|
+
]
|
|
435
|
+
}),
|
|
436
|
+
(0, jsx.jsxs)("div", {
|
|
437
|
+
className: "dac-row-actions",
|
|
438
|
+
children: [
|
|
439
|
+
(0, jsx.jsx)("button", {
|
|
440
|
+
type: "button",
|
|
441
|
+
className: "dac-iconbtn dac-danger",
|
|
442
|
+
"aria-label": t("menu.deleteAll"),
|
|
443
|
+
disabled: busy[session.id] === true,
|
|
444
|
+
onClick: () => onDelete([session.id], null),
|
|
445
|
+
children: (0, jsx.jsx)(IconTrash, {})
|
|
446
|
+
}),
|
|
447
|
+
(0, jsx.jsx)("button", {
|
|
448
|
+
type: "button",
|
|
449
|
+
className: "dac-unarchive",
|
|
450
|
+
disabled: busy[session.id] === true,
|
|
451
|
+
onClick: () => onUnarchive([session.id]),
|
|
452
|
+
children: t("chat.unarchive")
|
|
453
|
+
})
|
|
454
|
+
]
|
|
455
|
+
})
|
|
456
|
+
]
|
|
457
|
+
}, session.id))
|
|
458
|
+
})
|
|
459
|
+
]
|
|
460
|
+
}, group.key);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** The settings section page: archived chats, CodeX-layout. */
|
|
464
|
+
function ArchivedChatsSection({ t, refreshSidebar }) {
|
|
465
|
+
const [sessions, setSessions] = _react.useState(null);
|
|
466
|
+
const [loadError, setLoadError] = _react.useState(null);
|
|
467
|
+
const [query, setQuery] = _react.useState("");
|
|
468
|
+
const [typeFilter, setTypeFilter] = _react.useState("all");
|
|
469
|
+
const [projectFilter, setProjectFilter] = _react.useState("all");
|
|
470
|
+
const [menuFor, setMenuFor] = _react.useState(null);
|
|
471
|
+
const [confirm, setConfirm] = _react.useState(null);
|
|
472
|
+
const [busy, setBusy] = _react.useState({});
|
|
473
|
+
const [notice, setNotice] = _react.useState(null);
|
|
474
|
+
const [collapsed, setCollapsed] = _react.useState(readCollapsed);
|
|
475
|
+
|
|
476
|
+
const toggleCollapsed = (key) => {
|
|
477
|
+
setCollapsed((prev) => {
|
|
478
|
+
const next = { ...prev };
|
|
479
|
+
if (next[key] === true) delete next[key];
|
|
480
|
+
else next[key] = true;
|
|
481
|
+
writeCollapsed(next);
|
|
482
|
+
return next;
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const refresh = _react.useCallback(async () => {
|
|
487
|
+
setLoadError(null);
|
|
488
|
+
try {
|
|
489
|
+
setSessions(await fetchState());
|
|
490
|
+
} catch (error) {
|
|
491
|
+
setLoadError(error);
|
|
492
|
+
}
|
|
493
|
+
}, []);
|
|
494
|
+
_react.useEffect(() => { refresh(); }, [refresh]);
|
|
495
|
+
|
|
496
|
+
const markBusy = (ids, value) => {
|
|
497
|
+
setBusy((prev) => {
|
|
498
|
+
const next = { ...prev };
|
|
499
|
+
for (const id of ids) {
|
|
500
|
+
if (value) next[id] = true;
|
|
501
|
+
else delete next[id];
|
|
502
|
+
}
|
|
503
|
+
return next;
|
|
504
|
+
});
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const unarchive = async (ids) => {
|
|
508
|
+
if (ids.length === 0) return;
|
|
509
|
+
markBusy(ids, true);
|
|
510
|
+
setNotice(null);
|
|
511
|
+
try {
|
|
512
|
+
await post("/unarchive-all", { sessionIds: ids });
|
|
513
|
+
setSessions((prev) => (prev ?? []).filter((s) => !ids.includes(s.id)));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
setNotice(String(error.message ?? error));
|
|
516
|
+
} finally {
|
|
517
|
+
markBusy(ids, false);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const runDelete = async (ids) => {
|
|
522
|
+
if (ids.length === 0) return;
|
|
523
|
+
markBusy(ids, true);
|
|
524
|
+
setNotice(null);
|
|
525
|
+
let changed = false;
|
|
526
|
+
try {
|
|
527
|
+
const result = await post("/delete-all", { sessionIds: ids });
|
|
528
|
+
const deleted = result.deleted ?? [];
|
|
529
|
+
const pending = result.pending ?? [];
|
|
530
|
+
const failed = result.failed ?? [];
|
|
531
|
+
if (deleted.length > 0 || pending.length > 0) {
|
|
532
|
+
changed = true;
|
|
533
|
+
// Pending rows leave the list too: the deletion is accepted,
|
|
534
|
+
// the session is parked and excluded from /state until the
|
|
535
|
+
// next-boot sweep physically removes it.
|
|
536
|
+
setSessions((prev) => (prev ?? []).filter((s) => !deleted.includes(s.id) && !pending.includes(s.id)));
|
|
537
|
+
}
|
|
538
|
+
if (failed.length > 0) setNotice(failureText(t, failed));
|
|
539
|
+
else if (pending.length > 0) setNotice(pendingText(t, pending.length));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
const deleted = error.body?.deleted ?? [];
|
|
542
|
+
const pending = error.body?.pending ?? [];
|
|
543
|
+
const failed = error.body?.failed ?? [];
|
|
544
|
+
if (deleted.length > 0 || pending.length > 0) {
|
|
545
|
+
changed = true;
|
|
546
|
+
setSessions((prev) => (prev ?? []).filter((s) => !deleted.includes(s.id) && !pending.includes(s.id)));
|
|
547
|
+
}
|
|
548
|
+
setNotice(failed.length > 0 ? failureText(t, failed) : String(error.message ?? error));
|
|
549
|
+
} finally {
|
|
550
|
+
markBusy(ids, false);
|
|
551
|
+
setConfirm(null);
|
|
552
|
+
// The stock wire has no "persisted session deleted" frame, so the
|
|
553
|
+
// sidebar keeps stale summary rows until it re-baselines. Pull a
|
|
554
|
+
// fresh session.list: deleted sessions are gone from persistence
|
|
555
|
+
// (and were never live), so the main page drops them at once.
|
|
556
|
+
if (changed) refreshSidebar?.();
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const askDelete = (ids, groupName) => {
|
|
561
|
+
if (ids.length === 0) return;
|
|
562
|
+
if (ids.length === 1 && groupName === null) {
|
|
563
|
+
setConfirm({ title: t("confirm.deleteOne.title"), body: t("confirm.deleteOne.body"), ids });
|
|
564
|
+
} else if (groupName !== null && groupName !== void 0) {
|
|
565
|
+
setConfirm({ title: t("confirm.deleteGroup.title"), body: deleteGroupBody(t, groupName, ids.length), ids });
|
|
566
|
+
} else {
|
|
567
|
+
setConfirm({ title: t("confirm.deleteAll.title"), body: deleteAllBody(t, ids.length), ids });
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const projects = _react.useMemo(() => {
|
|
572
|
+
const seen = new Map();
|
|
573
|
+
for (const s of sessions ?? []) {
|
|
574
|
+
const key = s.workspaceId ?? "ungrouped";
|
|
575
|
+
if (!seen.has(key)) seen.set(key, s.workspaceTitle ?? null);
|
|
576
|
+
}
|
|
577
|
+
return [...seen.entries()].map(([value, title]) => ({ value, title }));
|
|
578
|
+
}, [sessions]);
|
|
579
|
+
|
|
580
|
+
const groups = _react.useMemo(() => {
|
|
581
|
+
const q = query.trim().toLowerCase();
|
|
582
|
+
const visible = (sessions ?? []).filter((s) => {
|
|
583
|
+
if (projectFilter !== "all" && (s.workspaceId ?? "ungrouped") !== projectFilter) return false;
|
|
584
|
+
if (typeFilter === "normal" && s.origin === "subagent") return false;
|
|
585
|
+
if (typeFilter === "subagent" && s.origin !== "subagent") return false;
|
|
586
|
+
if (q !== "" && !(s.title ?? "").toLowerCase().includes(q)) return false;
|
|
587
|
+
return true;
|
|
588
|
+
});
|
|
589
|
+
const byKey = new Map();
|
|
590
|
+
for (const s of visible) {
|
|
591
|
+
const key = s.workspaceId ?? "ungrouped";
|
|
592
|
+
if (!byKey.has(key)) byKey.set(key, { key, title: s.workspaceTitle ?? t("group.noProject"), items: [] });
|
|
593
|
+
byKey.get(key).items.push(s);
|
|
594
|
+
}
|
|
595
|
+
const list = [...byKey.values()];
|
|
596
|
+
for (const g of list) g.items.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
597
|
+
list.sort((a, b) => {
|
|
598
|
+
if ((a.key === "ungrouped") !== (b.key === "ungrouped")) return a.key === "ungrouped" ? 1 : -1;
|
|
599
|
+
return (b.items[0]?.createdAt ?? 0) - (a.items[0]?.createdAt ?? 0);
|
|
600
|
+
});
|
|
601
|
+
return list;
|
|
602
|
+
}, [sessions, query, projectFilter, typeFilter, t]);
|
|
603
|
+
|
|
604
|
+
if (sessions === null && loadError === null) {
|
|
605
|
+
return (0, jsx.jsx)("div", { className: "dac-center", children: [(0, jsx.jsx)("span", { className: "dac-spin" }), (0, jsx.jsx)("span", { children: t("state.loading") })] });
|
|
606
|
+
}
|
|
607
|
+
if (loadError !== null && sessions === null) {
|
|
608
|
+
return (0, jsx.jsxs)("div", {
|
|
609
|
+
className: "dac-center",
|
|
610
|
+
children: [
|
|
611
|
+
(0, jsx.jsx)("span", { children: `${t("state.error")}: ${String(loadError.message ?? loadError)}` }),
|
|
612
|
+
(0, jsx.jsx)("button", { type: "button", className: "dac-retry", onClick: refresh, children: t("state.retry") })
|
|
613
|
+
]
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const allIds = (sessions ?? []).map((s) => s.id);
|
|
618
|
+
const filtering = query.trim() !== "" || projectFilter !== "all" || typeFilter !== "all";
|
|
619
|
+
|
|
620
|
+
return (0, jsx.jsxs)("div", {
|
|
621
|
+
className: "dac-page",
|
|
622
|
+
children: [
|
|
623
|
+
(0, jsx.jsxs)("div", {
|
|
624
|
+
className: "dac-head",
|
|
625
|
+
children: [
|
|
626
|
+
(0, jsx.jsx)("div", { className: "dac-title", children: t("page.title") }),
|
|
627
|
+
(0, jsx.jsxs)("button", {
|
|
628
|
+
type: "button",
|
|
629
|
+
className: "dac-deleteall",
|
|
630
|
+
disabled: allIds.length === 0,
|
|
631
|
+
onClick: () => askDelete(allIds, void 0),
|
|
632
|
+
children: [(0, jsx.jsx)(IconTrash, {}), (0, jsx.jsx)("span", { children: t("delete.all") })]
|
|
633
|
+
})
|
|
634
|
+
]
|
|
635
|
+
}),
|
|
636
|
+
(0, jsx.jsxs)("div", {
|
|
637
|
+
className: "dac-search",
|
|
638
|
+
children: [
|
|
639
|
+
(0, jsx.jsx)(IconSearch, {}),
|
|
640
|
+
(0, jsx.jsx)("input", {
|
|
641
|
+
type: "text",
|
|
642
|
+
placeholder: t("search.placeholder"),
|
|
643
|
+
value: query,
|
|
644
|
+
onChange: (e) => setQuery(e.target.value)
|
|
645
|
+
})
|
|
646
|
+
]
|
|
647
|
+
}),
|
|
648
|
+
(0, jsx.jsxs)("div", {
|
|
649
|
+
className: "dac-filters",
|
|
650
|
+
children: [
|
|
651
|
+
(0, jsx.jsx)(Select, {
|
|
652
|
+
value: typeFilter,
|
|
653
|
+
onChange: setTypeFilter,
|
|
654
|
+
ariaLabel: t("filter.allChats"),
|
|
655
|
+
options: [
|
|
656
|
+
{ value: "all", label: t("filter.allChats") },
|
|
657
|
+
{ value: "normal", label: t("filter.normal") },
|
|
658
|
+
{ value: "subagent", label: t("filter.subagent") }
|
|
659
|
+
]
|
|
660
|
+
}),
|
|
661
|
+
(0, jsx.jsx)(Select, {
|
|
662
|
+
value: projectFilter,
|
|
663
|
+
onChange: setProjectFilter,
|
|
664
|
+
ariaLabel: t("filter.allProjects"),
|
|
665
|
+
options: [
|
|
666
|
+
{ value: "all", label: t("filter.allProjects") },
|
|
667
|
+
...projects.map((p) => ({ value: p.value, label: p.title ?? t("group.noProject") }))
|
|
668
|
+
]
|
|
669
|
+
})
|
|
670
|
+
]
|
|
671
|
+
}),
|
|
672
|
+
notice !== null && (0, jsx.jsxs)("div", {
|
|
673
|
+
className: "dac-notice",
|
|
674
|
+
children: [
|
|
675
|
+
(0, jsx.jsx)("span", { children: notice }),
|
|
676
|
+
(0, jsx.jsx)("button", { type: "button", "aria-label": t("notice.dismiss"), onClick: () => setNotice(null), children: (0, jsx.jsx)(IconClose, {}) })
|
|
677
|
+
]
|
|
678
|
+
}),
|
|
679
|
+
(sessions ?? []).length === 0 && (0, jsx.jsxs)("div", {
|
|
680
|
+
className: "dac-empty",
|
|
681
|
+
children: [(0, jsx.jsx)(IconArchive, {}), (0, jsx.jsx)("span", { children: t("state.empty") })]
|
|
682
|
+
}),
|
|
683
|
+
(sessions ?? []).length > 0 && groups.length === 0 && filtering && (0, jsx.jsx)("div", {
|
|
684
|
+
className: "dac-center",
|
|
685
|
+
children: t("state.emptyFiltered")
|
|
686
|
+
}),
|
|
687
|
+
groups.map((group) => (0, jsx.jsx)(GroupSection, {
|
|
688
|
+
group,
|
|
689
|
+
t,
|
|
690
|
+
collapsed: collapsed[group.key] === true,
|
|
691
|
+
onToggleCollapsed: toggleCollapsed,
|
|
692
|
+
menuOpen: menuFor === group.key,
|
|
693
|
+
onToggleMenu: setMenuFor,
|
|
694
|
+
onUnarchive: unarchive,
|
|
695
|
+
onDelete: askDelete,
|
|
696
|
+
busy
|
|
697
|
+
}, group.key)),
|
|
698
|
+
confirm !== null && (0, jsx.jsx)(ConfirmDialog, {
|
|
699
|
+
title: confirm.title,
|
|
700
|
+
body: confirm.body,
|
|
701
|
+
confirmLabel: t("confirm.delete"),
|
|
702
|
+
cancelLabel: t("confirm.cancel"),
|
|
703
|
+
busy: confirm.ids.some((id) => busy[id] === true),
|
|
704
|
+
onConfirm: () => runDelete(confirm.ids),
|
|
705
|
+
onCancel: () => setConfirm(null)
|
|
706
|
+
})
|
|
707
|
+
]
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
|
|
712
|
+
//#region plugin body
|
|
713
|
+
const inject = ["slots", "locale"];
|
|
714
|
+
|
|
715
|
+
function apply(ctx) {
|
|
716
|
+
ctx.effect(() => ctx.locale.register(SETTINGS_NS, { zh, en }), "archived-chats: dictionaries");
|
|
717
|
+
ensureStyle();
|
|
718
|
+
ctx.effect(() => () => { document.getElementById(STYLE_ID)?.remove(); }, "archived-chats: css cleanup");
|
|
719
|
+
startNavIconPatch();
|
|
720
|
+
ctx.effect(() => () => { stopNavIconPatch(); }, "archived-chats: nav-icon observer cleanup");
|
|
721
|
+
|
|
722
|
+
const t = ctx.locale.bind(SETTINGS_NS);
|
|
723
|
+
// Sidebar convergence after deletes: the concrete client sessions
|
|
724
|
+
// service re-pulls its session.list baseline on refresh(). Optional
|
|
725
|
+
// on purpose — an older runtime without it still deletes correctly,
|
|
726
|
+
// the main page just needs a reload to catch up.
|
|
727
|
+
const refreshSidebar = () => {
|
|
728
|
+
try { ctx.get("sessions")?.refresh?.(); } catch { /* sidebar converges on next reconnect */ }
|
|
729
|
+
};
|
|
730
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
731
|
+
name: "settings.section",
|
|
732
|
+
id: "archived-chats",
|
|
733
|
+
order: 30,
|
|
734
|
+
label: () => t("nav"),
|
|
735
|
+
locale: SETTINGS_NS,
|
|
736
|
+
inject: () => ({ refreshSidebar })
|
|
737
|
+
}, ArchivedChatsSection));
|
|
738
|
+
}
|
|
739
|
+
//#endregion
|
|
740
|
+
|
|
741
|
+
exports.SETTINGS_NS = SETTINGS_NS;
|
|
742
|
+
exports.apply = apply;
|
|
743
|
+
exports.inject = inject;
|
|
744
|
+
return module.exports;
|
|
745
|
+
}
|
|
746
|
+
});
|