dsh-recall-plugin 2.3.2 → 2.3.4

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