dsh-recall-plugin 2.3.1 → 2.3.3

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/lib/client.js CHANGED
@@ -1,1484 +1,1689 @@
1
- // src/client/css.js
2
- var CSS = [
3
- ".dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}",
4
- ".dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}",
5
- ".dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}",
6
- ".dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}",
7
- ".dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}",
8
- ".dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}",
9
- ".dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}",
10
- ".dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
11
- "@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}",
12
- ".dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}",
13
- ".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
14
- ".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
15
- ".dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}",
16
- ".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}",
17
- ".dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}",
18
- ".dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}",
19
- ".dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
20
- ".dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}",
21
- ".dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}",
22
- ".dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}",
23
- ".dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
24
- ".dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}",
25
- ".dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}",
26
- ".dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}",
27
- ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
28
- ".dsh-recall-toast.dsh-recall-toast-in{opacity:1}",
29
- ".dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
30
- ".dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}",
31
- ".dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
32
- ".dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
33
- ".dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:20px;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}",
34
- ".dsh-recall-ex-quick{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
35
- ".dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:20px}",
36
- ".dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:18px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
37
- ".dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}",
38
- ".dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
39
- ".dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}",
40
- ".dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}",
41
- ".dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
42
- ".dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}",
43
- ".dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}",
44
- ".dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
45
- ".dsh-recall-tree-toggle{flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:11px;line-height:18px;user-select:none}",
46
- ".dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
47
- ".dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}",
48
- ".dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px;overflow:hidden}",
49
- ".dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}",
50
- ".dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
51
- ".dsh-recall-tree-meta{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
52
- ".dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:16px;border-left:1px solid var(--dsw-alias-border-l1);padding-left:8px}",
53
- ".dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
54
- ".dsh-recall-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s;display:flex;flex-direction:column;text-align:left}",
55
- ".dsh-recall-card.dsh-recall-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
56
- ".dsh-recall-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
57
- ".dsh-recall-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
58
- ".dsh-recall-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
59
- ".dsh-recall-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
60
- ".dsh-recall-cardbtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
61
- ".dsh-recall-cfg-row{display:flex;flex-direction:column;gap:4px}",
62
- ".dsh-recall-cfg-line{display:flex;align-items:center;gap:8px}",
63
- ".dsh-recall-cfg-label{flex:none;width:130px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}",
64
- ".dsh-recall-cfg-input{flex:1;min-width:0;box-sizing:border-box;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px}",
65
- ".dsh-recall-cfg-input:disabled{opacity:.5}",
66
- ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;min-height:64px}",
67
- ".dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding-left:138px}",
68
- ".dsh-recall-cfg-tag{flex:none;font-size:11px;line-height:16px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}"
69
- ].join("");
70
-
71
- // src/client/util.js
72
- function clockText(ms) {
73
- try {
74
- if (!ms || isNaN(new Date(ms).getTime())) return "";
75
- const d = new Date(ms);
76
- const now = /* @__PURE__ */ new Date();
77
- const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
78
- const hh = String(d.getHours()).padStart(2, "0");
79
- const mm = String(d.getMinutes()).padStart(2, "0");
80
- return sameDay ? hh + ":" + mm : d.getMonth() + 1 + "/" + d.getDate() + " " + hh + ":" + mm;
81
- } catch (e) {
82
- return "";
83
- }
84
- }
85
- function sizeText(bytes) {
86
- if (!bytes || bytes <= 0) return "0 MB";
87
- if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
88
- if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
89
- return (bytes / 1073741824).toFixed(2) + " GB";
90
- }
91
- function bytesToMb(bytes) {
92
- const n = Number(bytes);
93
- if (!Number.isFinite(n) || n <= 0) return "";
94
- return String(Math.round(n / 1048576 * 100) / 100);
95
- }
96
- function buildTree(list) {
97
- const workspaces = /* @__PURE__ */ new Map();
98
- for (const it of list || []) {
99
- const rootKey = it.root || "unknown-root";
100
- if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || "\u672A\u77E5\u5DE5\u4F5C\u533A", sessions: /* @__PURE__ */ new Map() });
101
- const ws = workspaces.get(rootKey);
102
- const sidKey = it.sessionId || "unknown-session";
103
- if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] });
104
- ws.sessions.get(sidKey).items.push(it);
105
- }
106
- const wsList = Array.from(workspaces.values());
107
- wsList.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
108
- for (const ws of wsList) {
109
- ws.sessions = Array.from(ws.sessions.values());
110
- ws.sessions.sort((a, b) => (a.title || "").localeCompare(b.title || ""));
111
- for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0));
112
- }
113
- return wsList;
114
- }
115
- function buildUtil() {
116
- function api(name, args) {
117
- return fetch("/api/recall/" + name, {
118
- method: "POST",
119
- headers: { "content-type": "application/json" },
120
- body: JSON.stringify(args || {})
121
- }).then((r) => r.json());
122
- }
123
- const CODE_TEXT = {
124
- STALE: "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4",
125
- AGENT_BUSY: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE",
126
- NO_SNAPSHOT: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167",
127
- NO_STORE: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528",
128
- ROLLBACK_FAILED: "\u56DE\u9000\u5931\u8D25"
129
- };
130
- function messageFor(res, fallback) {
131
- if (!res) return fallback;
132
- const code = res && res.code;
133
- if (code && CODE_TEXT[code]) return CODE_TEXT[code];
134
- return res && (res.message || res.error) || fallback;
135
- }
136
- const noticeShown = /* @__PURE__ */ new Set();
137
- function mountToast(text) {
138
- if (typeof document === "undefined") return;
139
- try {
140
- let dismiss = function() {
141
- if (dismissed) return;
142
- dismissed = true;
143
- clearTimeout(timer);
144
- el.classList.remove("dsh-recall-toast-in");
145
- setTimeout(() => el.remove(), 300);
146
- };
147
- const el = document.createElement("div");
148
- el.className = "dsh-recall-toast";
149
- const tag = document.createElement("span");
150
- tag.className = "dsh-recall-toast-tag";
151
- tag.textContent = "\u64A4\u56DE\u63D2\u4EF6";
152
- const body = document.createElement("span");
153
- body.textContent = text;
154
- el.appendChild(tag);
155
- el.appendChild(body);
156
- el.addEventListener("click", () => dismiss(), { once: true });
157
- document.body.appendChild(el);
158
- requestAnimationFrame(() => el.classList.add("dsh-recall-toast-in"));
159
- const timer = setTimeout(dismiss, 7e3);
160
- let dismissed = false;
161
- } catch (e) {
162
- }
163
- }
164
- function showNotice(kind, text) {
165
- if (noticeShown.has(kind)) return;
166
- noticeShown.add(kind);
167
- mountToast(text);
168
- }
169
- const toastLastShown = /* @__PURE__ */ new Map();
170
- function showThrottledToast(text) {
171
- const key = String(text).slice(0, 80);
172
- const now = Date.now();
173
- if (now - (toastLastShown.get(key) || 0) < 10 * 60 * 1e3) return;
174
- if (toastLastShown.size > 50) toastLastShown.clear();
175
- toastLastShown.set(key, now);
176
- mountToast(text);
177
- }
178
- const pluginConfig = { refillDraft: true, archiveOriginal: true };
179
- const initMap = /* @__PURE__ */ new Map();
180
- function ensureInit(sessionId) {
181
- if (!sessionId) return Promise.resolve();
182
- const cached = initMap.get(sessionId);
183
- if (cached) return cached;
184
- const done = api("init", { sessionId }).then((res) => {
185
- if (res && res.config && typeof res.config === "object") {
186
- if (typeof res.config.refillDraft === "boolean") pluginConfig.refillDraft = res.config.refillDraft;
187
- if (typeof res.config.archiveOriginal === "boolean") pluginConfig.archiveOriginal = res.config.archiveOriginal;
188
- }
189
- const notice = res && res.notice;
190
- if (notice && notice.unsupported) {
191
- showNotice("unsupported", "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002");
192
- }
193
- if (notice && notice.gitMissing) {
194
- showNotice("git", "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002");
195
- }
196
- if (notice && notice.homeFallback) {
197
- showNotice("home", "home \u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u5FEB\u7167\u5DF2\u964D\u7EA7\u5B58\u50A8\u5230\u9879\u76EE\u5185 .dsh-recall-snapshots \u76EE\u5F55\u3002");
198
- }
199
- }).catch(() => {
200
- initMap.delete(sessionId);
201
- });
202
- initMap.set(sessionId, done);
203
- return done;
204
- }
205
- function writeClipboard(text) {
206
- try {
207
- if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
208
- return navigator.clipboard.writeText(text).then(() => true, () => false);
209
- }
210
- } catch (e) {
211
- }
212
- try {
213
- if (typeof document !== "undefined" && typeof document.execCommand === "function") {
214
- const el = document.createElement("textarea");
215
- el.value = text;
216
- el.setAttribute("readonly", "");
217
- el.style.position = "fixed";
218
- el.style.left = "-9999px";
219
- document.body.appendChild(el);
220
- el.select();
221
- try {
222
- return Promise.resolve(document.execCommand("copy"));
223
- } finally {
224
- el.remove();
225
- }
226
- }
227
- } catch (e) {
228
- }
229
- return Promise.resolve(false);
230
- }
231
- return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, pluginConfig };
232
- }
233
-
234
- // src/client/recall-node.js
235
- var KIND_INFO = {
236
- modified: { label: "\u4FEE\u6539", cls: "modified" },
237
- restored: { label: "\u6062\u590D", cls: "restored" },
238
- added: { label: "\u5220\u9664", cls: "added" }
239
- };
240
- function summaryText(counts) {
241
- const parts = [];
242
- for (const kind of Object.keys(KIND_INFO)) {
243
- if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + " " + counts[kind]);
244
- }
245
- return parts.join(" \xB7 ");
246
- }
247
- function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
248
- const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor } = util;
249
- function CopyIcon() {
250
- return React.createElement(
251
- "svg",
252
- { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": true },
253
- React.createElement("rect", { x: 5.5, y: 5.5, width: 8, height: 8, rx: 1.5 }),
254
- React.createElement("path", { d: "M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5" })
255
- );
256
- }
257
- function CheckIcon() {
258
- return React.createElement(
259
- "svg",
260
- { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
261
- React.createElement("path", { d: "m3 8.5 3.2 3.2L13 5" })
262
- );
263
- }
264
- function UndoIcon() {
265
- return React.createElement(
266
- "svg",
267
- { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
268
- React.createElement("path", { d: "M7.5 3.5 3.5 7.5l4 4" }),
269
- React.createElement("path", { d: "M4.5 7.5h5a3 3 0 0 1 0 6H8" })
270
- );
271
- }
272
- function recallPanel(recall, closePanel, executeRecall) {
273
- if (recall.stage === "loading") {
274
- return React.createElement(
275
- "div",
276
- { className: "dsh-recall-panel" },
277
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u8BA1\u7B97\u53D8\u66F4\u2026")
278
- );
279
- }
280
- if (recall.stage === "error") {
281
- return React.createElement(
282
- "div",
283
- { className: "dsh-recall-panel" },
284
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u65E0\u6CD5\u56DE\u9000"),
285
- React.createElement("div", { className: "dsh-recall-panel-note" }, recall.message || ""),
286
- React.createElement(
287
- "div",
288
- { className: "dsh-recall-panel-actions" },
289
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
290
- )
291
- );
292
- }
293
- if (recall.stage === "confirm") {
294
- const changes = recall.changes || [];
295
- const total = typeof recall.total === "number" ? recall.total : changes.length;
296
- const counts = { modified: 0, restored: 0, added: 0 };
297
- for (const c of changes) {
298
- if (c && counts[c.kind] !== void 0) counts[c.kind]++;
299
- }
300
- const rows = changes.map((c, i) => {
301
- const info = KIND_INFO[c.kind];
302
- return React.createElement(
303
- "div",
304
- { className: "dsh-recall-file", key: i },
305
- React.createElement("span", { className: "dsh-recall-badge dsh-recall-badge-" + (info ? info.cls : "") }, info ? info.label : c.kind || ""),
306
- React.createElement("span", { className: "dsh-recall-rel" }, c.rel || "")
307
- );
308
- });
309
- if (recall.truncated) {
310
- rows.push(React.createElement("div", { className: "dsh-recall-panel-note", key: "truncated" }, "\u2026\u4EC5\u663E\u793A\u524D " + changes.length + " \u6761\uFF0C\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4"));
311
- }
312
- const canRevertChat = typeof recall.cutSeq === "number";
313
- return React.createElement(
314
- "div",
315
- { className: "dsh-recall-panel" },
316
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6574\u6BB5\u56DE\u9000"),
317
- React.createElement(
318
- "div",
319
- { className: "dsh-recall-panel-note" },
320
- "\u5C06\u9879\u76EE\u6062\u590D\u5230" + (recall.time ? " " + clockText2(recall.time) + " " : " ") + "\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4" + (summaryText(counts) ? "\uFF08" + summaryText(counts) + "\uFF09" : "") + "\u3002\u6B64\u64CD\u4F5C\u4F1A\u8986\u76D6\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF1B\u56DE\u9000\u524D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u4E00\u4EFD\u5F53\u524D\u72B6\u6001\u7684\u5B89\u5168\u5FEB\u7167\uFF08\u4E0D\u542B\u5728\u4E0B\u65B9\u6E05\u5355\u5185\uFF09\u3002"
321
- ),
322
- React.createElement(
323
- "div",
324
- { className: "dsh-recall-panel-note" },
325
- canRevertChat ? "\u5BF9\u8BDD\u5C06\u4E00\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5168\u90E8\u5BF9\u8BDD\u4F1A\u4ECE\u5F53\u524D\u89C6\u56FE\u79FB\u9664\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u65E0\u6CD5\u56DE\u9000\u5BF9\u8BDD\uFF1B\u786E\u8BA4\u540E\u4EC5\u56DE\u9000\u9879\u76EE\u6587\u4EF6\u3002"
326
- ),
327
- changes.length > 0 ? React.createElement("div", { className: "dsh-recall-list" }, ...rows) : null,
328
- React.createElement(
329
- "div",
330
- { className: "dsh-recall-panel-actions" },
331
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u53D6\u6D88"),
332
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", onClick: executeRecall }, "\u786E\u8BA4\u56DE\u9000")
333
- )
334
- );
335
- }
336
- if (recall.stage === "executing") {
337
- return React.createElement(
338
- "div",
339
- { className: "dsh-recall-panel" },
340
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u56DE\u9000\u2026")
341
- );
342
- }
343
- if (recall.stage === "done") {
344
- return React.createElement(
345
- "div",
346
- { className: "dsh-recall-panel" },
347
- React.createElement("div", { className: "dsh-recall-panel-title" }, "\u56DE\u9000\u5B8C\u6210"),
348
- React.createElement(
349
- "div",
350
- { className: "dsh-recall-panel-note" },
351
- recall.chatReverted ? "\u9879\u76EE\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u9879\u76EE\u5DF2\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002" + (recall.chatError ? " \u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A" + recall.chatError : "")
352
- ),
353
- React.createElement(
354
- "div",
355
- { className: "dsh-recall-panel-actions" },
356
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
357
- )
358
- );
359
- }
360
- return null;
361
- }
362
- function fillDraft(targetSessionId, draftText) {
363
- if (!draftText || !targetSessionId) return;
364
- let attempts = 0;
365
- const attempt = () => {
366
- try {
367
- const conversation = ctx.get("conversation");
368
- if (conversation && conversation.input && typeof conversation.input.shell === "function") {
369
- const shell = conversation.input.shell(targetSessionId);
370
- if (shell) {
371
- if (shell.actions && typeof shell.actions.setDraft === "function") {
372
- shell.actions.setDraft(draftText);
373
- return;
374
- }
375
- if (typeof shell.setDraft === "function") {
376
- shell.setDraft(draftText);
377
- return;
378
- }
379
- }
380
- }
381
- } catch (e) {
382
- }
383
- if (attempts++ < 8) setTimeout(attempt, 150);
384
- };
385
- attempt();
386
- }
387
- function UserRecallNode(props) {
388
- const node = props && props.node;
389
- const renderMessageImages = props && props.renderMessageImages;
390
- const sessionId = props && props.sessionId;
391
- const data = node && node.data ? node.data : {};
392
- const messageId = node ? String(node.id || node.key || "") : "";
393
- const blocks = Array.isArray(data.content) ? data.content : [];
394
- const text = blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
395
- const imageBlocks = blocks.filter((b) => b && b.type === "image" && b.attachment).map((b) => ({ attachment: b.attachment }));
396
- const rest = blocks.filter((b) => !b || !(b.type === "text" && typeof b.text === "string") && !(b.type === "image" && b.attachment));
397
- const [copied, setCopied] = React.useState(false);
398
- const [hasSnapshot, setHasSnapshot] = React.useState(false);
399
- const [recall, setRecall] = React.useState({ stage: "idle" });
400
- React.useEffect(() => {
401
- let alive = true;
402
- let timer = null;
403
- let attempts = 0;
404
- const RETRY_WINDOW_MS = 5 * 60 * 1e3;
405
- const MAX_ATTEMPTS = 20;
406
- const RETRY_MS = 1e3;
407
- const msgTime = data && typeof data.time === "number" ? data.time : NaN;
408
- const recent = !isNaN(msgTime) && Date.now() - msgTime <= RETRY_WINDOW_MS;
409
- function schedule() {
410
- if (!alive || !messageId) return;
411
- attempts++;
412
- api("snapshot-info", { messageId, sessionId }).then((res) => {
413
- if (!alive) return;
414
- if (res && res.has) {
415
- if (recent && Array.isArray(res.skipped) && res.skipped.length) {
416
- const names = res.skipped.slice(0, 5).join("\u3001") + (res.skipped.length > 5 ? " \u7B49 " + res.skipped.length + " \u9879" : "");
417
- showThrottledToast("\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A" + names + "\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09");
418
- }
419
- setHasSnapshot(true);
420
- return;
421
- }
422
- if (res && res.failed) {
423
- if (recent) showThrottledToast("\u5FEB\u7167\u5931\u8D25\uFF1A" + String(res.error || "\u672A\u77E5\u539F\u56E0").slice(0, 140));
424
- return;
425
- }
426
- if (recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
427
- }).catch(() => {
428
- if (alive && recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
429
- });
430
- }
431
- ensureInit(sessionId).then(() => {
432
- if (!messageId || !alive) return;
433
- schedule();
434
- }).catch(() => {
435
- if (alive && messageId) timer = setTimeout(schedule, RETRY_MS);
436
- });
437
- return () => {
438
- alive = false;
439
- if (timer !== null) clearTimeout(timer);
440
- };
441
- }, [messageId, sessionId]);
442
- const onCopy = () => {
443
- if (copied) return;
444
- writeClipboard(text).then(() => {
445
- setCopied(true);
446
- const timer = ctx.timer;
447
- if (timer && typeof timer.timeout === "function") {
448
- timer.timeout(() => setCopied(false), 1200);
449
- } else {
450
- setTimeout(() => setCopied(false), 1200);
451
- }
452
- });
453
- };
454
- const openPreview = () => {
455
- if (recall.stage === "loading" || recall.stage === "executing") return;
456
- setRecall({ stage: "loading" });
457
- api("preview", { messageId, sessionId }).then((res) => {
458
- if (!res || !res.ok) {
459
- setRecall({ stage: "error", message: messageFor(res, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
460
- return;
461
- }
462
- setRecall({
463
- stage: "confirm",
464
- changes: res.changes || [],
465
- total: typeof res.total === "number" ? res.total : (res.changes || []).length,
466
- truncated: Boolean(res.truncated),
467
- treeId: res.treeId || null,
468
- time: res.time || null,
469
- cutSeq: typeof res.cutSeq === "number" ? res.cutSeq : null
470
- });
471
- }).catch((error) => {
472
- setRecall({ stage: "error", message: String(error) });
473
- });
474
- };
475
- const executeRecall = () => {
476
- if (recall.stage !== "confirm") return;
477
- const changes = recall.changes || [];
478
- const previewCut = typeof recall.cutSeq === "number" ? recall.cutSeq : null;
479
- const previewTotal = typeof recall.total === "number" ? recall.total : changes.length;
480
- setRecall({ stage: "executing", changes });
481
- api("execute", { messageId, sessionId, previewTotal, previewTreeId: recall.treeId || void 0, previewAt: Date.now() }).then(async (res) => {
482
- if (!res || !res.ok) {
483
- if (res && res.code === "STALE") {
484
- setRecall({ stage: "loading" });
485
- api("preview", { messageId, sessionId }).then((res2) => {
486
- if (!res2 || !res2.ok) {
487
- setRecall({ stage: "error", message: messageFor(res2, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
488
- return;
489
- }
490
- setRecall({
491
- stage: "confirm",
492
- changes: res2.changes || [],
493
- total: typeof res2.total === "number" ? res2.total : (res2.changes || []).length,
494
- truncated: Boolean(res2.truncated),
495
- treeId: res2.treeId || null,
496
- time: res2.time || null,
497
- cutSeq: typeof res2.cutSeq === "number" ? res2.cutSeq : null
498
- });
499
- }).catch((error) => {
500
- setRecall({ stage: "error", message: String(error) });
501
- });
502
- return;
503
- }
504
- setRecall({ stage: "error", message: messageFor(res, "\u56DE\u9000\u5931\u8D25") });
505
- return;
506
- }
507
- const cutSeq = typeof res.cutSeq === "number" ? res.cutSeq : previewCut;
508
- let chatReverted = false;
509
- let chatError = "";
510
- let fillTarget = sessionId;
511
- if (cutSeq !== null && sessionsSvc && typeof sessionsSvc.fork === "function") {
512
- try {
513
- const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq });
514
- if (childId) {
515
- if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
516
- chatReverted = true;
517
- fillTarget = childId;
518
- api("lineage-record", { childId, parentId: sessionId }).catch(() => {
519
- });
520
- if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === "function") {
521
- workspacesSvc.archiveSession(sessionId).catch(() => {
522
- });
523
- }
524
- } else {
525
- chatError = "\u672A\u8FD4\u56DE\u65B0\u4F1A\u8BDD";
526
- }
527
- } catch (error) {
528
- chatError = String(error);
529
- }
530
- }
531
- if (pluginConfig.refillDraft) fillDraft(fillTarget, text);
532
- setHasSnapshot(false);
533
- setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
534
- }).catch((error) => {
535
- setRecall({ stage: "error", message: String(error) });
536
- });
537
- };
538
- const closePanel = () => setRecall({ stage: "idle" });
539
- const bubbleChildren = [];
540
- if (imageBlocks.length && typeof renderMessageImages === "function") {
541
- bubbleChildren.push(React.createElement(
542
- React.Fragment,
543
- { key: "images" },
544
- renderMessageImages({ images: imageBlocks, align: "end" })
545
- ));
546
- }
547
- if (text !== "") bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" }, text));
548
- for (let i = 0; i < rest.length; i++) {
549
- bubbleChildren.push(React.createElement("pre", { className: "dsh-recall-json", key: "rest-" + i }, JSON.stringify(rest[i], null, 2)));
550
- }
551
- const actions = [];
552
- actions.push(React.createElement("span", { className: "dsh-recall-time", key: "time" }, clockText2(data.time)));
553
- actions.push(React.createElement("button", {
554
- key: "copy",
555
- type: "button",
556
- className: "dsh-recall-action",
557
- "aria-label": copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
558
- title: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
559
- onClick: onCopy
560
- }, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})));
561
- if (hasSnapshot) {
562
- actions.push(React.createElement("button", {
563
- key: "recall",
564
- type: "button",
565
- className: "dsh-recall-action",
566
- "aria-label": "\u64A4\u56DE",
567
- title: "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
568
- onClick: openPreview
569
- }, React.createElement(UndoIcon, {})));
570
- }
571
- return React.createElement(
572
- "div",
573
- { className: "dsh-recall-row", "data-time-hover-root": true },
574
- bubbleChildren.length > 0 ? React.createElement("div", { className: "dsh-recall-stack", key: "stack" }, ...bubbleChildren) : null,
575
- React.createElement("div", { className: "dsh-recall-actions", key: "actions" }, ...actions),
576
- recallPanel(recall, closePanel, executeRecall)
577
- );
578
- }
579
- return { UserRecallNode };
580
- }
581
-
582
- // src/client/settings-cards.js
583
- function groupByLineage(ids, lineage) {
584
- const childOf = /* @__PURE__ */ new Map();
585
- const childrenOf = /* @__PURE__ */ new Map();
586
- for (const e of lineage || []) {
587
- if (e && e.childId && e.parentId) {
588
- const child = String(e.childId);
589
- const parent = String(e.parentId);
590
- childOf.set(child, parent);
591
- const kids = childrenOf.get(parent) || [];
592
- kids.push(child);
593
- childrenOf.set(parent, kids);
594
- }
595
- }
596
- const idSet = new Set((ids || []).map(String));
597
- const result = /* @__PURE__ */ new Map();
598
- const assigned = /* @__PURE__ */ new Set();
599
- for (const id of idSet) {
600
- if (assigned.has(id)) continue;
601
- let root = id;
602
- const seen = /* @__PURE__ */ new Set();
603
- while (childOf.has(root) && idSet.has(childOf.get(root)) && !seen.has(root)) {
604
- seen.add(root);
605
- root = childOf.get(root);
606
- }
607
- const chain = [];
608
- const queue = [root];
609
- while (queue.length) {
610
- const cur = queue.shift();
611
- if (!cur || !idSet.has(cur) || assigned.has(cur)) continue;
612
- chain.push(cur);
613
- assigned.add(cur);
614
- for (const k of childrenOf.get(cur) || []) queue.push(k);
615
- }
616
- if (chain.length > 1) {
617
- chain.forEach((sid, i) => result.set(sid, { family: chain, index: i + 1 }));
618
- }
619
- }
620
- return result;
621
- }
622
- function buildSettingsCards(React, util, sessionsSvc) {
623
- const { api, clockText: clockText2, sizeText: sizeText2, bytesToMb: bytesToMb2, buildTree: buildTree2 } = util;
624
- const EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
625
- function ExcludeCard(props) {
626
- const file = props.file;
627
- const [draft, setDraft] = React.useState(file.content || "");
628
- const [baseline, setBaseline] = React.useState(file.content || "");
629
- const [quick, setQuick] = React.useState("");
630
- const [state, setState] = React.useState({ busy: false, message: "", error: false });
631
- const dirty = draft !== baseline;
632
- function appendPattern(pattern) {
633
- setDraft((d) => (d && !d.endsWith("\n") ? d + "\n" : d) + pattern + "\n");
634
- }
635
- function addQuick() {
636
- const t = quick.trim();
637
- if (!t) return;
638
- appendPattern(t);
639
- setQuick("");
640
- }
641
- function save() {
642
- if (state.busy || !dirty) return;
643
- setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
644
- api("exclude-set", { path: file.path, content: draft }).then((res) => {
645
- if (res && res.ok) {
646
- setBaseline(draft);
647
- setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6B21\u5FEB\u7167 / \u9884\u89C8 / \u56DE\u9000\u65F6\u751F\u6548", error: false });
648
- } else {
649
- setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
650
- }
651
- }).catch((error) => {
652
- setState({ busy: false, message: String(error), error: true });
653
- });
654
- }
655
- function discard() {
656
- if (state.busy) return;
657
- setDraft(baseline);
658
- setState({ busy: false, message: "", error: false });
659
- }
660
- const draftLines = draft.split("\n").map((l) => l.trim());
661
- const suggestions = EXCLUDE_SUGGESTIONS.filter((s) => draftLines.indexOf(s) < 0);
662
- return React.createElement(
663
- "div",
664
- { className: "dsh-recall-ex-card" },
665
- React.createElement("div", { className: "dsh-recall-ex-title" }, "\u5FEB\u7167\u6392\u9664\u9879"),
666
- React.createElement(
667
- "div",
668
- { className: "dsh-recall-ex-note" },
669
- file.home ? "\u6B64\u914D\u7F6E\u5168\u5C40\u5171\u4EAB\uFF0C\u5BF9\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5FEB\u7167\u751F\u6548\uFF08\u5B58\u50A8\u4F4D\u7F6E\uFF1A" + file.path + "\uFF09\u3002" : "home \u76EE\u5F55\u4E0D\u53EF\u5199\u65F6\u6B64\u5DE5\u4F5C\u533A\u964D\u7EA7\u5B58\u50A8\uFF0C\u6392\u9664\u914D\u7F6E\u72EC\u7ACB\u751F\u6548\uFF08\u5B58\u50A8\u4F4D\u7F6E\uFF1A" + file.path + "\uFF09\u3002"
670
- ),
671
- React.createElement("div", { className: "dsh-recall-ex-note" }, "gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF0C\u652F\u6301 # \u6CE8\u91CA\uFF1B\u547D\u4E2D\u6392\u9664\u7684\u6587\u4EF6\u4E0E\u76EE\u5F55\u4E0D\u8FDB\u5165\u5FEB\u7167\uFF0C\u4E5F\u4E0D\u4F1A\u88AB\u56DE\u9000\u89E6\u78B0\u3002"),
672
- React.createElement("textarea", {
673
- className: "dsh-recall-ex-area",
674
- value: draft,
675
- spellCheck: false,
676
- onChange: (e) => setDraft(e.target.value)
677
- }),
678
- React.createElement(
679
- "div",
680
- { className: "dsh-recall-ex-quick" },
681
- React.createElement("input", {
682
- className: "dsh-recall-ex-input",
683
- value: quick,
684
- placeholder: "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
685
- onChange: (e) => setQuick(e.target.value),
686
- onKeyDown: (e) => {
687
- if (e.key === "Enter") {
688
- e.preventDefault();
689
- addQuick();
690
- }
691
- }
692
- }),
693
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: addQuick }, "\u6DFB\u52A0"),
694
- ...suggestions.map((s) => React.createElement("button", {
695
- key: "chip-" + s,
696
- type: "button",
697
- className: "dsh-recall-ex-chip",
698
- title: "\u70B9\u51FB\u8FFD\u52A0 " + s,
699
- onClick: () => appendPattern(s)
700
- }, s))
701
- ),
702
- React.createElement(
703
- "div",
704
- { className: "dsh-recall-panel-actions" },
705
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
706
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
707
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
708
- )
709
- );
710
- }
711
- function ManageCard(props) {
712
- const [items, setItems] = React.useState(null);
713
- const [usage, setUsage] = React.useState(null);
714
- const [errors, setErrors] = React.useState(null);
715
- const [state, setState] = React.useState({ busy: false, message: "", error: false });
716
- const [limit, setLimit] = React.useState(200);
717
- const [total, setTotal] = React.useState(0);
718
- const [health, setHealth] = React.useState(null);
719
- const [query, setQuery] = React.useState("");
720
- const [showAllErrors, setShowAllErrors] = React.useState(false);
721
- const [titlesPending, setTitlesPending] = React.useState(false);
722
- const [lineage, setLineage] = React.useState([]);
723
- function fetchTitles(list) {
724
- const missing = Array.from(new Set(
725
- (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
726
- )).slice(0, 100);
727
- if (!missing.length) {
728
- setTitlesPending(false);
729
- return;
730
- }
731
- setTitlesPending(true);
732
- api("manage", { op: "titles", sessionIds: missing }).then((res) => {
733
- const map = res && res.ok ? res.titles : null;
734
- if (map) {
735
- setItems((prev) => (prev || []).map((it) => it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it));
736
- }
737
- setTitlesPending(false);
738
- }).catch(() => setTitlesPending(false));
739
- }
740
- function fetchMessages(list) {
741
- const requests = (list || []).filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, "messageText")).map((it) => ({ sessionId: it.sessionId, messageId: it.id })).slice(0, 200);
742
- if (!requests.length) return;
743
- api("manage", { op: "messages", requests }).then((res) => {
744
- const map = res && res.ok ? res.messageTexts : null;
745
- if (map) {
746
- setItems((prev) => (prev || []).map((it) => it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it));
747
- }
748
- }).catch(() => {
749
- });
750
- }
751
- function refresh(overLimit) {
752
- const useLimit = overLimit || limit;
753
- api("manage", { op: "list", limit: useLimit }).then((res) => {
754
- if (res && res.ok) {
755
- setItems(res.items || []);
756
- setTotal(typeof res.total === "number" ? res.total : (res.items || []).length);
757
- fetchTitles(res.items || []);
758
- fetchMessages(res.items || []);
759
- if (res.stale) {
760
- api("manage", { op: "list", limit: useLimit }).then((res2) => {
761
- if (res2 && res2.ok && !res2.stale) {
762
- setItems(res2.items || []);
763
- setTotal(typeof res2.total === "number" ? res2.total : (res2.items || []).length);
764
- fetchTitles(res2.items || []);
765
- fetchMessages(res2.items || []);
766
- }
767
- }).catch(() => {
768
- });
769
- }
770
- }
771
- api("manage", { op: "lineage" }).then((res2) => {
772
- if (res2 && res2.ok && Array.isArray(res2.lineage)) setLineage(res2.lineage);
773
- }).catch(() => {
774
- });
775
- api("manage", { op: "usage" }).then((res2) => {
776
- if (res2 && res2.ok) {
777
- setUsage(res2.bytes || 0);
778
- setHealth({ gitAvailable: res2.gitAvailable !== false, homeStores: res2.homeStores || 0, fallbackStores: res2.fallbackStores || 0 });
779
- }
780
- }).catch(() => {
781
- });
782
- api("status", {}).then((res2) => {
783
- if (res2 && res2.ok) setErrors(res2.errors || []);
784
- }).catch(() => {
785
- });
786
- }).catch(() => {
787
- api("manage", { op: "usage" }).then((res) => {
788
- if (res && res.ok) {
789
- setUsage(res.bytes || 0);
790
- setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 });
791
- }
792
- }).catch(() => {
793
- });
794
- api("status", {}).then((res) => {
795
- if (res && res.ok) setErrors(res.errors || []);
796
- }).catch(() => {
797
- });
798
- });
799
- }
800
- React.useEffect(() => {
801
- refresh();
802
- }, []);
803
- function clearErrors() {
804
- setErrors([]);
805
- api("status", { op: "clear" }).catch(() => {
806
- });
807
- }
808
- function run(op, extra, doneText) {
809
- if (state.busy) return;
810
- setState({ busy: true, message: "\u6267\u884C\u4E2D\u2026", error: false });
811
- api("manage", Object.assign({ op }, extra || {})).then((res) => {
812
- if (res && res.ok) {
813
- setState({ busy: false, message: typeof res.deleted === "number" ? "\u5DF2\u5220\u9664 " + res.deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
814
- refresh();
815
- } else {
816
- setState({ busy: false, message: res && (res.message || res.error) || "\u64CD\u4F5C\u5931\u8D25", error: true });
817
- }
818
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
819
- }
820
- const [expanded, setExpanded] = React.useState(() => /* @__PURE__ */ new Set());
821
- const [confirming, setConfirming] = React.useState(null);
822
- function renderDeleteAllConfirm() {
823
- if (!confirming || confirming.kind !== "all") return null;
824
- return React.createElement(
825
- "div",
826
- { className: "dsh-recall-tree-confirm" },
827
- "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
828
- React.createElement("button", {
829
- type: "button",
830
- className: "dsh-recall-btn dsh-recall-btn-danger",
831
- onClick: () => {
832
- setConfirming(null);
833
- run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
834
- }
835
- }, "\u786E\u8BA4\u5168\u90E8\u5220\u9664"),
836
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
837
- );
838
- }
839
- function toggle(key) {
840
- setExpanded((prev) => {
841
- const next = new Set(prev);
842
- if (next.has(key)) next.delete(key);
843
- else next.add(key);
844
- return next;
845
- });
846
- }
847
- const q = query.trim().toLowerCase();
848
- const filteredItems = q ? (items || []).filter(
849
- (it) => (it.workspace || "").toLowerCase().indexOf(q) >= 0 || (it.sessionTitle || "").toLowerCase().indexOf(q) >= 0 || (it.messageText || "").toLowerCase().indexOf(q) >= 0 || String(it.id || "").toLowerCase().indexOf(q) >= 0
850
- ) : items;
851
- const tree = buildTree2(filteredItems);
852
- const allSessionIds = Array.from(new Set((items || []).map((it) => it.sessionId).filter(Boolean)));
853
- const versionMap = groupByLineage(allSessionIds, lineage);
854
- let listById = null;
855
- try {
856
- if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.getSnapshot === "function") {
857
- listById = sessionsSvc.list.getSnapshot().byId || null;
858
- }
859
- } catch (e) {
860
- listById = null;
861
- }
862
- function confirmDelete(kind, key, extra, text) {
863
- setConfirming({ kind, key, extra, text });
864
- }
865
- function renderConfirm(kind, key, extra, text) {
866
- if (!confirming || confirming.kind !== kind || confirming.key !== key) return null;
867
- return React.createElement(
868
- "div",
869
- { className: "dsh-recall-tree-confirm" },
870
- text,
871
- React.createElement("button", {
872
- type: "button",
873
- className: "dsh-recall-ex-chip",
874
- onClick: () => {
875
- const c = confirming;
876
- setConfirming(null);
877
- run("delete", c.extra, "\u5DF2\u5220\u9664");
878
- }
879
- }, "\u786E\u8BA4"),
880
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
881
- );
882
- }
883
- function renderLeaf(it) {
884
- const key = "snap-" + it.id;
885
- const text = it.messageText;
886
- const title = text || it.id;
887
- const label = text ? clockText2(it.time) + " " + text : clockText2(it.time) + " " + it.id.slice(0, 12) + "\u2026";
888
- return React.createElement(
889
- "div",
890
- { className: "dsh-recall-tree-node", key },
891
- React.createElement(
892
- "div",
893
- { className: "dsh-recall-tree-row", title },
894
- React.createElement("span", { className: "dsh-recall-tree-toggle-placeholder" }),
895
- React.createElement(
896
- "span",
897
- { className: "dsh-recall-tree-label" },
898
- React.createElement("span", { className: "dsh-recall-tree-title" }, label)
899
- ),
900
- React.createElement("button", {
901
- type: "button",
902
- className: "dsh-recall-ex-chip",
903
- title: "\u5220\u9664\u8BE5\u5FEB\u7167\uFF08tag \u4E0E\u7D22\u5F15\u6761\u76EE\uFF09",
904
- onClick: () => confirmDelete("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
905
- }, "\u5220\u9664")
906
- ),
907
- renderConfirm("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
908
- );
909
- }
910
- function renderSession(s) {
911
- const key = "session-" + (s.root || "") + "-" + s.sessionId;
912
- const open = expanded.has(key);
913
- const label = s.title || (titlesPending && s.sessionId ? "\u2026" : "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09");
914
- const version = s.sessionId ? versionMap.get(String(s.sessionId)) : null;
915
- const switchable = Boolean(s.sessionId && listById && listById[s.sessionId]);
916
- return React.createElement(
917
- "div",
918
- { className: "dsh-recall-tree-node", key },
919
- React.createElement(
920
- "div",
921
- { className: "dsh-recall-tree-row" },
922
- React.createElement("span", {
923
- className: "dsh-recall-tree-toggle",
924
- onClick: () => toggle(key)
925
- }, open ? "\u25BE" : "\u25B8"),
926
- React.createElement(
927
- "span",
928
- { className: "dsh-recall-tree-label", title: s.sessionId || "" },
929
- React.createElement("span", { className: "dsh-recall-tree-title" }, label),
930
- version ? React.createElement("span", { className: "dsh-recall-tree-meta", title: "\u7248\u672C\u5BB6\u65CF\uFF1A" + version.family.join(" \u2192 ") }, "v" + version.index + "/" + version.family.length) : null,
931
- React.createElement("span", { className: "dsh-recall-tree-meta" }, s.items.length + " \u6761")
932
- ),
933
- switchable ? React.createElement("button", {
934
- type: "button",
935
- className: "dsh-recall-ex-chip",
936
- title: "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
937
- onClick: () => {
938
- try {
939
- sessionsSvc.open(s.sessionId);
940
- } catch (e) {
941
- }
942
- }
943
- }, "\u5207\u6362") : null,
944
- s.sessionId ? React.createElement("button", {
945
- type: "button",
946
- className: "dsh-recall-ex-chip",
947
- title: "\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167",
948
- onClick: () => confirmDelete("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
949
- }, "\u5220\u9664") : null
950
- ),
951
- open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...s.items.map(renderLeaf)) : null,
952
- s.sessionId ? renderConfirm("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
953
- );
954
- }
955
- function renderWorkspace(ws) {
956
- const key = "ws-" + ws.root;
957
- const open = expanded.has(key);
958
- const sessionCount = ws.sessions.length;
959
- const snapCount = ws.sessions.reduce((n, s) => n + s.items.length, 0);
960
- return React.createElement(
961
- "div",
962
- { className: "dsh-recall-tree-node", key },
963
- React.createElement(
964
- "div",
965
- { className: "dsh-recall-tree-row" },
966
- React.createElement("span", {
967
- className: "dsh-recall-tree-toggle",
968
- onClick: () => toggle(key)
969
- }, open ? "\u25BE" : "\u25B8"),
970
- React.createElement(
971
- "span",
972
- { className: "dsh-recall-tree-label", title: ws.root || "" },
973
- React.createElement("span", { className: "dsh-recall-tree-name" }, ws.name),
974
- React.createElement("span", { className: "dsh-recall-tree-meta" }, sessionCount + " \u4F1A\u8BDD / " + snapCount + " \u5FEB\u7167")
975
- ),
976
- ws.root ? React.createElement("button", {
977
- type: "button",
978
- className: "dsh-recall-ex-chip",
979
- title: "\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167",
980
- onClick: () => confirmDelete("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
981
- }, "\u5220\u9664") : null
982
- ),
983
- open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...ws.sessions.map(renderSession)) : null,
984
- ws.root ? renderConfirm("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
985
- );
986
- }
987
- const treeNodes = tree.map(renderWorkspace);
988
- const loaded = items ? items.length : null;
989
- const countText = loaded === null ? "\u5171 \u2026 \u6761\u5FEB\u7167" : "\u5171 " + total + " \u6761\u5FEB\u7167" + (limit < total ? "\uFF08\u5F53\u524D\u663E\u793A\u6700\u65B0 " + loaded + " \u6761\uFF09" : "");
990
- function loadMore() {
991
- const next = Math.min(Math.max(total, limit), 2e3);
992
- if (next <= limit) return;
993
- setLimit(next);
994
- refresh(next);
995
- }
996
- return React.createElement(
997
- "div",
998
- { className: "dsh-recall-ex-card" },
999
- React.createElement("div", { className: "dsh-recall-ex-title" }, "\u5FEB\u7167\u7BA1\u7406"),
1000
- React.createElement(
1001
- "div",
1002
- { className: "dsh-recall-ex-note" },
1003
- usage === null ? countText + "\u3002" : countText + "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 " + sizeText2(usage) + "\u3002"
1004
- ),
1005
- health ? React.createElement(
1006
- "div",
1007
- { className: "dsh-recall-ex-note", key: "health" },
1008
- React.createElement("span", {
1009
- className: health.gitAvailable ? "" : "dsh-recall-ex-status-error"
1010
- }, health.gitAvailable ? "git \u53EF\u7528" : "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09"),
1011
- " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome " + health.homeStores + " \u4E2A\u5DE5\u4F5C\u533A" + (health.fallbackStores ? "\uFF0C\u964D\u7EA7 " + health.fallbackStores + " \u4E2A" : "")
1012
- ) : null,
1013
- React.createElement("input", {
1014
- className: "dsh-recall-ex-input",
1015
- placeholder: "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
1016
- value: query,
1017
- spellCheck: false,
1018
- onChange: (e) => setQuery(e.target.value)
1019
- }),
1020
- treeNodes.length > 0 ? React.createElement("div", { className: "dsh-recall-tree" }, ...treeNodes) : null,
1021
- items && items.length === 0 && !q ? React.createElement("div", { className: "dsh-recall-ex-note", key: "empty" }, "\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u5FEB\u7167\u3002") : null,
1022
- q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-ex-note", key: "no-match" }, "\u65E0\u5339\u914D\u5FEB\u7167") : null,
1023
- renderDeleteAllConfirm(),
1024
- React.createElement(
1025
- "div",
1026
- { className: "dsh-recall-panel-actions" },
1027
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1028
- limit < total ? React.createElement("button", {
1029
- type: "button",
1030
- className: "dsh-recall-btn",
1031
- disabled: state.busy,
1032
- onClick: loadMore
1033
- }, "\u52A0\u8F7D\u66F4\u591A") : null,
1034
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: refresh }, "\u5237\u65B0"),
1035
- React.createElement("button", {
1036
- type: "button",
1037
- className: "dsh-recall-btn dsh-recall-btn-danger",
1038
- disabled: state.busy,
1039
- title: "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
1040
- onClick: () => setConfirming({ kind: "all" })
1041
- }, "\u5168\u90E8\u5220\u9664"),
1042
- React.createElement("button", {
1043
- type: "button",
1044
- className: "dsh-recall-btn",
1045
- disabled: state.busy,
1046
- title: "\u7ACB\u5373\u5BF9\u5168\u90E8\u5DE5\u4F5C\u533A\u6267\u884C\u4E00\u6B21 git gc\uFF08\u538B\u7F29\u5BF9\u8C61\u5E93\u91CA\u653E\u7A7A\u95F4\uFF09",
1047
- onClick: () => run("gc", {}, "gc \u5B8C\u6210")
1048
- }, "\u7ACB\u5373 gc")
1049
- ),
1050
- errors && errors.length > 0 ? React.createElement(
1051
- "div",
1052
- { className: "dsh-recall-ex-note", key: "errors" },
1053
- React.createElement(
1054
- "div",
1055
- { className: "dsh-recall-ex-status" },
1056
- "\u6700\u8FD1\u9519\u8BEF\uFF1A",
1057
- (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message))
1058
- ),
1059
- React.createElement(
1060
- "div",
1061
- { className: "dsh-recall-panel-actions" },
1062
- errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u90E8 (" + errors.length + ")") : null,
1063
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
1064
- )
1065
- ) : null
1066
- );
1067
- }
1068
- function ExcludeFilesSection() {
1069
- const [files, setFiles] = React.useState(null);
1070
- const [error, setError] = React.useState("");
1071
- function load() {
1072
- api("exclude-get", {}).then((res) => {
1073
- if (res && res.ok) {
1074
- setFiles(res.files || []);
1075
- setError("");
1076
- return;
1077
- }
1078
- if (res && res.unsupported) {
1079
- setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1080
- return;
1081
- }
1082
- setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1083
- }).catch((e) => setError(String(e)));
1084
- }
1085
- React.useEffect(() => {
1086
- load();
1087
- }, []);
1088
- if (error) {
1089
- return React.createElement(
1090
- "div",
1091
- { className: "dsh-recall-ex-card" },
1092
- React.createElement("div", { className: "dsh-recall-ex-note" }, error),
1093
- React.createElement(
1094
- "div",
1095
- { className: "dsh-recall-panel-actions" },
1096
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
1097
- )
1098
- );
1099
- }
1100
- if (files === null) {
1101
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
1102
- }
1103
- if (!files.length) {
1104
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002");
1105
- }
1106
- return React.createElement(
1107
- "div",
1108
- { className: "dsh-recall-ex-card" },
1109
- ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1110
- );
1111
- }
1112
- function ConfigForm() {
1113
- const [baseline, setBaseline] = React.useState(null);
1114
- const [draft, setDraft] = React.useState(null);
1115
- const [envLocks, setEnvLocks] = React.useState({});
1116
- const [overridden, setOverridden] = React.useState({});
1117
- const [writable, setWritable] = React.useState(true);
1118
- const [state, setState] = React.useState({ busy: false, message: "", error: false });
1119
- const [showAdvanced, setShowAdvanced] = React.useState(false);
1120
- function load() {
1121
- api("config-get", {}).then((res) => {
1122
- if (res && res.ok) {
1123
- const v = res.values || {};
1124
- const next = {
1125
- gcSnaps: String(v.gcSnaps == null ? "" : v.gcSnaps),
1126
- gcHours: String(v.gcHours == null ? "" : v.gcHours),
1127
- maxFileBytes: bytesToMb2(v.maxFileBytes),
1128
- maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? "" : v.maxSnapshotsPerWorkspace),
1129
- baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join("\n") : "",
1130
- refillDraft: v.refillDraft !== false,
1131
- snapshotEnabled: v.snapshotEnabled !== false,
1132
- archiveOriginal: v.archiveOriginal !== false,
1133
- retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
1134
- };
1135
- setDraft(next);
1136
- setBaseline(next);
1137
- setEnvLocks(res.envLocks || {});
1138
- setOverridden(res.overridden || {});
1139
- setWritable(res.writable !== false);
1140
- } else {
1141
- setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
1142
- }
1143
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1144
- }
1145
- React.useEffect(() => {
1146
- load();
1147
- }, []);
1148
- function edit(key, value) {
1149
- setDraft((d) => Object.assign({}, d, { [key]: value }));
1150
- }
1151
- function save() {
1152
- if (state.busy || !draft || !baseline) return;
1153
- const patch = {};
1154
- for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
1155
- if (draft[key] !== baseline[key]) patch[key] = draft[key];
1156
- }
1157
- if (!Object.keys(patch).length) {
1158
- setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
1159
- return;
1160
- }
1161
- const clean = {};
1162
- if (patch.gcSnaps !== void 0) {
1163
- const n = parseInt(patch.gcSnaps, 10);
1164
- if (!Number.isFinite(n) || n < 1) {
1165
- setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1166
- return;
1167
- }
1168
- clean.gcSnaps = n;
1169
- }
1170
- if (patch.gcHours !== void 0) {
1171
- const n = parseInt(patch.gcHours, 10);
1172
- if (!Number.isFinite(n) || n < 1) {
1173
- setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1174
- return;
1175
- }
1176
- clean.gcHours = n;
1177
- }
1178
- if (patch.maxFileBytes !== void 0) {
1179
- const mb = Number(patch.maxFileBytes);
1180
- if (!Number.isFinite(mb) || mb < 0.01) {
1181
- setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
1182
- return;
1183
- }
1184
- clean.maxFileBytes = Math.round(mb * 1048576);
1185
- }
1186
- if (patch.maxSnapshotsPerWorkspace !== void 0) {
1187
- const n = parseInt(patch.maxSnapshotsPerWorkspace, 10);
1188
- if (!Number.isFinite(n) || n < 0) {
1189
- setState({ busy: false, message: "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09", error: true });
1190
- return;
1191
- }
1192
- clean.maxSnapshotsPerWorkspace = n;
1193
- }
1194
- if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
1195
- if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
1196
- if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
1197
- if (patch.retentionDays !== void 0) {
1198
- const n = parseInt(patch.retentionDays, 10);
1199
- if (!Number.isFinite(n) || n < 0) {
1200
- setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
1201
- return;
1202
- }
1203
- clean.retentionDays = n;
1204
- }
1205
- if (patch.baseExcludes !== void 0) {
1206
- clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
1207
- }
1208
- setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1209
- api("config-set", { patch: clean }).then((res) => {
1210
- if (res && res.ok) {
1211
- setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
1212
- load();
1213
- } else {
1214
- setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1215
- }
1216
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1217
- }
1218
- function numRow(key, label, hint, opts) {
1219
- const locked = Boolean(envLocks && envLocks[key]);
1220
- const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
1221
- return React.createElement(
1222
- "div",
1223
- { className: "dsh-recall-cfg-row", key },
1224
- React.createElement(
1225
- "div",
1226
- { className: "dsh-recall-cfg-line" },
1227
- React.createElement("label", { className: "dsh-recall-cfg-label" }, label),
1228
- React.createElement("input", {
1229
- className: "dsh-recall-cfg-input",
1230
- type: "number",
1231
- value: draft ? draft[key] : "",
1232
- disabled: locked || !writable,
1233
- min: opts && opts.min,
1234
- step: opts && opts.step,
1235
- onChange: (e) => edit(key, e.target.value)
1236
- }),
1237
- opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
1238
- changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1239
- overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
1240
- locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
1241
- ),
1242
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
1243
- );
1244
- }
1245
- function resetDefaults() {
1246
- if (state.busy || !writable) return;
1247
- setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
1248
- api("config-reset", {}).then((res) => {
1249
- if (res && res.ok) {
1250
- load();
1251
- setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
1252
- } else {
1253
- setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
1254
- }
1255
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1256
- }
1257
- if (!draft) {
1258
- return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
1259
- }
1260
- return React.createElement(
1261
- "div",
1262
- { className: "dsh-recall-ex-card" },
1263
- React.createElement(
1264
- "div",
1265
- { className: "dsh-recall-cfg-row", key: "snapshotEnabled" },
1266
- React.createElement(
1267
- "div",
1268
- { className: "dsh-recall-cfg-line" },
1269
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-snapshot" }, "\u542F\u7528\u5FEB\u7167"),
1270
- React.createElement("input", {
1271
- id: "dsh-recall-cfg-snapshot",
1272
- type: "checkbox",
1273
- checked: Boolean(draft.snapshotEnabled),
1274
- disabled: !writable,
1275
- onChange: (e) => edit("snapshotEnabled", e.target.checked)
1276
- }),
1277
- draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1278
- overridden && overridden.snapshotEnabled !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1279
- ),
1280
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF08\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09\uFF0C\u9002\u5408\u4E34\u65F6\u7981\u7528\u5FEB\u7167\u7684\u573A\u5408")
1281
- ),
1282
- numRow("gcSnaps", "gc \u89E6\u53D1\u6761\u6570", "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 git gc", { min: 1, step: 1 }),
1283
- numRow("gcHours", "gc \u89E6\u53D1\u5C0F\u65F6", "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF08\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1\uFF09", { min: 1, step: 1 }),
1284
- numRow("maxFileBytes", "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650", "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\uFF08\u5355\u4F4D MB\uFF0C\u652F\u6301\u5C0F\u6570\uFF09", { suffix: "MB", min: 0.01, step: 0.5 }),
1285
- numRow("maxSnapshotsPerWorkspace", "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650", "\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u9650\u5236", { min: 0, step: 1 }),
1286
- numRow("retentionDays", "\u5FEB\u7167\u4FDD\u7559\u5929\u6570", "\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u542F\u7528\uFF08\u4E0E\u5FEB\u7167\u603B\u6570\u4E0A\u9650\u5404\u81EA\u751F\u6548\uFF09", { min: 0, step: 1 }),
1287
- React.createElement(
1288
- "div",
1289
- { className: "dsh-recall-cfg-row", key: "refillDraft" },
1290
- React.createElement(
1291
- "div",
1292
- { className: "dsh-recall-cfg-line" },
1293
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-refill" }, "\u64A4\u56DE\u540E\u56DE\u586B\u8F93\u5165\u6846"),
1294
- React.createElement("input", {
1295
- id: "dsh-recall-cfg-refill",
1296
- type: "checkbox",
1297
- checked: Boolean(draft.refillDraft),
1298
- disabled: !writable,
1299
- onChange: (e) => edit("refillDraft", e.target.checked)
1300
- }),
1301
- draft.refillDraft !== baseline.refillDraft ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1302
- overridden && overridden.refillDraft !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1303
- ),
1304
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u6210\u529F\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846\uFF0C\u65B9\u4FBF\u4FEE\u6539\u540E\u91CD\u65B0\u53D1\u9001")
1305
- ),
1306
- React.createElement(
1307
- "div",
1308
- { className: "dsh-recall-cfg-row", key: "archiveOriginal" },
1309
- React.createElement(
1310
- "div",
1311
- { className: "dsh-recall-cfg-line" },
1312
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-archive" }, "\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD"),
1313
- React.createElement("input", {
1314
- id: "dsh-recall-cfg-archive",
1315
- type: "checkbox",
1316
- checked: Boolean(draft.archiveOriginal),
1317
- disabled: !writable,
1318
- onChange: (e) => edit("archiveOriginal", e.target.checked)
1319
- }),
1320
- draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1321
- overridden && overridden.archiveOriginal !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1322
- ),
1323
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u540E\u539F\u4F1A\u8BDD\u4ECE\u5217\u8868\u5F52\u6863\u9690\u85CF\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\uFF1B\u5173\u95ED\u5219\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF0C\u65B9\u4FBF\u5BF9\u7167\u56DE\u9000\u524D\u540E\u7684\u4E0A\u4E0B\u6587")
1324
- ),
1325
- React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
1326
- showAdvanced ? React.createElement(
1327
- "div",
1328
- { className: "dsh-recall-cfg-row", key: "baseExcludes" },
1329
- React.createElement(
1330
- "div",
1331
- { className: "dsh-recall-cfg-line" },
1332
- React.createElement("label", { className: "dsh-recall-cfg-label" }, "\u57FA\u7840\u6392\u9664\u8868"),
1333
- draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1334
- overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1335
- ),
1336
- React.createElement("textarea", {
1337
- className: "dsh-recall-cfg-area",
1338
- rows: 4,
1339
- value: draft.baseExcludes,
1340
- disabled: !writable,
1341
- onChange: (e) => edit("baseExcludes", e.target.value)
1342
- }),
1343
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5185\u7F6E\u89C4\u5219\uFF0C\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u5171\u4EAB\uFF0C\u5EFA\u8BAE\u4FDD\u6301\u9ED8\u8BA4\uFF1Bgitignore \u8BED\u6CD5\u6BCF\u884C\u4E00\u6761\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u91CC\u7684 exclude.txt\uFF08S3-2 \u6298\u53E0\uFF09")
1344
- ) : null,
1345
- React.createElement(
1346
- "div",
1347
- { className: "dsh-recall-panel-actions" },
1348
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1349
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, "\u653E\u5F03\u4FEE\u6539"),
1350
- React.createElement("button", {
1351
- type: "button",
1352
- className: "dsh-recall-btn",
1353
- disabled: state.busy || !writable,
1354
- title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
1355
- onClick: resetDefaults
1356
- }, "\u6062\u590D\u9ED8\u8BA4"),
1357
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
1358
- !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
1359
- )
1360
- );
1361
- }
1362
- function SectionToggle(props) {
1363
- return React.createElement(
1364
- "button",
1365
- {
1366
- type: "button",
1367
- className: "dsh-recall-cardbtn",
1368
- "aria-expanded": props.open,
1369
- onClick: props.onToggle
1370
- },
1371
- React.createElement("span", { className: "dsh-recall-tree-toggle" }, props.open ? "\u25BE" : "\u25B8"),
1372
- React.createElement("span", { style: { fontWeight: 600, fontSize: "14px", lineHeight: "22px" } }, props.title),
1373
- props.meta ? React.createElement("span", { className: "dsh-recall-tree-meta" }, props.meta) : null
1374
- );
1375
- }
1376
- function RecallSettingsCard() {
1377
- const [open, setOpen] = React.useState(false);
1378
- const [sections, setSections] = React.useState({ exclude: false, manage: false });
1379
- function toggle(key) {
1380
- setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
1381
- }
1382
- return React.createElement(
1383
- "li",
1384
- { className: "dsh-recall-card" + (open ? " dsh-recall-card-open" : "") },
1385
- React.createElement(
1386
- "button",
1387
- {
1388
- type: "button",
1389
- className: "dsh-recall-cardbtn",
1390
- "aria-expanded": open,
1391
- "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + ": \u64A4\u56DE\u63D2\u4EF6",
1392
- onClick: () => setOpen((v) => !v)
1393
- },
1394
- React.createElement(
1395
- "span",
1396
- { className: "dsh-recall-card-head" },
1397
- React.createElement("span", { className: "dsh-recall-card-name" }, "\u64A4\u56DE\u63D2\u4EF6"),
1398
- React.createElement("span", { className: "dsh-recall-card-desc" }, "\u6D88\u606F\u64A4\u56DE\uFF08\u6587\u4EF6\u5FEB\u7167 + \u5BF9\u8BDD\u56DE\u9000\uFF09\u7684\u9608\u503C\u4E0E\u6CBB\u7406")
1399
- ),
1400
- React.createElement("svg", {
1401
- width: 14,
1402
- height: 14,
1403
- viewBox: "0 0 16 16",
1404
- style: { color: "var(--dsw-alias-label-tertiary)", flex: "none", transition: "transform .16s", transform: open ? "rotate(180deg)" : "none" }
1405
- }, React.createElement("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }))
1406
- ),
1407
- open ? React.createElement(
1408
- "div",
1409
- { className: "dsh-recall-card-body" },
1410
- React.createElement(ConfigForm),
1411
- React.createElement(SectionToggle, { title: "\u6392\u9664\u914D\u7F6E\uFF08exclude.txt\uFF09", open: sections.exclude, onToggle: () => toggle("exclude") }),
1412
- sections.exclude ? React.createElement(ExcludeFilesSection) : null,
1413
- React.createElement(SectionToggle, { title: "\u5FEB\u7167\u7BA1\u7406", open: sections.manage, onToggle: () => toggle("manage") }),
1414
- sections.manage ? React.createElement(ManageCard) : null
1415
- ) : null
1416
- );
1417
- }
1418
- return { RecallSettingsCard };
1419
- }
1420
-
1421
- // src/client/app.js
1422
- function nextShadowPriority(entries, key) {
1423
- let priority = -1;
1424
- for (const entry of Array.isArray(entries) ? entries : []) {
1425
- if (!entry || !entry.options || entry.options.key !== key) continue;
1426
- const occupied = Number.isFinite(entry.options.priority) ? entry.options.priority : 0;
1427
- if (occupied <= priority) priority = occupied - 1;
1428
- }
1429
- return priority;
1430
- }
1431
- function createApp(React) {
1432
- return function apply(ctx) {
1433
- const slots = ctx.slots;
1434
- if (!slots) return;
1435
- const sessionsSvc = ctx.sessions;
1436
- const workspacesSvc = ctx.workspaces;
1437
- const stylesSvc = ctx.get("styles");
1438
- if (stylesSvc && typeof stylesSvc.insert === "function") {
1439
- stylesSvc.insert(CSS);
1440
- } else if (typeof document !== "undefined") {
1441
- const tag = document.createElement("style");
1442
- tag.setAttribute("data-plugin", "dsh-recall-plugin");
1443
- tag.textContent = CSS;
1444
- document.head.appendChild(tag);
1445
- }
1446
- const util = buildUtil();
1447
- const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc);
1448
- const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc);
1449
- for (const slotKey of ["user", "steering"]) {
1450
- try {
1451
- slots.inject("conversation.chat.node", () => {
1452
- const priority = nextShadowPriority(slots.entries("conversation.chat.node"), slotKey);
1453
- return slots.register(
1454
- { name: "conversation.chat.node", key: slotKey, priority },
1455
- UserRecallNode
1456
- );
1457
- });
1458
- } catch (error) {
1459
- console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
1460
- }
1461
- }
1462
- try {
1463
- slots.inject("settings.plugin.item", () => slots.register(
1464
- { name: "settings.plugin.item", key: "dsh-recall" },
1465
- RecallSettingsCard
1466
- ));
1467
- } catch (error) {
1468
- console.error("[dsh-recall-plugin] settings card register failed:", error);
1469
- }
1470
- };
1471
- }
1472
-
1473
- // src/client/entry.js
1474
- window.__ModuleLoader__.load({
1475
- id: "dsh-recall-plugin",
1476
- factory: (require2) => {
1477
- const React = require2("react");
1478
- return {
1479
- name: "dsh-recall-plugin",
1480
- inject: ["slots", "sessions", "workspaces", "timer"],
1481
- apply: createApp(React)
1482
- };
1483
- }
1484
- });
1
+ "use strict";
2
+
3
+ // src/client/css.ts
4
+ var CSS = [
5
+ // 语义变量集中声明(V1-5):btn-danger 的 hover 亮度是令牌体系外补丁,
6
+ // 集中成单一来源避免散落硬编码;前景色用官方实证配对 bg-layer-3(error
7
+ // primary 底色上文本随主题翻转,见 plan-settings-ui V1 核验记录)。
8
+ ":root{--dsh-recall-btn-danger-hover:brightness(1.08);--dsh-recall-tree-indent:24px;--dsh-recall-space-1:4px;--dsh-recall-space-2:8px;--dsh-recall-space-3:12px}",
9
+ ".dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}",
10
+ ".dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}",
11
+ ".dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:1.5;white-space:pre-wrap;word-break:break-word}",
12
+ ".dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}",
13
+ ".dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}",
14
+ ".dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:1.5}",
15
+ ".dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}",
16
+ ".dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
17
+ "@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}",
18
+ ".dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}",
19
+ ".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.5}",
20
+ ".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;word-break:break-word}",
21
+ ".dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}",
22
+ ".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.5}",
23
+ ".dsh-recall-badge{flex:none;font-size:12px;line-height:1.5;padding:0 6px;border-radius:6px}",
24
+ ".dsh-recall-badge-modified{color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}",
25
+ ".dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
26
+ ".dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}",
27
+ ".dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}",
28
+ // grid-column 对非 grid 祖先(exclude/快照卡片的 flex 布局)自动无效,无害;
29
+ // 在 cfg-grid 内则保证操作区/占满行不被 auto-placement 塞进第一列撑爆列宽。
30
+ ".dsh-recall-panel-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;margin-top:2px;grid-column:1/-1}",
31
+ // 按钮体系对齐官方设置卡(dsh-client-ui-settings-plugins PluginCard 底部按钮
32
+ // 组实测产物):次级 = 描边幽灵(discard 逐字配方——l2 描边 + 透明底,hover
33
+ // 升 label-dimmed 描边 + primary 字色);官方 discard/save 同高靠 save 也带
34
+ // 1px 描边,故变体统一 border-color:transparent 保同盒模型。hover 用
35
+ // :not(:disabled) 收口后,disabled hover 抵消规则不再需要;disabled 透明度
36
+ // 随官方 .4。
37
+ ".dsh-recall-btn{border:1px solid var(--dsw-alias-border-l2);background:0 0;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;cursor:pointer;color:var(--dsw-alias-label-secondary)}",
38
+ ".dsh-recall-btn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}",
39
+ ".dsh-recall-btn:disabled,.dsh-recall-ex-chip:disabled{opacity:.4;cursor:default}",
40
+ // 危险/主按钮的 hover 须显式重申文字与描边——基础 ghost hover 带伪类、优先级
41
+ // 更高,会把它们的字色刷回 label-primary 并描出亮边
42
+ ".dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}",
43
+ ".dsh-recall-btn-danger:hover:not(:disabled){color:var(--dsw-alias-bg-layer-3);border-color:transparent;filter:var(--dsh-recall-btn-danger-hover)}",
44
+ // 主按钮(保存)改用官方插件设置卡 save 的逐字配方:label-primary 反色实心 +
45
+ // bg-layer-3 前景(同列官方插件卡的保存即此形态,比 button-primary-fill 更贴
46
+ // 设置页语境);官方 save 无 hover 态,这里的 hover 规则只做「不被基础 ghost
47
+ // hover 污染」的抵消,视觉变化为零即是官方行为。只挂「保存」,主次分明。
48
+ ".dsh-recall-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}",
49
+ ".dsh-recall-btn-primary:hover:not(:disabled){color:var(--dsw-alias-bg-layer-3);border-color:transparent}",
50
+ // 焦点可见性对齐官方设置页实测写法(同上产物):按钮类 = brand-primary 2px
51
+ // outline(卡片头 offset -2px 内收,其余 +1px);输入类 = brand-primary 描边
52
+ // 变色、不套 ring。替换 V2-5 border-l3 环方案(当时核验的是旧版写法)。
53
+ ".dsh-recall-btn:focus-visible,.dsh-recall-ex-chip:focus-visible,.dsh-recall-tree-toggle:focus-visible,.dsh-recall-cfg-switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}",
54
+ ".dsh-recall-cardbtn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}",
55
+ ".dsh-recall-ex-input:focus-visible,.dsh-recall-cfg-input:focus-visible,.dsh-recall-ex-area:focus-visible,.dsh-recall-cfg-area:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}",
56
+ ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:1.5;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
57
+ ".dsh-recall-toast.dsh-recall-toast-in{opacity:1}",
58
+ ".dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
59
+ ".dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}",
60
+ ".dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.5}",
61
+ ".dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;word-break:break-word}",
62
+ // 存储路径独立行:等宽字体 + break-all,Windows 长路径整齐折行;12px tertiary
63
+ // 降为辅助信息层级(与说明正文同色系但更轻),margin-top 收紧与上一行说明的
64
+ // 归属关系(ex-card 统一 gap 8px 对「说明→其路径」偏松)。
65
+ ".dsh-recall-ex-path{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);word-break:break-all;margin-top:-4px}",
66
+ // 输入类控件统一官方 field input 配方(At1oFq_input 逐字):.5px l4 发丝描边 +
67
+ // layer-3 底 + 8px 圆角 + 0/12px 内边距(单行框 34px 高);卡片打开态底色是
68
+ // layer-2,layer-3 输入框在其上恰好与官方插件配置卡同层叠关系
69
+ ".dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.5;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}",
70
+ ".dsh-recall-ex-quick{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
71
+ ".dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;height:34px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}",
72
+ // chip 改官方 badge 丸形配方(bg-module-platform + 999px)——树内小操作与
73
+ // 排除建议属「标签级动作」,与状态徽章同形态更一致;hover 用 :not(:disabled)
74
+ // 收口(与按钮同法)
75
+ ".dsh-recall-ex-chip{border:none;border-radius:999px;padding:1px 10px;font-size:12px;line-height:1.5;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}",
76
+ ".dsh-recall-ex-chip:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
77
+ // V9 确认按钮 danger chip:红色文字 + 官方失败状态行底,与 .btn-danger 同义
78
+ // 但保持 chip 尺寸层级(确认条内部不出现大按钮)
79
+ ".dsh-recall-ex-chip-danger{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger)}",
80
+ // 基础 chip hover 带伪类、优先级更高,会把危险 chip 的红字刷回普通色——
81
+ // 显式重申保持 hover 下仍是红字(危险语义在悬停确认时最不能丢)
82
+ ".dsh-recall-ex-chip-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary)}",
83
+ ".dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}",
84
+ ".dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}",
85
+ ".dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}",
86
+ // V6 健康徽章(git 可用性):pill 配色直接复用官方状态行配对——成功用
87
+ // success-tertiary 底、失败用 interactive-bg-hover-danger 底(error
88
+ // tertiary 令牌,官方失败状态行即用此搭配,主题感知)。
89
+ ".dsh-recall-health-pill{display:inline-flex;align-items:center;padding:1px 10px;border-radius:999px;font-size:12px;line-height:1.5}",
90
+ ".dsh-recall-health-pill-ok{background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary)}",
91
+ ".dsh-recall-health-pill-bad{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}",
92
+ // V6 错误区标题:error 色 + 条数,错误不再是灰色小字(fail-loud 可见性)
93
+ ".dsh-recall-errors-title{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:1.5;font-weight:600}",
94
+ ".dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
95
+ ".dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}",
96
+ ".dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}",
97
+ ".dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
98
+ // V2 树折叠钮 span→button UA 默认样式重置:button 自带 appearance/背景/边框/
99
+ // 内边距,与 span 形态差异在此抹平,保证纯键盘可达不引入视觉回归。
100
+ ".dsh-recall-tree-toggle{appearance:none;background:0 0;border:0;padding:0;font:inherit;flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:12px;line-height:1.5;user-select:none}",
101
+ ".dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
102
+ ".dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}",
103
+ ".dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.5;overflow:hidden}",
104
+ ".dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}",
105
+ ".dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
106
+ ".dsh-recall-tree-meta{flex:none;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
107
+ // V4 树缩进契约化:总缩进 = --dsh-recall-tree-indent(折叠钮 18px + gap 6px),
108
+ // children 2/3(16px margin)与 1/3(8px padding)拆分,恰好让子行文字
109
+ // 与父行折叠钮右缘对齐——把「16+8 恰等于 18+6」的巧合变成单一事实源。
110
+ ".dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:calc(var(--dsh-recall-tree-indent)*2/3);border-left:1px solid var(--dsw-alias-border-l1);padding-left:calc(var(--dsh-recall-tree-indent)/3);animation:dsh-recall-unfold .16s ease-out}",
111
+ ".dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);animation:dsh-recall-unfold .16s ease-out}",
112
+ // V9 展开动效:opacity + 2px 上移入场(确认条/树展开共用一个 keyframes)。
113
+ // 弃用 max-height 过渡:树展开高度可变(长列表数百 px),固定上限会裁切;
114
+ // opacity+位移给出同等显隐反馈且零裁切风险。
115
+ "@keyframes dsh-recall-unfold{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}",
116
+ "@media (prefers-reduced-motion:reduce){.dsh-recall-tree-children,.dsh-recall-tree-confirm{animation:none}}",
117
+ // 卡片对齐同列官方插件设置卡(YyYd_a_card 逐字配方):.5px l4 发丝描边 +
118
+ // 16px 圆角 + hover 升 label-dimmed 描边;打开态(layer-2 底 + dimmed 描边)
119
+ // 本就与官方一致,保持不变
120
+ ".dsh-recall-card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s;display:flex;flex-direction:column;text-align:left}",
121
+ ".dsh-recall-card:hover{border-color:var(--dsw-alias-label-dimmed)}",
122
+ ".dsh-recall-card.dsh-recall-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
123
+ ".dsh-recall-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
124
+ ".dsh-recall-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
125
+ ".dsh-recall-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
126
+ ".dsh-recall-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
127
+ ".dsh-recall-cardbtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
128
+ // 折叠头(SectionToggle)复用卡片头 cardbtn:14/16px 内边距在卡片 body 内表现为
129
+ // 16px 左缩进,与分组标题/表单内容错位(用户实测反馈)——body 内覆写为贴左,
130
+ // 卡片头(.dsh-recall-card 直接子级)不受影响。
131
+ ".dsh-recall-card-body .dsh-recall-cardbtn{padding:6px 0}",
132
+ // 折叠头箭头贴左:tree-toggle 18px 定宽居中盒,▸/▾ 字形居中使其墨迹比
133
+ // 上方表单标签右偏 ~5px(用户实测折叠头与内容左缘不齐);盒内改左对齐后,
134
+ // 三个折叠头(高级/排除配置/快照管理)箭头与标签共享同一左缘。
135
+ ".dsh-recall-card-body .dsh-recall-cardbtn .dsh-recall-tree-toggle{justify-content:flex-start}",
136
+ // 分区折叠头箭头旋转化:与官方卡片 chevron 同一动效语言(transition .16s、
137
+ // 开合=旋转),替代 ▸/▾ 字符瞬切;只挂 SectionToggle,树内折叠钮维持字符
138
+ // 切换不受影响
139
+ ".dsh-recall-section-chevron{transition:transform .16s}",
140
+ ".dsh-recall-section-chevron-open{transform:rotate(90deg)}",
141
+ // cfg-grid:ConfigForm 全部行共享的单一 grid 容器。此前每行 cfg-row 是独立
142
+ // grid,「第一列 max-content」各行各算,跨行对齐从未成立(checkbox/输入框
143
+ // 列参差);cfg-row 改 display:contents 透明化后,label/控件行/hint 直接成为
144
+ // 本容器的 item,第一列列宽由全表单最长 label 决定——跨行对齐自此成立。
145
+ ".dsh-recall-cfg-grid{display:grid;grid-template-columns:max-content minmax(0,1fr);column-gap:8px;row-gap:4px;align-items:start}",
146
+ ".dsh-recall-cfg-row{display:contents}",
147
+ ".dsh-recall-cfg-grid > .dsh-recall-cardbtn{grid-column:1/-1}",
148
+ // V5 表单分组小标题:语义分组分隔符。组间用整行分隔线 + 加倍留白划界
149
+ // (「快照行为」「自动治理」是两个配置维度,仅小标题用户实测分不清组界);
150
+ // 首组(紧跟卡片头)不需要分隔线,三件套清零。
151
+ // 组间分隔线随官方发丝线规格(.5px l2,与官方卡体/字段分隔同粗细)
152
+ ".dsh-recall-cfg-group{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;font-weight:600;grid-column:1/-1;margin-top:10px;padding-top:10px;border-top:.5px solid var(--dsw-alias-border-l2)}",
153
+ ".dsh-recall-cfg-group:first-child{margin-top:0;padding-top:0;border-top:none}",
154
+ // V4 后 cfg-line 只装控件(label 已是 cfg-row 直接子元素,占 grid 第一列 max-content)
155
+ ".dsh-recall-cfg-line{display:flex;align-items:center;gap:8px;min-width:0;grid-column:2}",
156
+ // 主标签用 label-primary + 官方字段标签字重 500(At1oFq_label),与 12px
157
+ // tertiary 的说明文字拉开层级;说明固定在控件下方第二行,不混排进主标签行。
158
+ ".dsh-recall-cfg-label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;font-weight:500}",
159
+ // 数字输入框 = 官方 field input 配方(.5px l4 发丝描边 + layer-3 底 + 34px
160
+ // + 0/12px 内边距)叠加本插件既有约定:定宽 120px + 右对齐(不定宽时框宽
161
+ // 随行内 tag 有无伸缩,实测参差);tabular-nums 让同列数字等宽、纵向扫描
162
+ // 小数位整齐。120px 足够容纳「0.01」~「1000000」区间。
163
+ ".dsh-recall-cfg-input{flex:none;width:120px;box-sizing:border-box;height:34px;font:inherit;font-size:13px;text-align:right;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:0 12px}",
164
+ // disabled 随官方 input:文字降 tertiary,不动透明度(整框变淡会让「框还在
165
+ // 只是不可写」的语义变含糊)
166
+ ".dsh-recall-cfg-input:disabled,.dsh-recall-cfg-area:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}",
167
+ // 数字输入框隐藏原生加减微调按钮(spinner):34px 高的定宽框里 spinner 挤占
168
+ // 右侧数字区、跨引擎渲染不一(Chromium 上下箭头 / Firefox 无),与「右对齐
169
+ // 数字」的纵向扫描相冲;隐藏后键盘 ↑↓ 与直接输入仍可用,步进语义不丢。
170
+ // scope 到 cfg-input,不外溢影响宿主或其他插件的 number 输入。
171
+ ".dsh-recall-cfg-input::-webkit-inner-spin-button,.dsh-recall-cfg-input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}",
172
+ ".dsh-recall-cfg-input{-moz-appearance:textfield;appearance:textfield}",
173
+ ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:6px 8px;min-height:64px;box-sizing:border-box;width:100%;grid-column:2}",
174
+ // 布尔开关:官方设置表单的布尔字段用 role=switch 滑钮而非原生 checkbox
175
+ //(vCGm7G_switch 逐字配方:36×20 轨道 border-l3 底、开=brand-primary、
176
+ // 16px 拇指 label-primary-foreground 滑动 16px、transition .12s)
177
+ ".dsh-recall-cfg-switch{box-sizing:border-box;flex:none;width:36px;height:20px;padding:2px;border:0;border-radius:10px;background:var(--dsw-alias-border-l3);cursor:pointer;position:relative}",
178
+ '.dsh-recall-cfg-switch[aria-checked="true"]{background:var(--dsw-alias-brand-primary)}',
179
+ ".dsh-recall-cfg-switch:disabled{opacity:.4;cursor:default}",
180
+ ".dsh-recall-cfg-switch-thumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform .12s}",
181
+ '.dsh-recall-cfg-switch[aria-checked="true"] .dsh-recall-cfg-switch-thumb{transform:translateX(16px)}',
182
+ // hint padding-bottom 即组内行距的单一事实源(row-gap 只管网格行缝)——
183
+ // 行间节奏统一为 8+4=12px,避免各行松紧不一。
184
+ ".dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;grid-column:2;padding-bottom:8px}",
185
+ // 基础排除表通栏(baseExcludes textarea/hint):折叠展开后内容顶满卡片宽度,
186
+ // 消除左侧 label 列竖直死区。必须定义在 480px media 之前——media 内的
187
+ // grid-column:auto 同特异性、后出现,窄屏下仍能正确回落单列堆叠。
188
+ ".dsh-recall-cfg-span{grid-column:1/-1}",
189
+ // V4/V8 共用 480px 断点:cfg 表单单列堆叠(V4);exclude quick 行输入框独占
190
+ // 一行(flex:1 1 100%,basis 100% 强制换行,添加按钮与芯片建议另起一行)、
191
+ // panel-actions 长状态文案不再挤压按钮(wrap 已在基础规则,此处无需重复)。
192
+ "@media (max-width:480px){.dsh-recall-cfg-grid{grid-template-columns:minmax(0,1fr);row-gap:2px}.dsh-recall-cfg-line,.dsh-recall-cfg-hint,.dsh-recall-cfg-area{grid-column:auto}.dsh-recall-ex-quick .dsh-recall-ex-input{flex:1 1 100%}}",
193
+ ".dsh-recall-cfg-tag{flex:none;font-size:12px;line-height:1.4;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}",
194
+ // 已修改(用户改了配置)用 warn 底色提示「有未保存变更」——修改不是错误,
195
+ // warn 家族来自 V1 核验的官方配对(tertiary 底 + label 文)。
196
+ ".dsh-recall-cfg-tag-modified{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}",
197
+ // 环境变量锁定:系统锁住不可写,区别于「已覆盖」(值被更高优先源覆盖)——
198
+ // 两者同属中性,用左边框做结构级区分,不引入第三色。
199
+ ".dsh-recall-cfg-tag-locked{border-left:2px solid var(--dsw-alias-border-l2)}",
200
+ // V3 快照树加载骨架:items===null 时的 5 条占位行,pulse 明暗呼吸模拟加载。
201
+ ".dsh-recall-tree-skeleton{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
202
+ ".dsh-recall-tree-skeleton-row{height:20px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);animation:dsh-recall-pulse 1.2s ease-in-out infinite}",
203
+ "@keyframes dsh-recall-pulse{0%,100%{opacity:1}50%{opacity:.45}}"
204
+ ].join("");
205
+
206
+ // src/client/util.ts
207
+ function clockText(ms) {
208
+ try {
209
+ if (!ms || isNaN(new Date(ms).getTime())) return "";
210
+ const d = new Date(ms);
211
+ const now = /* @__PURE__ */ new Date();
212
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
213
+ const hh = String(d.getHours()).padStart(2, "0");
214
+ const mm = String(d.getMinutes()).padStart(2, "0");
215
+ return sameDay ? hh + ":" + mm : d.getMonth() + 1 + "/" + d.getDate() + " " + hh + ":" + mm;
216
+ } catch (e) {
217
+ return "";
218
+ }
219
+ }
220
+ function sizeText(bytes) {
221
+ const n = Number(bytes);
222
+ if (!bytes || n <= 0) return "0 MB";
223
+ if (n < 1048576) return (n / 1024).toFixed(0) + " KB";
224
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
225
+ return (n / 1073741824).toFixed(2) + " GB";
226
+ }
227
+ function bytesToMb(bytes) {
228
+ const n = Number(bytes);
229
+ if (!Number.isFinite(n) || n <= 0) return "";
230
+ return String(Math.round(n / 1048576 * 100) / 100);
231
+ }
232
+ function buildTree(list) {
233
+ const workspaces = /* @__PURE__ */ new Map();
234
+ for (const it of list || []) {
235
+ const rootKey = it.root || "unknown-root";
236
+ if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || "\u672A\u77E5\u5DE5\u4F5C\u533A", sessions: /* @__PURE__ */ new Map() });
237
+ const ws = workspaces.get(rootKey);
238
+ const sidKey = it.sessionId || "unknown-session";
239
+ if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] });
240
+ ws.sessions.get(sidKey).items.push(it);
241
+ }
242
+ const wsList = Array.from(workspaces.values()).map((ws) => ({ root: ws.root, name: ws.name, sessions: Array.from(ws.sessions.values()) }));
243
+ wsList.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
244
+ for (const ws of wsList) {
245
+ ws.sessions.sort((a, b) => (a.title || "").localeCompare(b.title || ""));
246
+ for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0));
247
+ }
248
+ return wsList;
249
+ }
250
+ function useAutoDismissMessage(React, state, setState) {
251
+ React.useEffect(() => {
252
+ if (!state.message || state.error || state.busy) return;
253
+ const timer = setTimeout(() => {
254
+ setState((prev) => prev.message === state.message && !prev.error && !prev.busy ? Object.assign({}, prev, { message: "" }) : prev);
255
+ }, 4e3);
256
+ return () => clearTimeout(timer);
257
+ }, [state.message, state.error, state.busy]);
258
+ }
259
+ function buildUtil() {
260
+ function api(name, args) {
261
+ return fetch("/api/recall/" + name, {
262
+ method: "POST",
263
+ headers: { "content-type": "application/json" },
264
+ body: JSON.stringify(args || {})
265
+ }).then((r) => r.json());
266
+ }
267
+ const CODE_TEXT = {
268
+ STALE: "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4",
269
+ AGENT_BUSY: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE",
270
+ NO_SNAPSHOT: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167",
271
+ NO_STORE: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528",
272
+ ROLLBACK_FAILED: "\u56DE\u9000\u5931\u8D25"
273
+ };
274
+ function messageFor(res, fallback) {
275
+ if (!res) return fallback;
276
+ const code = res.code;
277
+ if (code && CODE_TEXT[String(code)]) return CODE_TEXT[String(code)];
278
+ const m = res.message;
279
+ const e = res.error;
280
+ return String(m || e || fallback);
281
+ }
282
+ const noticeShown = /* @__PURE__ */ new Set();
283
+ function mountToast(text) {
284
+ if (typeof document === "undefined") return;
285
+ try {
286
+ let dismiss2 = function() {
287
+ if (dismissed) return;
288
+ dismissed = true;
289
+ clearTimeout(timer);
290
+ el.classList.remove("dsh-recall-toast-in");
291
+ setTimeout(() => el.remove(), 300);
292
+ };
293
+ var dismiss = dismiss2;
294
+ const el = document.createElement("div");
295
+ el.className = "dsh-recall-toast";
296
+ const tag = document.createElement("span");
297
+ tag.className = "dsh-recall-toast-tag";
298
+ tag.textContent = "\u64A4\u56DE\u63D2\u4EF6";
299
+ const body = document.createElement("span");
300
+ body.textContent = text;
301
+ el.appendChild(tag);
302
+ el.appendChild(body);
303
+ el.addEventListener("click", () => dismiss2(), { once: true });
304
+ document.body.appendChild(el);
305
+ requestAnimationFrame(() => el.classList.add("dsh-recall-toast-in"));
306
+ const timer = setTimeout(dismiss2, 7e3);
307
+ let dismissed = false;
308
+ } catch (e) {
309
+ }
310
+ }
311
+ function showNotice(kind, text) {
312
+ if (noticeShown.has(kind)) return;
313
+ noticeShown.add(kind);
314
+ mountToast(text);
315
+ }
316
+ const toastLastShown = /* @__PURE__ */ new Map();
317
+ function showThrottledToast(text) {
318
+ const key = String(text).slice(0, 80);
319
+ const now = Date.now();
320
+ if (now - (toastLastShown.get(key) || 0) < 10 * 60 * 1e3) return;
321
+ if (toastLastShown.size > 50) toastLastShown.clear();
322
+ toastLastShown.set(key, now);
323
+ mountToast(text);
324
+ }
325
+ const pluginConfig = { refillDraft: true, archiveOriginal: true };
326
+ const initMap = /* @__PURE__ */ new Map();
327
+ function ensureInit(sessionId) {
328
+ if (!sessionId) return Promise.resolve();
329
+ const cached = initMap.get(sessionId);
330
+ if (cached) return cached;
331
+ const done = api("init", { sessionId }).then((res) => {
332
+ if (res && res.config && typeof res.config === "object") {
333
+ const cfg = res.config;
334
+ if (typeof cfg.refillDraft === "boolean") pluginConfig.refillDraft = cfg.refillDraft;
335
+ if (typeof cfg.archiveOriginal === "boolean") pluginConfig.archiveOriginal = cfg.archiveOriginal;
336
+ }
337
+ const notice = res && res.notice;
338
+ if (notice && notice.unsupported) {
339
+ showNotice("unsupported", "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002");
340
+ }
341
+ if (notice && notice.gitMissing) {
342
+ showNotice("git", "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002");
343
+ }
344
+ if (notice && notice.homeFallback) {
345
+ showNotice("home", "home \u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u5FEB\u7167\u5DF2\u964D\u7EA7\u5B58\u50A8\u5230\u9879\u76EE\u5185 .dsh-recall-snapshots \u76EE\u5F55\u3002");
346
+ }
347
+ }).catch(() => {
348
+ initMap.delete(sessionId);
349
+ });
350
+ initMap.set(sessionId, done);
351
+ return done;
352
+ }
353
+ function writeClipboard(text) {
354
+ try {
355
+ if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
356
+ return navigator.clipboard.writeText(text).then(() => true, () => false);
357
+ }
358
+ } catch (e) {
359
+ }
360
+ try {
361
+ if (typeof document !== "undefined" && typeof document.execCommand === "function") {
362
+ const el = document.createElement("textarea");
363
+ el.value = text;
364
+ el.setAttribute("readonly", "");
365
+ el.style.position = "fixed";
366
+ el.style.left = "-9999px";
367
+ document.body.appendChild(el);
368
+ el.select();
369
+ try {
370
+ return Promise.resolve(document.execCommand("copy"));
371
+ } finally {
372
+ el.remove();
373
+ }
374
+ }
375
+ } catch (e) {
376
+ }
377
+ return Promise.resolve(false);
378
+ }
379
+ return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, pluginConfig };
380
+ }
381
+
382
+ // src/client/recall-node.ts
383
+ var KIND_INFO = {
384
+ modified: { label: "\u4FEE\u6539", cls: "modified" },
385
+ restored: { label: "\u6062\u590D", cls: "restored" },
386
+ added: { label: "\u5220\u9664", cls: "added" }
387
+ };
388
+ function summaryText(counts) {
389
+ const parts = [];
390
+ for (const kind of Object.keys(KIND_INFO)) {
391
+ if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + " " + counts[kind]);
392
+ }
393
+ return parts.join(" \xB7 ");
394
+ }
395
+ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
396
+ const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor } = util;
397
+ function CopyIcon() {
398
+ return React.createElement(
399
+ "svg",
400
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": true },
401
+ React.createElement("rect", { x: 5.5, y: 5.5, width: 8, height: 8, rx: 1.5 }),
402
+ React.createElement("path", { d: "M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5" })
403
+ );
404
+ }
405
+ function CheckIcon() {
406
+ return React.createElement(
407
+ "svg",
408
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
409
+ React.createElement("path", { d: "m3 8.5 3.2 3.2L13 5" })
410
+ );
411
+ }
412
+ function UndoIcon() {
413
+ return React.createElement(
414
+ "svg",
415
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
416
+ React.createElement("path", { d: "M7.5 3.5 3.5 7.5l4 4" }),
417
+ React.createElement("path", { d: "M4.5 7.5h5a3 3 0 0 1 0 6H8" })
418
+ );
419
+ }
420
+ function recallPanel(recall, closePanel, executeRecall) {
421
+ if (recall.stage === "loading") {
422
+ return React.createElement(
423
+ "div",
424
+ { className: "dsh-recall-panel" },
425
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u8BA1\u7B97\u53D8\u66F4\u2026")
426
+ );
427
+ }
428
+ if (recall.stage === "error") {
429
+ return React.createElement(
430
+ "div",
431
+ { className: "dsh-recall-panel" },
432
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u65E0\u6CD5\u56DE\u9000"),
433
+ React.createElement("div", { className: "dsh-recall-panel-note" }, recall.message || ""),
434
+ React.createElement(
435
+ "div",
436
+ { className: "dsh-recall-panel-actions" },
437
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
438
+ )
439
+ );
440
+ }
441
+ if (recall.stage === "confirm") {
442
+ const changes = recall.changes || [];
443
+ const total = typeof recall.total === "number" ? recall.total : changes.length;
444
+ const counts = { modified: 0, restored: 0, added: 0 };
445
+ for (const c of changes) {
446
+ if (c && counts[c.kind] !== void 0) counts[c.kind]++;
447
+ }
448
+ const rows = changes.map((c, i) => {
449
+ const info = KIND_INFO[c.kind];
450
+ return React.createElement(
451
+ "div",
452
+ { className: "dsh-recall-file", key: i },
453
+ React.createElement("span", { className: "dsh-recall-badge dsh-recall-badge-" + (info ? info.cls : "") }, info ? info.label : c.kind || ""),
454
+ React.createElement("span", { className: "dsh-recall-rel" }, c.rel || "")
455
+ );
456
+ });
457
+ if (recall.truncated) {
458
+ rows.push(React.createElement("div", { className: "dsh-recall-panel-note", key: "truncated" }, "\u2026\u4EC5\u663E\u793A\u524D " + changes.length + " \u6761\uFF0C\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4"));
459
+ }
460
+ const canRevertChat = typeof recall.cutSeq === "number";
461
+ return React.createElement(
462
+ "div",
463
+ { className: "dsh-recall-panel" },
464
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6574\u6BB5\u56DE\u9000"),
465
+ React.createElement(
466
+ "div",
467
+ { className: "dsh-recall-panel-note" },
468
+ "\u5C06\u9879\u76EE\u6062\u590D\u5230" + (recall.time ? " " + clockText2(recall.time) + " " : " ") + "\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4" + (summaryText(counts) ? "\uFF08" + summaryText(counts) + "\uFF09" : "") + "\u3002\u6B64\u64CD\u4F5C\u4F1A\u8986\u76D6\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF1B\u56DE\u9000\u524D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u4E00\u4EFD\u5F53\u524D\u72B6\u6001\u7684\u5B89\u5168\u5FEB\u7167\uFF08\u4E0D\u542B\u5728\u4E0B\u65B9\u6E05\u5355\u5185\uFF09\u3002"
469
+ ),
470
+ React.createElement(
471
+ "div",
472
+ { className: "dsh-recall-panel-note" },
473
+ canRevertChat ? "\u5BF9\u8BDD\u5C06\u4E00\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5168\u90E8\u5BF9\u8BDD\u4F1A\u4ECE\u5F53\u524D\u89C6\u56FE\u79FB\u9664\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u65E0\u6CD5\u56DE\u9000\u5BF9\u8BDD\uFF1B\u786E\u8BA4\u540E\u4EC5\u56DE\u9000\u9879\u76EE\u6587\u4EF6\u3002"
474
+ ),
475
+ changes.length > 0 ? React.createElement("div", { className: "dsh-recall-list" }, ...rows) : null,
476
+ React.createElement(
477
+ "div",
478
+ { className: "dsh-recall-panel-actions" },
479
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u53D6\u6D88"),
480
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", onClick: executeRecall }, "\u786E\u8BA4\u56DE\u9000")
481
+ )
482
+ );
483
+ }
484
+ if (recall.stage === "executing") {
485
+ return React.createElement(
486
+ "div",
487
+ { className: "dsh-recall-panel" },
488
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u56DE\u9000\u2026")
489
+ );
490
+ }
491
+ if (recall.stage === "done") {
492
+ return React.createElement(
493
+ "div",
494
+ { className: "dsh-recall-panel" },
495
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u56DE\u9000\u5B8C\u6210"),
496
+ React.createElement(
497
+ "div",
498
+ { className: "dsh-recall-panel-note" },
499
+ recall.chatReverted ? "\u9879\u76EE\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u9879\u76EE\u5DF2\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002" + (recall.chatError ? " \u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A" + recall.chatError : "")
500
+ ),
501
+ React.createElement(
502
+ "div",
503
+ { className: "dsh-recall-panel-actions" },
504
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
505
+ )
506
+ );
507
+ }
508
+ return null;
509
+ }
510
+ function fillDraft(targetSessionId, draftText) {
511
+ if (!draftText || !targetSessionId) return;
512
+ let attempts = 0;
513
+ const attempt = () => {
514
+ try {
515
+ const conversation = ctx.get("conversation");
516
+ if (conversation && conversation.input && typeof conversation.input.shell === "function") {
517
+ const shell = conversation.input.shell(targetSessionId);
518
+ if (shell) {
519
+ if (shell.actions && typeof shell.actions.setDraft === "function") {
520
+ shell.actions.setDraft(draftText);
521
+ return;
522
+ }
523
+ if (typeof shell.setDraft === "function") {
524
+ shell.setDraft(draftText);
525
+ return;
526
+ }
527
+ }
528
+ }
529
+ } catch (e) {
530
+ }
531
+ if (attempts++ < 8) setTimeout(attempt, 150);
532
+ };
533
+ attempt();
534
+ }
535
+ function UserRecallNode(props) {
536
+ const node = props && props.node;
537
+ const renderMessageImages = props && props.renderMessageImages;
538
+ const sessionId = props && props.sessionId;
539
+ const data = node && node.data ? node.data : {};
540
+ const messageId = node ? String(node.id || node.key || "") : "";
541
+ const blocks = Array.isArray(data.content) ? data.content : [];
542
+ const text = blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
543
+ const imageBlocks = blocks.filter((b) => b && b.type === "image" && b.attachment).map((b) => ({ attachment: b.attachment }));
544
+ const rest = blocks.filter((b) => !b || !(b.type === "text" && typeof b.text === "string") && !(b.type === "image" && b.attachment));
545
+ const [copied, setCopied] = React.useState(false);
546
+ const [hasSnapshot, setHasSnapshot] = React.useState(false);
547
+ const [recall, setRecall] = React.useState({ stage: "idle" });
548
+ React.useEffect(() => {
549
+ let alive = true;
550
+ let timer = null;
551
+ let attempts = 0;
552
+ const RETRY_WINDOW_MS = 5 * 60 * 1e3;
553
+ const MAX_ATTEMPTS = 20;
554
+ const RETRY_MS = 1e3;
555
+ const msgTime = data && typeof data.time === "number" ? data.time : NaN;
556
+ const recent = !isNaN(msgTime) && Date.now() - msgTime <= RETRY_WINDOW_MS;
557
+ function schedule() {
558
+ if (!alive || !messageId) return;
559
+ attempts++;
560
+ api("snapshot-info", { messageId, sessionId }).then((res) => {
561
+ if (!alive) return;
562
+ if (res && res.has) {
563
+ if (recent && Array.isArray(res.skipped) && res.skipped.length) {
564
+ const names = res.skipped.slice(0, 5).join("\u3001") + (res.skipped.length > 5 ? " \u7B49 " + res.skipped.length + " \u9879" : "");
565
+ showThrottledToast("\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A" + names + "\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09");
566
+ }
567
+ setHasSnapshot(true);
568
+ return;
569
+ }
570
+ if (res && res.failed) {
571
+ if (recent) showThrottledToast("\u5FEB\u7167\u5931\u8D25\uFF1A" + String(res.error || "\u672A\u77E5\u539F\u56E0").slice(0, 140));
572
+ return;
573
+ }
574
+ if (recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
575
+ }).catch(() => {
576
+ if (alive && recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
577
+ });
578
+ }
579
+ ensureInit(sessionId).then(() => {
580
+ if (!messageId || !alive) return;
581
+ schedule();
582
+ }).catch(() => {
583
+ if (alive && messageId) timer = setTimeout(schedule, RETRY_MS);
584
+ });
585
+ return () => {
586
+ alive = false;
587
+ if (timer !== null) clearTimeout(timer);
588
+ };
589
+ }, [messageId, sessionId]);
590
+ const onCopy = () => {
591
+ if (copied) return;
592
+ writeClipboard(text).then(() => {
593
+ setCopied(true);
594
+ const timer = ctx.timer;
595
+ if (timer && typeof timer.timeout === "function") {
596
+ timer.timeout(() => setCopied(false), 1200);
597
+ } else {
598
+ setTimeout(() => setCopied(false), 1200);
599
+ }
600
+ });
601
+ };
602
+ const openPreview = () => {
603
+ if (recall.stage === "loading" || recall.stage === "executing") return;
604
+ setRecall({ stage: "loading" });
605
+ api("preview", { messageId, sessionId }).then((res) => {
606
+ if (!res || !res.ok) {
607
+ setRecall({ stage: "error", message: messageFor(res, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
608
+ return;
609
+ }
610
+ setRecall({
611
+ stage: "confirm",
612
+ changes: res.changes || [],
613
+ total: typeof res.total === "number" ? res.total : (res.changes || []).length,
614
+ truncated: Boolean(res.truncated),
615
+ treeId: res.treeId || null,
616
+ time: res.time || null,
617
+ cutSeq: typeof res.cutSeq === "number" ? res.cutSeq : null
618
+ });
619
+ }).catch((error) => {
620
+ setRecall({ stage: "error", message: String(error) });
621
+ });
622
+ };
623
+ const executeRecall = () => {
624
+ if (recall.stage !== "confirm") return;
625
+ const changes = recall.changes || [];
626
+ const previewCut = typeof recall.cutSeq === "number" ? recall.cutSeq : null;
627
+ const previewTotal = typeof recall.total === "number" ? recall.total : changes.length;
628
+ setRecall({ stage: "executing", changes });
629
+ api("execute", { messageId, sessionId, previewTotal, previewTreeId: recall.treeId || void 0, previewAt: Date.now() }).then(async (res) => {
630
+ if (!res || !res.ok) {
631
+ if (res && res.code === "STALE") {
632
+ setRecall({ stage: "loading" });
633
+ api("preview", { messageId, sessionId }).then((res2) => {
634
+ if (!res2 || !res2.ok) {
635
+ setRecall({ stage: "error", message: messageFor(res2, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
636
+ return;
637
+ }
638
+ setRecall({
639
+ stage: "confirm",
640
+ changes: res2.changes || [],
641
+ total: typeof res2.total === "number" ? res2.total : (res2.changes || []).length,
642
+ truncated: Boolean(res2.truncated),
643
+ treeId: res2.treeId || null,
644
+ time: res2.time || null,
645
+ cutSeq: typeof res2.cutSeq === "number" ? res2.cutSeq : null
646
+ });
647
+ }).catch((error) => {
648
+ setRecall({ stage: "error", message: String(error) });
649
+ });
650
+ return;
651
+ }
652
+ setRecall({ stage: "error", message: messageFor(res, "\u56DE\u9000\u5931\u8D25") });
653
+ return;
654
+ }
655
+ const cutSeq = typeof res.cutSeq === "number" ? res.cutSeq : previewCut;
656
+ let chatReverted = false;
657
+ let chatError = "";
658
+ let fillTarget = sessionId;
659
+ if (cutSeq !== null && sessionsSvc && typeof sessionsSvc.fork === "function") {
660
+ try {
661
+ const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq });
662
+ if (childId) {
663
+ if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
664
+ chatReverted = true;
665
+ fillTarget = childId;
666
+ api("lineage-record", { childId, parentId: sessionId }).catch(() => {
667
+ });
668
+ if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === "function") {
669
+ workspacesSvc.archiveSession(sessionId).catch(() => {
670
+ });
671
+ }
672
+ } else {
673
+ chatError = "\u672A\u8FD4\u56DE\u65B0\u4F1A\u8BDD";
674
+ }
675
+ } catch (error) {
676
+ chatError = String(error);
677
+ }
678
+ }
679
+ if (pluginConfig.refillDraft) fillDraft(fillTarget, text);
680
+ setHasSnapshot(false);
681
+ setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
682
+ }).catch((error) => {
683
+ setRecall({ stage: "error", message: String(error) });
684
+ });
685
+ };
686
+ const closePanel = () => setRecall({ stage: "idle" });
687
+ const bubbleChildren = [];
688
+ if (imageBlocks.length && typeof renderMessageImages === "function") {
689
+ const render = renderMessageImages;
690
+ bubbleChildren.push(React.createElement(
691
+ React.Fragment,
692
+ { key: "images" },
693
+ render({ images: imageBlocks, align: "end" })
694
+ ));
695
+ }
696
+ if (text !== "") bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" }, text));
697
+ for (let i = 0; i < rest.length; i++) {
698
+ bubbleChildren.push(React.createElement("pre", { className: "dsh-recall-json", key: "rest-" + i }, JSON.stringify(rest[i], null, 2)));
699
+ }
700
+ const actions = [];
701
+ actions.push(React.createElement("span", { className: "dsh-recall-time", key: "time" }, clockText2(data.time)));
702
+ actions.push(React.createElement("button", {
703
+ key: "copy",
704
+ type: "button",
705
+ className: "dsh-recall-action",
706
+ "aria-label": copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
707
+ title: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
708
+ onClick: onCopy
709
+ }, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})));
710
+ if (hasSnapshot) {
711
+ actions.push(React.createElement("button", {
712
+ key: "recall",
713
+ type: "button",
714
+ className: "dsh-recall-action",
715
+ "aria-label": "\u64A4\u56DE",
716
+ title: "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
717
+ onClick: openPreview
718
+ }, React.createElement(UndoIcon, {})));
719
+ }
720
+ return React.createElement(
721
+ "div",
722
+ { className: "dsh-recall-row", "data-time-hover-root": true },
723
+ bubbleChildren.length > 0 ? React.createElement("div", { className: "dsh-recall-stack", key: "stack" }, ...bubbleChildren) : null,
724
+ React.createElement("div", { className: "dsh-recall-actions", key: "actions" }, ...actions),
725
+ recallPanel(recall, closePanel, executeRecall)
726
+ );
727
+ }
728
+ return { UserRecallNode };
729
+ }
730
+
731
+ // src/client/config-card.ts
732
+ function buildConfigForm(React, util, SectionToggle) {
733
+ const { api, bytesToMb: bytesToMb2 } = util;
734
+ function ConfigForm() {
735
+ const [baseline, setBaseline] = React.useState(null);
736
+ const [draft, setDraft] = React.useState(null);
737
+ const [envLocks, setEnvLocks] = React.useState({});
738
+ const [overridden, setOverridden] = React.useState({});
739
+ const [writable, setWritable] = React.useState(true);
740
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
741
+ useAutoDismissMessage(React, state, setState);
742
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
743
+ function load() {
744
+ api("config-get", {}).then((res) => {
745
+ if (res && res.ok) {
746
+ const v = res.values;
747
+ const next = {
748
+ gcSnaps: String(v.gcSnaps == null ? "" : v.gcSnaps),
749
+ gcHours: String(v.gcHours == null ? "" : v.gcHours),
750
+ maxFileBytes: bytesToMb2(v.maxFileBytes),
751
+ maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? "" : v.maxSnapshotsPerWorkspace),
752
+ baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join("\n") : "",
753
+ refillDraft: v.refillDraft !== false,
754
+ snapshotEnabled: v.snapshotEnabled !== false,
755
+ archiveOriginal: v.archiveOriginal !== false,
756
+ retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
757
+ };
758
+ setDraft(next);
759
+ setBaseline(next);
760
+ setEnvLocks(res.envLocks || {});
761
+ setOverridden(res.overridden || {});
762
+ setWritable(res.writable !== false);
763
+ } else {
764
+ setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
765
+ }
766
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
767
+ }
768
+ React.useEffect(() => {
769
+ load();
770
+ }, []);
771
+ function edit(key, value) {
772
+ setDraft((d) => Object.assign({}, d, { [key]: value }));
773
+ }
774
+ function save() {
775
+ if (state.busy || !draft || !baseline) return;
776
+ const patch = {};
777
+ for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
778
+ if (draft[key] !== baseline[key]) patch[key] = draft[key];
779
+ }
780
+ if (!Object.keys(patch).length) {
781
+ setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
782
+ return;
783
+ }
784
+ const clean = {};
785
+ if (patch.gcSnaps !== void 0) {
786
+ const n = parseInt(String(patch.gcSnaps), 10);
787
+ if (!Number.isFinite(n) || n < 1) {
788
+ setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
789
+ return;
790
+ }
791
+ clean.gcSnaps = n;
792
+ }
793
+ if (patch.gcHours !== void 0) {
794
+ const n = parseInt(String(patch.gcHours), 10);
795
+ if (!Number.isFinite(n) || n < 1) {
796
+ setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
797
+ return;
798
+ }
799
+ clean.gcHours = n;
800
+ }
801
+ if (patch.maxFileBytes !== void 0) {
802
+ const mb = Number(patch.maxFileBytes);
803
+ if (!Number.isFinite(mb) || mb < 0.01) {
804
+ setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
805
+ return;
806
+ }
807
+ clean.maxFileBytes = Math.round(mb * 1048576);
808
+ }
809
+ if (patch.maxSnapshotsPerWorkspace !== void 0) {
810
+ const n = parseInt(String(patch.maxSnapshotsPerWorkspace), 10);
811
+ if (!Number.isFinite(n) || n < 0) {
812
+ setState({ busy: false, message: "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09", error: true });
813
+ return;
814
+ }
815
+ clean.maxSnapshotsPerWorkspace = n;
816
+ }
817
+ if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
818
+ if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
819
+ if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
820
+ if (patch.retentionDays !== void 0) {
821
+ const n = parseInt(String(patch.retentionDays), 10);
822
+ if (!Number.isFinite(n) || n < 0) {
823
+ setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
824
+ return;
825
+ }
826
+ clean.retentionDays = n;
827
+ }
828
+ if (patch.baseExcludes !== void 0) {
829
+ clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
830
+ }
831
+ setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
832
+ api("config-set", { patch: clean }).then((res) => {
833
+ if (res && res.ok) {
834
+ setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
835
+ load();
836
+ } else {
837
+ setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
838
+ }
839
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
840
+ }
841
+ function numRow(key, label, hint, opts) {
842
+ const locked = Boolean(envLocks && envLocks[key]);
843
+ const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
844
+ return React.createElement(
845
+ "div",
846
+ { className: "dsh-recall-cfg-row", key },
847
+ // V4:label 上提为 cfg-row 直接子元素——grid 第一列(max-content)跨行
848
+ // 对齐最长标签,消灭 130px 定宽魔法数与 hint 138px 缩进耦合;
849
+ // htmlFor-id 关联不变(V2)
850
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-" + key }, label),
851
+ React.createElement(
852
+ "div",
853
+ { className: "dsh-recall-cfg-line" },
854
+ React.createElement("input", {
855
+ id: "dsh-recall-cfg-" + key,
856
+ className: "dsh-recall-cfg-input",
857
+ type: "number",
858
+ value: draft ? draft[key] : "",
859
+ disabled: locked || !writable,
860
+ min: opts && opts.min,
861
+ step: opts && opts.step,
862
+ onChange: (e) => edit(key, e.target.value)
863
+ }),
864
+ opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
865
+ changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
866
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
867
+ locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-locked" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
868
+ ),
869
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
870
+ );
871
+ }
872
+ function boolRow(key, label, hint) {
873
+ const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
874
+ const on = Boolean(draft && draft[key]);
875
+ return React.createElement(
876
+ "div",
877
+ { className: "dsh-recall-cfg-row", key },
878
+ // label 上提为 cfg-row 直接子元素(与 numRow 同法,V4 跨行对齐契约)
879
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-" + key }, label),
880
+ React.createElement(
881
+ "div",
882
+ { className: "dsh-recall-cfg-line" },
883
+ React.createElement("button", {
884
+ id: "dsh-recall-cfg-" + key,
885
+ type: "button",
886
+ role: "switch",
887
+ "aria-checked": on,
888
+ className: "dsh-recall-cfg-switch",
889
+ disabled: !writable,
890
+ onClick: () => edit(key, !on)
891
+ }, React.createElement("span", { className: "dsh-recall-cfg-switch-thumb" })),
892
+ changed ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
893
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
894
+ ),
895
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
896
+ );
897
+ }
898
+ function resetDefaults() {
899
+ if (state.busy || !writable) return;
900
+ setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
901
+ api("config-reset", {}).then((res) => {
902
+ if (res && res.ok) {
903
+ load();
904
+ setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
905
+ } else {
906
+ setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
907
+ }
908
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
909
+ }
910
+ if (!draft || !baseline) {
911
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
912
+ }
913
+ return React.createElement(
914
+ "div",
915
+ { className: "dsh-recall-ex-card" },
916
+ // 全部行包进单一 cfg-grid:cfg-row 是 display:contents 透明层(css.ts),
917
+ // label/控件行/hint 直接成为同一 grid 的 item,第一列 max-content 由全表单
918
+ // 最长 label 决定——checkbox 与数字输入框的控件列自此跨行对齐(此前每行
919
+ // 是独立 grid 容器,max-content 各算各的,跨行对齐从未成立,实测参差)。
920
+ React.createElement(
921
+ "div",
922
+ { className: "dsh-recall-cfg-grid" },
923
+ // V5 表单分组:9 字段平铺 → 「快照行为 / 自动治理」两组语义分组小标题,
924
+ // 降低认知负担;「高级:基础排除表」沿用 SectionToggle 折叠,不重复加标题。
925
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u5FEB\u7167\u884C\u4E3A"),
926
+ boolRow("snapshotEnabled", "\u542F\u7528\u5FEB\u7167", "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF08\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09\uFF0C\u9002\u5408\u4E34\u65F6\u7981\u7528\u5FEB\u7167\u7684\u573A\u5408"),
927
+ boolRow("refillDraft", "\u64A4\u56DE\u540E\u56DE\u586B\u8F93\u5165\u6846", "\u64A4\u56DE\u6210\u529F\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846\uFF0C\u65B9\u4FBF\u4FEE\u6539\u540E\u91CD\u65B0\u53D1\u9001"),
928
+ boolRow("archiveOriginal", "\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD", "\u64A4\u56DE\u540E\u539F\u4F1A\u8BDD\u4ECE\u5217\u8868\u5F52\u6863\u9690\u85CF\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\uFF1B\u5173\u95ED\u5219\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF0C\u65B9\u4FBF\u5BF9\u7167\u56DE\u9000\u524D\u540E\u7684\u4E0A\u4E0B\u6587"),
929
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u81EA\u52A8\u6CBB\u7406"),
930
+ // 单位后缀统一挂输入框右侧(与状态标签同基线),不再只藏在说明文字里
931
+ numRow("gcSnaps", "gc \u89E6\u53D1\u6761\u6570", "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 git gc", { suffix: "\u6761", min: 1, step: 1 }),
932
+ numRow("gcHours", "gc \u89E6\u53D1\u5C0F\u65F6", "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF08\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1\uFF09", { suffix: "\u5C0F\u65F6", min: 1, step: 1 }),
933
+ numRow("maxFileBytes", "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650", "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\uFF08\u5355\u4F4D MB\uFF0C\u652F\u6301\u5C0F\u6570\uFF09", { suffix: "MB", min: 0.01, step: 0.5 }),
934
+ numRow("maxSnapshotsPerWorkspace", "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650", "\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u9650\u5236", { suffix: "\u6761", min: 0, step: 1 }),
935
+ numRow("retentionDays", "\u5FEB\u7167\u4FDD\u7559\u5929\u6570", "\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u542F\u7528\uFF08\u4E0E\u5FEB\u7167\u603B\u6570\u4E0A\u9650\u5404\u81EA\u751F\u6548\uFF09", { suffix: "\u5929", min: 0, step: 1 }),
936
+ // 操作区在「基础排除表」折叠头之前:按钮服务整个表单(含折叠区之外的字段),
937
+ // 排在折叠头之后会被误读为折叠区内容、折叠时像漏收起(用户实测反馈);
938
+ // 因此也不把按钮藏进折叠分支——否则折叠基础排除表后将无法保存。
939
+ React.createElement(
940
+ "div",
941
+ { className: "dsh-recall-panel-actions" },
942
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
943
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(baseline ? Object.assign({}, baseline) : null) }, "\u653E\u5F03\u4FEE\u6539"),
944
+ React.createElement("button", {
945
+ type: "button",
946
+ className: "dsh-recall-btn",
947
+ disabled: state.busy || !writable,
948
+ title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
949
+ onClick: resetDefaults
950
+ }, "\u6062\u590D\u9ED8\u8BA4"),
951
+ // 「保存」是表单唯一主动作,升主色实心按钮;放弃修改/恢复默认维持次级
952
+ // 灰底(同排按钮只有一个视觉焦点,配色令牌经官方主题产物核验,见 css.ts)
953
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
954
+ !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
955
+ ),
956
+ React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
957
+ showAdvanced ? React.createElement(
958
+ "div",
959
+ { className: "dsh-recall-cfg-row", key: "baseExcludes" },
960
+ // V4:label 上提(与 numRow 同法);cfg-line 只剩 tags,textarea/hint 各自归第二列
961
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-baseExcludes" }, "\u57FA\u7840\u6392\u9664\u8868"),
962
+ React.createElement(
963
+ "div",
964
+ { className: "dsh-recall-cfg-line" },
965
+ draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
966
+ overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
967
+ ),
968
+ // textarea/hint cfg-span 通栏:折叠区在共享 grid 内展开时,内容若只占
969
+ // 第二列,左侧长 label 列会成为竖直死区(实测 textarea 被挤窄);通栏后
970
+ // 与 label/tags 行左缘对齐。label 保持原位不动——htmlFor 关联是 a11y 契约。
971
+ React.createElement("textarea", {
972
+ id: "dsh-recall-cfg-baseExcludes",
973
+ className: "dsh-recall-cfg-area dsh-recall-cfg-span",
974
+ rows: 4,
975
+ value: draft.baseExcludes,
976
+ disabled: !writable,
977
+ onChange: (e) => edit("baseExcludes", e.target.value)
978
+ }),
979
+ React.createElement("div", { className: "dsh-recall-cfg-hint dsh-recall-cfg-span" }, "\u5185\u7F6E\u89C4\u5219\uFF0C\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u5171\u4EAB\uFF0C\u5EFA\u8BAE\u4FDD\u6301\u9ED8\u8BA4\uFF1Bgitignore \u8BED\u6CD5\u6BCF\u884C\u4E00\u6761\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u91CC\u7684 exclude.txt\uFF08S3-2 \u6298\u53E0\uFF09")
980
+ ) : null
981
+ )
982
+ );
983
+ }
984
+ return { ConfigForm };
985
+ }
986
+
987
+ // src/client/exclude-card.ts
988
+ var EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
989
+ function buildExcludeCards(React, util) {
990
+ const { api } = util;
991
+ function ExcludeCard(props) {
992
+ const file = props.file;
993
+ const [draft, setDraft] = React.useState(file.content || "");
994
+ const [baseline, setBaseline] = React.useState(file.content || "");
995
+ const [quick, setQuick] = React.useState("");
996
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
997
+ useAutoDismissMessage(React, state, setState);
998
+ const dirty = draft !== baseline;
999
+ function appendPattern(pattern) {
1000
+ setDraft((d) => (d && !d.endsWith("\n") ? d + "\n" : d) + pattern + "\n");
1001
+ }
1002
+ function addQuick() {
1003
+ const t = quick.trim();
1004
+ if (!t) return;
1005
+ appendPattern(t);
1006
+ setQuick("");
1007
+ }
1008
+ function save() {
1009
+ if (state.busy || !dirty) return;
1010
+ setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1011
+ api("exclude-set", { path: file.path, content: draft }).then((res) => {
1012
+ if (res && res.ok) {
1013
+ setBaseline(draft);
1014
+ setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6B21\u5FEB\u7167 / \u9884\u89C8 / \u56DE\u9000\u65F6\u751F\u6548", error: false });
1015
+ } else {
1016
+ setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1017
+ }
1018
+ }).catch((error) => {
1019
+ setState({ busy: false, message: String(error), error: true });
1020
+ });
1021
+ }
1022
+ function discard() {
1023
+ if (state.busy) return;
1024
+ setDraft(baseline);
1025
+ setState({ busy: false, message: "", error: false });
1026
+ }
1027
+ const draftLines = draft.split("\n").map((l) => l.trim());
1028
+ const suggestions = EXCLUDE_SUGGESTIONS.filter((s) => draftLines.indexOf(s) < 0);
1029
+ return React.createElement(
1030
+ "div",
1031
+ { className: "dsh-recall-ex-card" },
1032
+ // 不渲染「快照排除项」内标题(与折叠头「排除配置(exclude.txt)」语义重复);
1033
+ // 多文件场景(降级工作区)靠紧随说明的存储路径行区分,标题文本本就相同、
1034
+ // 不承担区分职责。路径从长句中抽出独立等宽行——Windows 长路径内联在句号
1035
+ // 前会撑出断裂换行(实测丑),独立行 break-all 整齐折行。
1036
+ React.createElement(
1037
+ "div",
1038
+ { className: "dsh-recall-ex-note" },
1039
+ file.home ? "\u6B64\u914D\u7F6E\u5168\u5C40\u5171\u4EAB\uFF0C\u5BF9\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5FEB\u7167\u751F\u6548\u3002" : "home \u76EE\u5F55\u4E0D\u53EF\u5199\u65F6\u6B64\u5DE5\u4F5C\u533A\u964D\u7EA7\u5B58\u50A8\uFF0C\u6392\u9664\u914D\u7F6E\u72EC\u7ACB\u751F\u6548\u3002"
1040
+ ),
1041
+ React.createElement("div", { className: "dsh-recall-ex-path" }, "\u5B58\u50A8\u4F4D\u7F6E\uFF1A" + file.path),
1042
+ React.createElement("div", { className: "dsh-recall-ex-note" }, "gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF0C\u652F\u6301 # \u6CE8\u91CA\uFF1B\u547D\u4E2D\u6392\u9664\u7684\u6587\u4EF6\u4E0E\u76EE\u5F55\u4E0D\u8FDB\u5165\u5FEB\u7167\uFF0C\u4E5F\u4E0D\u4F1A\u88AB\u56DE\u9000\u89E6\u78B0\u3002"),
1043
+ React.createElement("textarea", {
1044
+ className: "dsh-recall-ex-area",
1045
+ "aria-label": "\u5FEB\u7167\u6392\u9664\u6A21\u5F0F\u5217\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF09",
1046
+ value: draft,
1047
+ spellCheck: false,
1048
+ onChange: (e) => setDraft(e.target.value)
1049
+ }),
1050
+ React.createElement(
1051
+ "div",
1052
+ { className: "dsh-recall-ex-quick" },
1053
+ React.createElement("input", {
1054
+ className: "dsh-recall-ex-input",
1055
+ value: quick,
1056
+ placeholder: "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
1057
+ "aria-label": "\u5FEB\u901F\u6DFB\u52A0\u6392\u9664\u6A21\u5F0F",
1058
+ onChange: (e) => setQuick(e.target.value),
1059
+ onKeyDown: (e) => {
1060
+ if (e.key === "Enter") {
1061
+ e.preventDefault();
1062
+ addQuick();
1063
+ }
1064
+ }
1065
+ }),
1066
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: addQuick }, "\u6DFB\u52A0"),
1067
+ ...suggestions.map((s) => React.createElement("button", {
1068
+ key: "chip-" + s,
1069
+ type: "button",
1070
+ className: "dsh-recall-ex-chip",
1071
+ title: "\u70B9\u51FB\u8FFD\u52A0 " + s,
1072
+ onClick: () => appendPattern(s)
1073
+ }, s))
1074
+ ),
1075
+ React.createElement(
1076
+ "div",
1077
+ { className: "dsh-recall-panel-actions" },
1078
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
1079
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
1080
+ // 保存升主色实心(与配置表单同一约定:每卡唯一主动作,对齐官方插件卡
1081
+ // footer 的 discard 幽灵 + save 实心组合)
1082
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-primary", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
1083
+ )
1084
+ );
1085
+ }
1086
+ function ExcludeFilesSection() {
1087
+ const [files, setFiles] = React.useState(null);
1088
+ const [error, setError] = React.useState("");
1089
+ function load() {
1090
+ api("exclude-get", {}).then((res) => {
1091
+ if (res && res.ok) {
1092
+ setFiles(res.files || []);
1093
+ setError("");
1094
+ return;
1095
+ }
1096
+ if (res && res.unsupported) {
1097
+ setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1098
+ return;
1099
+ }
1100
+ setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1101
+ }).catch((e) => setError(String(e)));
1102
+ }
1103
+ React.useEffect(() => {
1104
+ load();
1105
+ }, []);
1106
+ if (error) {
1107
+ return React.createElement(
1108
+ "div",
1109
+ { className: "dsh-recall-ex-card" },
1110
+ React.createElement("div", { className: "dsh-recall-ex-note" }, error),
1111
+ React.createElement(
1112
+ "div",
1113
+ { className: "dsh-recall-panel-actions" },
1114
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
1115
+ )
1116
+ );
1117
+ }
1118
+ if (files === null) {
1119
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
1120
+ }
1121
+ if (!files.length) {
1122
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002");
1123
+ }
1124
+ return React.createElement(
1125
+ "div",
1126
+ { className: "dsh-recall-ex-card" },
1127
+ ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1128
+ );
1129
+ }
1130
+ return { ExcludeFilesSection };
1131
+ }
1132
+
1133
+ // src/client/snapshot-manager.ts
1134
+ function groupByLineage(ids, lineage) {
1135
+ const childOf = /* @__PURE__ */ new Map();
1136
+ const childrenOf = /* @__PURE__ */ new Map();
1137
+ for (const e of lineage || []) {
1138
+ if (e && e.childId && e.parentId) {
1139
+ const child = String(e.childId);
1140
+ const parent = String(e.parentId);
1141
+ childOf.set(child, parent);
1142
+ const kids = childrenOf.get(parent) || [];
1143
+ kids.push(child);
1144
+ childrenOf.set(parent, kids);
1145
+ }
1146
+ }
1147
+ const idSet = new Set((ids || []).map((v) => String(v)));
1148
+ const result = /* @__PURE__ */ new Map();
1149
+ const assigned = /* @__PURE__ */ new Set();
1150
+ for (const id of idSet) {
1151
+ if (assigned.has(id)) continue;
1152
+ let root = id;
1153
+ const seen = /* @__PURE__ */ new Set();
1154
+ while (childOf.has(root) && idSet.has(childOf.get(root)) && !seen.has(root)) {
1155
+ seen.add(root);
1156
+ root = childOf.get(root);
1157
+ }
1158
+ const chain = [];
1159
+ const queue = [root];
1160
+ while (queue.length) {
1161
+ const cur = queue.shift();
1162
+ if (!cur || !idSet.has(cur) || assigned.has(cur)) continue;
1163
+ chain.push(cur);
1164
+ assigned.add(cur);
1165
+ for (const k of childrenOf.get(cur) || []) queue.push(k);
1166
+ }
1167
+ if (chain.length > 1) {
1168
+ chain.forEach((sid, i) => result.set(sid, { family: chain, index: i + 1 }));
1169
+ }
1170
+ }
1171
+ return result;
1172
+ }
1173
+ function buildSnapshotManager(React, util, sessionsSvc) {
1174
+ const { api, clockText: clockText2, sizeText: sizeText2, buildTree: buildTree2 } = util;
1175
+ function ManageCard() {
1176
+ const [items, setItems] = React.useState(null);
1177
+ const [usage, setUsage] = React.useState(null);
1178
+ const [errors, setErrors] = React.useState(null);
1179
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
1180
+ useAutoDismissMessage(React, state, setState);
1181
+ const [limit, setLimit] = React.useState(200);
1182
+ const [total, setTotal] = React.useState(0);
1183
+ const [health, setHealth] = React.useState(null);
1184
+ const [query, setQuery] = React.useState("");
1185
+ const [showAllErrors, setShowAllErrors] = React.useState(false);
1186
+ const [titlesPending, setTitlesPending] = React.useState(false);
1187
+ const [lineage, setLineage] = React.useState([]);
1188
+ function fetchTitles(list) {
1189
+ const missing = Array.from(new Set(
1190
+ (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
1191
+ )).slice(0, 100);
1192
+ if (!missing.length) {
1193
+ setTitlesPending(false);
1194
+ return;
1195
+ }
1196
+ setTitlesPending(true);
1197
+ api("manage", { op: "titles", sessionIds: missing }).then((res) => {
1198
+ const map = res && res.ok ? res.titles : null;
1199
+ if (map) {
1200
+ setItems((prev) => (prev || []).map((it) => it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it));
1201
+ }
1202
+ setTitlesPending(false);
1203
+ }).catch(() => setTitlesPending(false));
1204
+ }
1205
+ function fetchMessages(list) {
1206
+ const requests = (list || []).filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, "messageText")).map((it) => ({ sessionId: it.sessionId, messageId: it.id })).slice(0, 200);
1207
+ if (!requests.length) return;
1208
+ api("manage", { op: "messages", requests }).then((res) => {
1209
+ const map = res && res.ok ? res.messageTexts : null;
1210
+ if (map) {
1211
+ setItems((prev) => (prev || []).map((it) => it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it));
1212
+ }
1213
+ }).catch(() => {
1214
+ });
1215
+ }
1216
+ function refresh(overLimit) {
1217
+ const useLimit = overLimit || limit;
1218
+ api("manage", { op: "list", limit: useLimit }).then((res) => {
1219
+ if (res && res.ok) {
1220
+ setItems(res.items || []);
1221
+ setTotal(typeof res.total === "number" ? res.total : (res.items || []).length);
1222
+ fetchTitles(res.items || []);
1223
+ fetchMessages(res.items || []);
1224
+ if (res.stale) {
1225
+ api("manage", { op: "list", limit: useLimit }).then((res2) => {
1226
+ if (res2 && res2.ok && !res2.stale) {
1227
+ setItems(res2.items || []);
1228
+ setTotal(typeof res2.total === "number" ? res2.total : (res2.items || []).length);
1229
+ fetchTitles(res2.items || []);
1230
+ fetchMessages(res2.items || []);
1231
+ }
1232
+ }).catch(() => {
1233
+ });
1234
+ }
1235
+ }
1236
+ api("manage", { op: "lineage" }).then((res2) => {
1237
+ if (res2 && res2.ok && Array.isArray(res2.lineage)) setLineage(res2.lineage);
1238
+ }).catch(() => {
1239
+ });
1240
+ api("manage", { op: "usage" }).then((res2) => {
1241
+ if (res2 && res2.ok) {
1242
+ setUsage(res2.bytes || 0);
1243
+ setHealth({ gitAvailable: res2.gitAvailable !== false, homeStores: res2.homeStores || 0, fallbackStores: res2.fallbackStores || 0 });
1244
+ }
1245
+ }).catch(() => {
1246
+ });
1247
+ api("status", {}).then((res2) => {
1248
+ if (res2 && res2.ok) setErrors(res2.errors || []);
1249
+ }).catch(() => {
1250
+ });
1251
+ }).catch(() => {
1252
+ api("manage", { op: "usage" }).then((res) => {
1253
+ if (res && res.ok) {
1254
+ setUsage(res.bytes || 0);
1255
+ setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 });
1256
+ }
1257
+ }).catch(() => {
1258
+ });
1259
+ api("status", {}).then((res) => {
1260
+ if (res && res.ok) setErrors(res.errors || []);
1261
+ }).catch(() => {
1262
+ });
1263
+ });
1264
+ }
1265
+ React.useEffect(() => {
1266
+ refresh();
1267
+ }, []);
1268
+ function clearErrors() {
1269
+ setErrors([]);
1270
+ api("status", { op: "clear" }).catch(() => {
1271
+ });
1272
+ }
1273
+ function run(op, extra, doneText) {
1274
+ if (state.busy) return;
1275
+ setState({ busy: true, message: "\u6267\u884C\u4E2D\u2026", error: false });
1276
+ api("manage", Object.assign({ op }, extra || {})).then((res) => {
1277
+ if (res && res.ok) {
1278
+ const deleted = res.deleted;
1279
+ setState({ busy: false, message: typeof deleted === "number" ? "\u5DF2\u5220\u9664 " + deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
1280
+ refresh();
1281
+ } else {
1282
+ setState({ busy: false, message: res && (res.message || res.error) || "\u64CD\u4F5C\u5931\u8D25", error: true });
1283
+ }
1284
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1285
+ }
1286
+ const [expanded, setExpanded] = React.useState(() => /* @__PURE__ */ new Set());
1287
+ const [confirming, setConfirming] = React.useState(null);
1288
+ function ConfirmRow(props) {
1289
+ return React.createElement(
1290
+ "div",
1291
+ { className: "dsh-recall-tree-confirm" },
1292
+ props.text,
1293
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip dsh-recall-ex-chip-danger", onClick: props.onConfirm }, "\u786E\u8BA4"),
1294
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: props.onCancel }, "\u53D6\u6D88")
1295
+ );
1296
+ }
1297
+ function renderDeleteAllConfirm() {
1298
+ if (!confirming || confirming.kind !== "all") return null;
1299
+ return React.createElement(ConfirmRow, {
1300
+ text: "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
1301
+ onConfirm: () => {
1302
+ setConfirming(null);
1303
+ run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
1304
+ },
1305
+ onCancel: () => setConfirming(null)
1306
+ });
1307
+ }
1308
+ function toggle(key) {
1309
+ setExpanded((prev) => {
1310
+ const next = new Set(prev);
1311
+ if (next.has(key)) next.delete(key);
1312
+ else next.add(key);
1313
+ return next;
1314
+ });
1315
+ }
1316
+ const q = query.trim().toLowerCase();
1317
+ const filteredItems = q ? (items || []).filter(
1318
+ (it) => (it.workspace || "").toLowerCase().indexOf(q) >= 0 || (it.sessionTitle || "").toLowerCase().indexOf(q) >= 0 || (it.messageText || "").toLowerCase().indexOf(q) >= 0 || String(it.id || "").toLowerCase().indexOf(q) >= 0
1319
+ ) : items;
1320
+ const tree = buildTree2(filteredItems);
1321
+ const allSessionIds = Array.from(new Set((items || []).map((it) => it.sessionId).filter(Boolean)));
1322
+ const versionMap = groupByLineage(allSessionIds, lineage);
1323
+ let listById = null;
1324
+ try {
1325
+ if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.getSnapshot === "function") {
1326
+ const snapshot = sessionsSvc.list.getSnapshot();
1327
+ listById = snapshot && snapshot.byId || null;
1328
+ }
1329
+ } catch (e) {
1330
+ listById = null;
1331
+ }
1332
+ function confirmDelete(kind, key, extra, text) {
1333
+ setConfirming({ kind, key, extra, text });
1334
+ }
1335
+ function renderConfirm(kind, key, extra, text) {
1336
+ if (!confirming || confirming.kind !== kind || confirming.key !== key) return null;
1337
+ return React.createElement(ConfirmRow, {
1338
+ text,
1339
+ onConfirm: () => {
1340
+ const c = confirming;
1341
+ setConfirming(null);
1342
+ run("delete", c.extra, "\u5DF2\u5220\u9664");
1343
+ },
1344
+ onCancel: () => setConfirming(null)
1345
+ });
1346
+ }
1347
+ function renderLeaf(it) {
1348
+ const key = "snap-" + it.id;
1349
+ const text = it.messageText;
1350
+ const title = text || it.id;
1351
+ const label = text ? clockText2(it.time) + " " + text : clockText2(it.time) + " " + it.id.slice(0, 12) + "\u2026";
1352
+ return React.createElement(
1353
+ "div",
1354
+ { className: "dsh-recall-tree-node", key },
1355
+ React.createElement(
1356
+ "div",
1357
+ { className: "dsh-recall-tree-row", title },
1358
+ React.createElement("span", { className: "dsh-recall-tree-toggle-placeholder" }),
1359
+ React.createElement(
1360
+ "span",
1361
+ { className: "dsh-recall-tree-label" },
1362
+ React.createElement("span", { className: "dsh-recall-tree-title" }, label)
1363
+ ),
1364
+ React.createElement("button", {
1365
+ type: "button",
1366
+ className: "dsh-recall-ex-chip",
1367
+ title: "\u5220\u9664\u8BE5\u5FEB\u7167\uFF08tag \u4E0E\u7D22\u5F15\u6761\u76EE\uFF09",
1368
+ onClick: () => confirmDelete("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
1369
+ }, "\u5220\u9664")
1370
+ ),
1371
+ renderConfirm("snapshot", key, { messageId: it.id, root: it.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
1372
+ );
1373
+ }
1374
+ function renderSession(s) {
1375
+ const key = "session-" + (s.root || "") + "-" + s.sessionId;
1376
+ const open = expanded.has(key);
1377
+ const label = s.title || (titlesPending && s.sessionId ? "\u2026" : "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09");
1378
+ const version = s.sessionId ? versionMap.get(String(s.sessionId)) : null;
1379
+ const switchable = Boolean(s.sessionId && listById && listById[s.sessionId]);
1380
+ return React.createElement(
1381
+ "div",
1382
+ { className: "dsh-recall-tree-node", key },
1383
+ React.createElement(
1384
+ "div",
1385
+ { className: "dsh-recall-tree-row" },
1386
+ // V2:折叠钮 span→button——Tab/Enter/Space 可达,读屏经 aria-expanded
1387
+ // 与 aria-label 播报展开语义与节点名;CSS 已做 button 重置防视觉回归。
1388
+ React.createElement("button", {
1389
+ type: "button",
1390
+ className: "dsh-recall-tree-toggle",
1391
+ "aria-expanded": open,
1392
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + label,
1393
+ onClick: () => toggle(key)
1394
+ }, open ? "\u25BE" : "\u25B8"),
1395
+ React.createElement(
1396
+ "span",
1397
+ { className: "dsh-recall-tree-label", title: s.sessionId || "" },
1398
+ React.createElement("span", { className: "dsh-recall-tree-title" }, label),
1399
+ version ? React.createElement("span", { className: "dsh-recall-tree-meta", title: "\u7248\u672C\u5BB6\u65CF\uFF1A" + version.family.join(" \u2192 ") }, "v" + version.index + "/" + version.family.length) : null,
1400
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, s.items.length + " \u6761")
1401
+ ),
1402
+ switchable ? React.createElement("button", {
1403
+ type: "button",
1404
+ className: "dsh-recall-ex-chip",
1405
+ title: "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
1406
+ onClick: () => {
1407
+ try {
1408
+ sessionsSvc.open(s.sessionId);
1409
+ } catch (e) {
1410
+ }
1411
+ }
1412
+ }, "\u5207\u6362") : null,
1413
+ s.sessionId ? React.createElement("button", {
1414
+ type: "button",
1415
+ className: "dsh-recall-ex-chip",
1416
+ title: "\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167",
1417
+ onClick: () => confirmDelete("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
1418
+ }, "\u5220\u9664") : null
1419
+ ),
1420
+ open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...s.items.map(renderLeaf)) : null,
1421
+ s.sessionId ? renderConfirm("session", key, { scope: "session", sessionId: s.sessionId, root: s.root || null }, "\u786E\u8BA4\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
1422
+ );
1423
+ }
1424
+ function renderWorkspace(ws) {
1425
+ const key = "ws-" + ws.root;
1426
+ const open = expanded.has(key);
1427
+ const sessionCount = ws.sessions.length;
1428
+ const snapCount = ws.sessions.reduce((n, s) => n + s.items.length, 0);
1429
+ return React.createElement(
1430
+ "div",
1431
+ { className: "dsh-recall-tree-node", key },
1432
+ React.createElement(
1433
+ "div",
1434
+ { className: "dsh-recall-tree-row" },
1435
+ // V2:工作区折叠钮同 renderSession——span→button 键盘化,aria 语义并列播报
1436
+ React.createElement("button", {
1437
+ type: "button",
1438
+ className: "dsh-recall-tree-toggle",
1439
+ "aria-expanded": open,
1440
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + ws.name,
1441
+ onClick: () => toggle(key)
1442
+ }, open ? "\u25BE" : "\u25B8"),
1443
+ React.createElement(
1444
+ "span",
1445
+ { className: "dsh-recall-tree-label", title: ws.root || "" },
1446
+ React.createElement("span", { className: "dsh-recall-tree-name" }, ws.name),
1447
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, sessionCount + " \u4F1A\u8BDD / " + snapCount + " \u5FEB\u7167")
1448
+ ),
1449
+ ws.root ? React.createElement("button", {
1450
+ type: "button",
1451
+ className: "dsh-recall-ex-chip",
1452
+ title: "\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167",
1453
+ onClick: () => confirmDelete("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002")
1454
+ }, "\u5220\u9664") : null
1455
+ ),
1456
+ open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...ws.sessions.map(renderSession)) : null,
1457
+ ws.root ? renderConfirm("workspace", key, { scope: "workspace", root: ws.root }, "\u786E\u8BA4\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002") : null
1458
+ );
1459
+ }
1460
+ const treeNodes = tree.map(renderWorkspace);
1461
+ const loaded = items ? items.length : null;
1462
+ const countText = loaded === null ? "\u5171 \u2026 \u6761\u5FEB\u7167" : "\u5171 " + total + " \u6761\u5FEB\u7167" + (limit < total ? "\uFF08\u5F53\u524D\u663E\u793A\u6700\u65B0 " + loaded + " \u6761\uFF09" : "");
1463
+ function loadMore() {
1464
+ const next = Math.min(Math.max(total, limit), 2e3);
1465
+ if (next <= limit) return;
1466
+ setLimit(next);
1467
+ refresh(next);
1468
+ }
1469
+ return React.createElement(
1470
+ "div",
1471
+ { className: "dsh-recall-ex-card" },
1472
+ // 不再渲染「快照管理」内标题:折叠头(SectionToggle)已承担分区标题
1473
+ // 角色,展开后再出现同名大标题是纯重复(实测观感噪音);计数 note 紧跟
1474
+ // 折叠头,信息层级 = 折叠头(标题)→ 计数/健康(摘要)→ 搜索 → 树。
1475
+ React.createElement(
1476
+ "div",
1477
+ { className: "dsh-recall-ex-note" },
1478
+ usage === null ? countText + "\u3002" : countText + "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 " + sizeText2(usage) + "\u3002"
1479
+ ),
1480
+ // V6 健康行徽章化:git 状态用彩色 pill(成功/失败,官方状态行配对),
1481
+ // 从普通 note 提升为卡片顶部横幅(渲染在搜索框与树之前);存储计数
1482
+ // 维持文字,避免 pill 堆叠丢失信息。
1483
+ health ? React.createElement(
1484
+ "div",
1485
+ { className: "dsh-recall-ex-note", key: "health" },
1486
+ React.createElement("span", {
1487
+ className: "dsh-recall-health-pill " + (health.gitAvailable ? "dsh-recall-health-pill-ok" : "dsh-recall-health-pill-bad"),
1488
+ title: "\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git"
1489
+ }, health.gitAvailable ? "git \u53EF\u7528" : "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09"),
1490
+ " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome " + health.homeStores + " \u4E2A\u5DE5\u4F5C\u533A" + (health.fallbackStores ? "\uFF0C\u964D\u7EA7 " + health.fallbackStores + " \u4E2A" : "")
1491
+ ) : null,
1492
+ React.createElement("input", {
1493
+ className: "dsh-recall-ex-input",
1494
+ placeholder: "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
1495
+ "aria-label": "\u641C\u7D22\u5FEB\u7167",
1496
+ value: query,
1497
+ spellCheck: false,
1498
+ onChange: (e) => setQuery(e.target.value)
1499
+ }),
1500
+ // V3 加载骨架:items===null 表示首查未回——用 5 条 pulse 灰条占位替代
1501
+ // 打开快照管理时的一段空白;aria-hidden 纯装饰不打扰读屏
1502
+ items === null ? React.createElement(
1503
+ "div",
1504
+ { className: "dsh-recall-tree-skeleton", "aria-hidden": true },
1505
+ ...[1, 2, 3, 4, 5].map((n) => React.createElement("div", { key: "sk-" + n, className: "dsh-recall-tree-skeleton-row" }))
1506
+ ) : null,
1507
+ treeNodes.length > 0 ? React.createElement("div", { className: "dsh-recall-tree" }, ...treeNodes) : null,
1508
+ items && items.length === 0 && !q ? React.createElement("div", { className: "dsh-recall-ex-note", key: "empty" }, "\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u5FEB\u7167\u3002") : null,
1509
+ q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-ex-note", key: "no-match" }, "\u65E0\u5339\u914D\u5FEB\u7167") : null,
1510
+ renderDeleteAllConfirm(),
1511
+ // V6:错误区从卡片最底上移到操作区上方(fail-loud 可见性);标题
1512
+ // error 色带条数徽章,时间戳格式维持原样
1513
+ errors && errors.length > 0 ? React.createElement(
1514
+ "div",
1515
+ { key: "errors" },
1516
+ React.createElement("div", { className: "dsh-recall-errors-title" }, "\u6700\u8FD1\u9519\u8BEF (" + errors.length + ")"),
1517
+ (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message)),
1518
+ React.createElement(
1519
+ "div",
1520
+ { className: "dsh-recall-panel-actions" },
1521
+ errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u90E8 (" + errors.length + ")") : null,
1522
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
1523
+ )
1524
+ ) : null,
1525
+ React.createElement(
1526
+ "div",
1527
+ { className: "dsh-recall-panel-actions" },
1528
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
1529
+ limit < total ? React.createElement("button", {
1530
+ type: "button",
1531
+ className: "dsh-recall-btn",
1532
+ disabled: state.busy,
1533
+ onClick: loadMore
1534
+ }, "\u52A0\u8F7D\u66F4\u591A") : null,
1535
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: refresh }, "\u5237\u65B0"),
1536
+ React.createElement("button", {
1537
+ type: "button",
1538
+ className: "dsh-recall-btn",
1539
+ disabled: state.busy,
1540
+ title: "\u7ACB\u5373\u5BF9\u5168\u90E8\u5DE5\u4F5C\u533A\u6267\u884C\u4E00\u6B21 git gc\uFF08\u538B\u7F29\u5BF9\u8C61\u5E93\u91CA\u653E\u7A7A\u95F4\uFF09",
1541
+ onClick: () => run("gc", {}, "gc \u5B8C\u6210")
1542
+ }, "\u7ACB\u5373 gc"),
1543
+ // V5:全部删除固定为操作区最后一个按钮(排在立即 gc 之后)——即使
1544
+ // 「加载更多」出现/消失也不漂移;danger 与普通按钮间由 panel-actions
1545
+ // 统一 gap:8px 形成视觉分组
1546
+ React.createElement("button", {
1547
+ type: "button",
1548
+ className: "dsh-recall-btn dsh-recall-btn-danger",
1549
+ disabled: state.busy,
1550
+ title: "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
1551
+ onClick: () => setConfirming({ kind: "all" })
1552
+ }, "\u5168\u90E8\u5220\u9664")
1553
+ )
1554
+ );
1555
+ }
1556
+ return { ManageCard };
1557
+ }
1558
+
1559
+ // src/client/settings-cards.ts
1560
+ function buildSettingsCards(React, util, sessionsSvc) {
1561
+ function SectionToggle(props) {
1562
+ return React.createElement(
1563
+ "button",
1564
+ {
1565
+ type: "button",
1566
+ className: "dsh-recall-cardbtn",
1567
+ "aria-expanded": props.open,
1568
+ onClick: props.onToggle
1569
+ },
1570
+ // 箭头恒为 ▸、开合靠旋转(CSS transition .16s)——与官方卡片 chevron 同一
1571
+ // 动效语言;读屏状态仍由 aria-expanded 播报,字形不再承担状态语义
1572
+ React.createElement("span", { className: "dsh-recall-tree-toggle dsh-recall-section-chevron" + (props.open ? " dsh-recall-section-chevron-open" : "") }, "\u25B8"),
1573
+ React.createElement("span", { className: "dsh-recall-ex-title" }, props.title),
1574
+ props.meta ? React.createElement("span", { className: "dsh-recall-tree-meta" }, props.meta) : null
1575
+ );
1576
+ }
1577
+ const { ConfigForm } = buildConfigForm(React, util, SectionToggle);
1578
+ const { ExcludeFilesSection } = buildExcludeCards(React, util);
1579
+ const { ManageCard } = buildSnapshotManager(React, util, sessionsSvc);
1580
+ function RecallSettingsCard() {
1581
+ const [open, setOpen] = React.useState(false);
1582
+ const [sections, setSections] = React.useState({ exclude: false, manage: false });
1583
+ function toggle(key) {
1584
+ setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
1585
+ }
1586
+ return React.createElement(
1587
+ "li",
1588
+ { className: "dsh-recall-card" + (open ? " dsh-recall-card-open" : "") },
1589
+ React.createElement(
1590
+ "button",
1591
+ {
1592
+ type: "button",
1593
+ className: "dsh-recall-cardbtn",
1594
+ "aria-expanded": open,
1595
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + ": \u64A4\u56DE\u63D2\u4EF6",
1596
+ onClick: () => setOpen((v) => !v)
1597
+ },
1598
+ React.createElement(
1599
+ "span",
1600
+ { className: "dsh-recall-card-head" },
1601
+ React.createElement("span", { className: "dsh-recall-card-name" }, "\u64A4\u56DE\u63D2\u4EF6"),
1602
+ React.createElement("span", { className: "dsh-recall-card-desc" }, "\u6D88\u606F\u64A4\u56DE\uFF08\u6587\u4EF6\u5FEB\u7167 + \u5BF9\u8BDD\u56DE\u9000\uFF09\u7684\u9608\u503C\u4E0E\u6CBB\u7406")
1603
+ ),
1604
+ React.createElement("svg", {
1605
+ width: 14,
1606
+ height: 14,
1607
+ viewBox: "0 0 16 16",
1608
+ style: { color: "var(--dsw-alias-label-tertiary)", flex: "none", transition: "transform .16s", transform: open ? "rotate(180deg)" : "none" }
1609
+ }, React.createElement("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }))
1610
+ ),
1611
+ open ? React.createElement(
1612
+ "div",
1613
+ { className: "dsh-recall-card-body" },
1614
+ React.createElement(ConfigForm),
1615
+ React.createElement(SectionToggle, { title: "\u6392\u9664\u914D\u7F6E\uFF08exclude.txt\uFF09", open: sections.exclude, onToggle: () => toggle("exclude") }),
1616
+ sections.exclude ? React.createElement(ExcludeFilesSection) : null,
1617
+ React.createElement(SectionToggle, { title: "\u5FEB\u7167\u7BA1\u7406", open: sections.manage, onToggle: () => toggle("manage") }),
1618
+ sections.manage ? React.createElement(ManageCard) : null
1619
+ ) : null
1620
+ );
1621
+ }
1622
+ return { RecallSettingsCard };
1623
+ }
1624
+
1625
+ // src/client/app.ts
1626
+ function nextShadowPriority(entries, key) {
1627
+ let priority = -1;
1628
+ for (const entry of Array.isArray(entries) ? entries : []) {
1629
+ if (!entry || !entry.options || entry.options.key !== key) continue;
1630
+ const p = entry.options.priority;
1631
+ const occupied = typeof p === "number" && Number.isFinite(p) ? p : 0;
1632
+ if (occupied <= priority) priority = occupied - 1;
1633
+ }
1634
+ return priority;
1635
+ }
1636
+ function createApp(React) {
1637
+ return function apply(ctx) {
1638
+ const slots = ctx.slots;
1639
+ if (!slots) return;
1640
+ const sessionsSvc = ctx.sessions;
1641
+ const workspacesSvc = ctx.workspaces;
1642
+ const stylesSvc = ctx.get("styles");
1643
+ if (stylesSvc && typeof stylesSvc.insert === "function") {
1644
+ stylesSvc.insert(CSS);
1645
+ } else if (typeof document !== "undefined") {
1646
+ const tag = document.createElement("style");
1647
+ tag.setAttribute("data-plugin", "dsh-recall-plugin");
1648
+ tag.textContent = CSS;
1649
+ document.head.appendChild(tag);
1650
+ }
1651
+ const util = buildUtil();
1652
+ const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc);
1653
+ const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc);
1654
+ for (const slotKey of ["user", "steering"]) {
1655
+ try {
1656
+ slots.inject("conversation.chat.node", () => {
1657
+ const priority = nextShadowPriority(slots.entries("conversation.chat.node"), slotKey);
1658
+ return slots.register(
1659
+ { name: "conversation.chat.node", key: slotKey, priority },
1660
+ UserRecallNode
1661
+ );
1662
+ });
1663
+ } catch (error) {
1664
+ console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
1665
+ }
1666
+ }
1667
+ try {
1668
+ slots.inject("settings.plugin.item", () => slots.register(
1669
+ { name: "settings.plugin.item", key: "dsh-recall" },
1670
+ RecallSettingsCard
1671
+ ));
1672
+ } catch (error) {
1673
+ console.error("[dsh-recall-plugin] settings card register failed:", error);
1674
+ }
1675
+ };
1676
+ }
1677
+
1678
+ // src/client/entry.ts
1679
+ window.__ModuleLoader__.load({
1680
+ id: "dsh-recall-plugin",
1681
+ factory: (require2) => {
1682
+ const React = require2("react");
1683
+ return {
1684
+ name: "dsh-recall-plugin",
1685
+ inject: ["slots", "sessions", "workspaces", "timer"],
1686
+ apply: createApp(React)
1687
+ };
1688
+ }
1689
+ });