dsh-recall-plugin 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,1479 +1,1459 @@
1
- /**
2
- * dsh-recall-plugin — Client 半(持久插件形态,浏览器 bundle)
3
- *
4
- * 职责:注册 conversation.chat.node 的 user 渲染器,重绘用户消息气泡
5
- * (文本/图片/JSON 块;图片经官方 props.renderMessageImages 管线渲染,
6
- * UserRecallNode 内注释),在复制按钮旁加「撤回」按钮;二次确认面板展示
7
- * 文件变更清单,确认后经 /api/recall/* 调用 Host 回退文件,并用官方
8
- * sessions.fork 把对话一并回退(新会话打开、原会话归档)。
9
- *
10
- * 这是 dsh.client bundle(exports["./client"]),由 client-modules 打包成
11
- * /plugins/<pkg>/client.js 注入页面;factory require("react") 由平台
12
- * 模块表提供。当前为纯 JS + React.createElement 直写。
13
- */
14
- window.__ModuleLoader__.load({
15
- id: "dsh-recall-plugin",
16
- factory: (require) => {
17
- const React = require("react")
18
-
19
- return {
20
- name: "dsh-recall-plugin",
21
- apply(ctx) {
22
- const slots = ctx.get('slots')
23
- if (!slots) return
24
- // 官方会话服务:fork 到已完成 turn 前缀 + open 切到新会话;
25
- // workspaces 的归档只是从列表隐藏、可恢复,用来收走回退前的原会话。
26
- const sessionsSvc = ctx.get('sessions')
27
- const workspacesSvc = ctx.get('workspaces')
28
-
29
- // Host HTTP API(动态插件的 harness RPC 在此换成 fetch 调用)
30
- function api(name, args) {
31
- return fetch('/api/recall/' + name, {
32
- method: 'POST',
33
- headers: { 'content-type': 'application/json' },
34
- body: JSON.stringify(args || {})
35
- }).then((r) => r.json())
36
- }
37
-
38
- const css = [
39
- '.dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}',
40
- '.dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}',
41
- '.dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}',
42
- '.dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}',
43
- '.dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}',
44
- '.dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}',
45
- '.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}',
46
- '.dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',
47
- '@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}}',
48
- '.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)}',
49
- '.dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}',
50
- '.dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}',
51
- '.dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}',
52
- '.dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}',
53
- '.dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}',
54
- '.dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}',
55
- '.dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
56
- '.dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}',
57
- '.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)}',
58
- '.dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}',
59
- '.dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
60
- '.dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}',
61
- '.dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}',
62
- '.dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}',
63
- '.dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}',
64
- '.dsh-recall-toast.dsh-recall-toast-in{opacity:1}',
65
- '.dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}',
66
- '.dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}',
67
- '.dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}',
68
- '.dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}',
69
- '.dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:20px;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}',
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;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:20px}',
72
- '.dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:18px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
73
- '.dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}',
74
- '.dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
75
- '.dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}',
76
- '.dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}',
77
- '.dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}',
78
- '.dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}',
79
- '.dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}',
80
- '.dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
81
- '.dsh-recall-tree-toggle{flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:11px;line-height:18px;user-select:none}',
82
- '.dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
83
- '.dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}',
84
- '.dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px;overflow:hidden}',
85
- '.dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}',
86
- '.dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
87
- '.dsh-recall-tree-meta{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}',
88
- '.dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:16px;border-left:1px solid var(--dsw-alias-border-l1);padding-left:8px}',
89
- '.dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}',
90
- '.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}',
91
- '.dsh-recall-card.dsh-recall-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
92
- '.dsh-recall-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}',
93
- '.dsh-recall-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
94
- '.dsh-recall-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}',
95
- '.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}',
96
- '.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}',
97
- '.dsh-recall-cfg-row{display:flex;flex-direction:column;gap:4px}',
98
- '.dsh-recall-cfg-line{display:flex;align-items:center;gap:8px}',
99
- '.dsh-recall-cfg-label{flex:none;width:130px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}',
100
- '.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}',
101
- '.dsh-recall-cfg-input:disabled{opacity:.5}',
102
- '.dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;min-height:64px}',
103
- '.dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding-left:138px}',
104
- '.dsh-recall-cfg-tag{flex:none;font-size:11px;line-height:16px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}'
105
- ].join('')
106
- // 静态 bundle 的 ctx 可能不提供 styles 服务,降级为直接注入 <style>
107
- const stylesSvc = ctx.get('styles')
108
- if (stylesSvc && typeof stylesSvc.insert === 'function') {
109
- stylesSvc.insert(css)
110
- } else if (typeof document !== 'undefined') {
111
- const tag = document.createElement('style')
112
- tag.setAttribute('data-plugin', 'dsh-recall-plugin')
113
- tag.textContent = css
114
- document.head.appendChild(tag)
115
- }
116
-
117
- // 每会话独立的 init 缓存:单槽缓存(initedSessionId + initDone)
118
- // 在 A→B→A 切换后会让 A 复用 B 的 init promise——Host 侧虽有
119
- // indexLoaded 兜底不至于出错,但 A 的 notice/降级提示会随 B 的
120
- // 结果走,语义不对;Map 形态才与「每会话预热一次」的意图一致。
121
- const initMap = new Map()
122
-
123
- // 降级提示:每个种类每次页面加载只弹一次(Set 去重),避免切会话时
124
- // 反复打扰。纯 DOM 直插(与剪贴板同样的零依赖思路),7 秒后自动淡出。
125
- const noticeShown = new Set()
126
- // toast 挂载本体:两类提示(降级/快照反馈)共用的纯 DOM 实现
127
- function mountToast(text) {
128
- if (typeof document === 'undefined') return
129
- try {
130
- const el = document.createElement('div')
131
- el.className = 'dsh-recall-toast'
132
- const tag = document.createElement('span')
133
- tag.className = 'dsh-recall-toast-tag'
134
- tag.textContent = '撤回插件'
135
- const body = document.createElement('span')
136
- body.textContent = text
137
- el.appendChild(tag)
138
- el.appendChild(body)
139
- el.addEventListener('click', () => dismiss(), { once: true })
140
- document.body.appendChild(el)
141
- requestAnimationFrame(() => el.classList.add('dsh-recall-toast-in'))
142
- const timer = setTimeout(dismiss, 7000)
143
- let dismissed = false
144
- function dismiss() {
145
- if (dismissed) return
146
- dismissed = true
147
- clearTimeout(timer)
148
- el.classList.remove('dsh-recall-toast-in')
149
- setTimeout(() => el.remove(), 300)
150
- }
151
- } catch (e) { /* 提示失败不影响主流程 */ }
152
- }
153
- function showNotice(kind, text) {
154
- if (noticeShown.has(kind)) return
155
- noticeShown.add(kind)
156
- mountToast(text)
157
- }
158
- // 快照失败/跳过提示(issue #7 失败可见性):与降级提示不同,这类
159
- // 事件会在持续故障期间随每条消息反复发生——按「文本前缀 + 时间窗」
160
- // 节流,同一故障 10 分钟内至多打扰一次;不同错误(文本不同)各自
161
- // 独立计数。Map 规模封顶后整体清空:提示是尽力而为的可见性,不是
162
- // 需要精确保留的状态。
163
- const toastLastShown = new Map()
164
- function showThrottledToast(text) {
165
- const key = String(text).slice(0, 80)
166
- const now = Date.now()
167
- if (now - (toastLastShown.get(key) || 0) < 10 * 60 * 1000) return
168
- if (toastLastShown.size > 50) toastLastShown.clear()
169
- toastLastShown.set(key, now)
170
- mountToast(text)
171
- }
172
-
173
- // 每个会话只向 Host 注册一次(预热其根目录解析缓存)。
174
- // 返回 init promise:Host 端 init 要跑数条 PowerShell(建仓/loadIndex),
175
- // snapshot-info 必须等它完成后再查,否则冷启动时索引尚未载入会误判
176
- // has:false 且不再重试,撤回按钮将永不出现。
177
- // init 顺带下发插件行为开关(refillDraft 等),存进 pluginConfig
178
- // 供撤回执行链读取——设置页改配置 + 重启后随下一次 init 刷新。
179
- const pluginConfig = { refillDraft: true, archiveOriginal: true }
180
- function ensureInit(sessionId) {
181
- if (!sessionId) return Promise.resolve()
182
- const cached = initMap.get(sessionId)
183
- if (cached) return cached
184
- const done = api('init', { sessionId }).then((res) => {
185
- if (res && res.config && typeof res.config === 'object') {
186
- if (typeof res.config.refillDraft === 'boolean') pluginConfig.refillDraft = res.config.refillDraft
187
- if (typeof res.config.archiveOriginal === 'boolean') pluginConfig.archiveOriginal = res.config.archiveOriginal
188
- }
189
- const notice = res && res.notice
190
- if (notice && notice.unsupported) {
191
- showNotice('unsupported', '撤回插件仅支持 Windows / Linux / macOS,当前平台的快照不可用。')
192
- }
193
- if (notice && notice.gitMissing) {
194
- showNotice('git', '未检测到 git CLI,撤回功能不可用(快照引擎依赖 git)。安装 git 并重启 DSH 后即可使用。')
195
- }
196
- if (notice && notice.homeFallback) {
197
- showNotice('home', 'home 目录不可写,快照已降级存储到项目内 .dsh-recall-snapshots 目录。')
198
- }
199
- }).catch(() => {
200
- // init 失败(如页面先于 Host API 就绪加载)时清掉标记:否则本会话
201
- // 内被判定“已初始化”,撤回按钮永不出现;清掉后下一条消息挂载会重试
202
- initMap.delete(sessionId)
203
- })
204
- initMap.set(sessionId, done)
205
- return done
206
- }
207
-
208
- // 消息时间:当天只显示时分,跨天显示月/日 时分
209
- function clockText(ms) {
210
- try {
211
- // time 字段缺失或非法时返回空串:Invalid Date 不会 throw,
212
- // 不拦会渲染出 "NaN/NaN NaN:NaN" 这样的坏时间戳
213
- if (!ms || isNaN(new Date(ms).getTime())) return ''
214
- const d = new Date(ms)
215
- const now = new Date()
216
- const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
217
- const hh = String(d.getHours()).padStart(2, '0')
218
- const mm = String(d.getMinutes()).padStart(2, '0')
219
- return sameDay ? hh + ':' + mm : (d.getMonth() + 1) + '/' + d.getDate() + ' ' + hh + ':' + mm
220
- } catch (e) {
221
- return ''
222
- }
223
- }
224
-
225
- // 复制按钮走浏览器剪贴板;无 primitives 依赖,直接调用并带降级
226
- function writeClipboard(text) {
227
- try {
228
- if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
229
- return navigator.clipboard.writeText(text).then(() => true, () => false)
230
- }
231
- } catch (e) { /* fall through */ }
232
- try {
233
- if (typeof document !== 'undefined' && typeof document.execCommand === 'function') {
234
- const el = document.createElement('textarea')
235
- el.value = text
236
- el.setAttribute('readonly', '')
237
- el.style.position = 'fixed'
238
- el.style.left = '-9999px'
239
- document.body.appendChild(el)
240
- el.select()
241
- try {
242
- return Promise.resolve(document.execCommand('copy'))
243
- } finally {
244
- el.remove()
245
- }
246
- }
247
- } catch (e) { /* ignore */ }
248
- return Promise.resolve(false)
249
- }
250
-
251
- function CopyIcon() {
252
- return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': true },
253
- React.createElement('rect', { x: 5.5, y: 5.5, width: 8, height: 8, rx: 1.5 }),
254
- React.createElement('path', { d: 'M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5' })
255
- )
256
- }
257
-
258
- function CheckIcon() {
259
- return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
260
- React.createElement('path', { d: 'm3 8.5 3.2 3.2L13 5' })
261
- )
262
- }
263
-
264
- function UndoIcon() {
265
- return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
266
- React.createElement('path', { d: 'M7.5 3.5 3.5 7.5l4 4' }),
267
- React.createElement('path', { d: 'M4.5 7.5h5a3 3 0 0 1 0 6H8' })
268
- )
269
- }
270
-
271
- // kind 语义单表承载(文案/徽章类名/汇总顺序):新增 kind 时只改
272
- // 这一处,避免出现「映射表改了、CSS 类名忘了」的半改状态
273
- const KIND_INFO = {
274
- modified: { label: '修改', cls: 'modified' },
275
- restored: { label: '恢复', cls: 'restored' },
276
- added: { label: '删除', cls: 'added' }
277
- }
278
-
279
- function summaryText(counts) {
280
- const parts = []
281
- for (const kind of Object.keys(KIND_INFO)) {
282
- if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + ' ' + counts[kind])
283
- }
284
- return parts.join(' · ')
285
- }
286
-
287
- function recallPanel(recall, closePanel, executeRecall) {
288
- if (recall.stage === 'loading') {
289
- return React.createElement('div', { className: 'dsh-recall-panel' },
290
- React.createElement('div', { className: 'dsh-recall-panel-title' }, '正在计算变更…')
291
- )
292
- }
293
- if (recall.stage === 'error') {
294
- return React.createElement('div', { className: 'dsh-recall-panel' },
295
- React.createElement('div', { className: 'dsh-recall-panel-title' }, '无法回退'),
296
- React.createElement('div', { className: 'dsh-recall-panel-note' }, recall.message || ''),
297
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
298
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '关闭')
299
- )
300
- )
301
- }
302
- if (recall.stage === 'confirm') {
303
- const changes = recall.changes || []
304
- const total = typeof recall.total === 'number' ? recall.total : changes.length
305
- const counts = { modified: 0, restored: 0, added: 0 }
306
- for (const c of changes) {
307
- if (c && counts[c.kind] !== undefined) counts[c.kind]++
308
- }
309
- const rows = changes.map((c, i) => {
310
- const info = KIND_INFO[c.kind]
311
- return React.createElement('div', { className: 'dsh-recall-file', key: i },
312
- React.createElement('span', { className: 'dsh-recall-badge dsh-recall-badge-' + (info ? info.cls : '') }, info ? info.label : (c.kind || '')),
313
- React.createElement('span', { className: 'dsh-recall-rel' }, c.rel || '')
314
- )
315
- })
316
- if (recall.truncated) {
317
- rows.push(React.createElement('div', { className: 'dsh-recall-panel-note', key: 'truncated' }, '…仅显示前 ' + changes.length + ' 条,共 ' + total + ' 个文件将变更'))
318
- }
319
- // cutSeq null 表示该消息是会话第一条用户消息:文件可回退但对话无从回退
320
- const canRevertChat = typeof recall.cutSeq === 'number'
321
- return React.createElement('div', { className: 'dsh-recall-panel' },
322
- React.createElement('div', { className: 'dsh-recall-panel-title' }, '整段回退'),
323
- React.createElement('div', { className: 'dsh-recall-panel-note' },
324
- '将项目恢复到' + (recall.time ? ' ' + clockText(recall.time) + ' ' : ' ') + '发送该消息时的状态。共 ' + total + ' 个文件将变更' + (summaryText(counts) ? '(' + summaryText(counts) + ')' : '') + '。此操作会覆盖当前文件内容;回退前会自动保存一份当前状态的安全快照(不含在下方清单内)。'
325
- ),
326
- React.createElement('div', { className: 'dsh-recall-panel-note' },
327
- canRevertChat
328
- ? '对话将一并回退到该消息之前:该消息及之后的全部对话会从当前视图移除,原会话归档保存(可从归档找回)。'
329
- : '该消息是本会话中第一条用户消息,无法回退对话;确认后仅回退项目文件。'
330
- ),
331
- changes.length > 0 ? React.createElement('div', { className: 'dsh-recall-list' }, ...rows) : null,
332
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
333
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '取消'),
334
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', onClick: executeRecall }, '确认回退')
335
- )
336
- )
337
- }
338
- if (recall.stage === 'executing') {
339
- return React.createElement('div', { className: 'dsh-recall-panel' },
340
- React.createElement('div', { className: 'dsh-recall-panel-title' }, '正在回退…')
341
- )
342
- }
343
- if (recall.stage === 'done') {
344
- return React.createElement('div', { className: 'dsh-recall-panel' },
345
- React.createElement('div', { className: 'dsh-recall-panel-title' }, '回退完成'),
346
- React.createElement('div', { className: 'dsh-recall-panel-note' },
347
- recall.chatReverted
348
- ? '项目文件与对话已回退到该消息之前。新会话已打开,原会话已归档(可从归档找回)。'
349
- : '项目已恢复到发送该消息时的状态。' + (recall.chatError ? ' 对话回退失败:' + recall.chatError : '')
350
- ),
351
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
352
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '关闭')
353
- )
354
- )
355
- }
356
- return null
357
- }
358
-
359
- function UserRecallNode(props) {
360
- const node = props && props.node
361
- // 图片渲染入口:官方 ChatNodeSeat 传给本 slot 的 props 契约只有
362
- // renderMessageImages(内部经 conversation.message.images slot 渲染
363
- // 官方 MessageImages,自带鉴权、缓存、失败重试与灯箱预览)。
364
- // 契约里从不存在 loadImage(issue #9):v1.6/v1.7 读该不存在的
365
- // 字段导致自研加载链从未执行、用户消息图片永久无声空白。
366
- const renderMessageImages = props && props.renderMessageImages
367
- const sessionId = props && props.sessionId
368
- const data = node && node.data ? node.data : {}
369
- // node.id 是会话事件匹配时写入的真实消息 ID;node.key 是位置键(如 13:input),不能用于快照查询
370
- const messageId = node ? String(node.id || node.key || '') : ''
371
- const blocks = Array.isArray(data.content) ? data.content : []
372
- const text = blocks.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
373
- // 官方契约:images 传 image 块数组({attachment}),官方内部取
374
- // image.attachment.attachmentId —— 不是裸 attachment 对象
375
- const imageBlocks = blocks.filter((b) => b && b.type === 'image' && b.attachment).map((b) => ({ attachment: b.attachment }))
376
- const rest = blocks.filter((b) => !b || !(b.type === 'text' && typeof b.text === 'string') && !(b.type === 'image' && b.attachment))
377
-
378
- const [copied, setCopied] = React.useState(false)
379
- const [hasSnapshot, setHasSnapshot] = React.useState(false)
380
- const [recall, setRecall] = React.useState({ stage: 'idle' })
381
-
382
- React.useEffect(() => {
383
- let alive = true
384
- let timer = null
385
- let attempts = 0
386
- // 快照捕获(Host 侧脚本)是异步的:消息节点挂载时 snapshot-info
387
- // 可能先于捕获完成而返回 has:false。此前只查一次、失败不重试,
388
- // 撤回按钮只能靠手动刷新才出现。改为有界轮询:has:true 或达到
389
- // 上限即停(覆盖常规快照耗时,又避免无限请求),捕获完成后
390
- // 按钮自动出现。
391
- // 只对「近 5 分钟内」的消息轮询:快照只在消息发送当下捕获,
392
- // 老消息若没有快照就永远不会再有,对它们轮询只会白打请求。
393
- const RETRY_WINDOW_MS = 5 * 60 * 1000
394
- const MAX_ATTEMPTS = 20
395
- const RETRY_MS = 1000
396
- const msgTime = data && typeof data.time === 'number' ? data.time : NaN
397
- const recent = !isNaN(msgTime) && Date.now() - msgTime <= RETRY_WINDOW_MS
398
- function schedule() {
399
- if (!alive || !messageId) return
400
- attempts++
401
- api('snapshot-info', { messageId, sessionId }).then((res) => {
402
- if (!alive) return
403
- if (res && res.has) {
404
- // fail-open 跳过的路径:快照存在但个别目录没进去(撤回不
405
- // 恢复也不删它们)——仅对正在发生的消息提示,让用户知道
406
- // 快照少了什么(issue #7 失败可见性)
407
- if (recent && Array.isArray(res.skipped) && res.skipped.length) {
408
- const names = res.skipped.slice(0, 5).join('、') + (res.skipped.length > 5 ? ' 等 ' + res.skipped.length + ' 项' : '')
409
- showThrottledToast('快照已跳过未纳入的路径:' + names + '(撤回不会恢复或删除这些路径)')
410
- }
411
- setHasSnapshot(true)
412
- return
413
- }
414
- // 失败/熔断是终止态:快照不会迟到,提示后停止轮询——
415
- // 「按钮为什么消失了」的排障成本由这条提示消掉(issue #7)
416
- if (res && res.failed) {
417
- if (recent) showThrottledToast('快照失败:' + String(res.error || '未知原因').slice(0, 140))
418
- return
419
- }
420
- if (recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS)
421
- }).catch(() => {
422
- if (alive && recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS)
423
- })
424
- }
425
- // 先等 init 预热完成再查快照存在性:避免索引未载入时误判
426
- // has:false;init 本身失败也进入轮询(预热可能只是慢,不是坏)。
427
- ensureInit(sessionId).then(() => {
428
- if (!messageId || !alive) return
429
- schedule()
430
- }).catch(() => {
431
- if (alive && messageId) timer = setTimeout(schedule, RETRY_MS)
432
- })
433
- return () => {
434
- alive = false
435
- if (timer !== null) clearTimeout(timer)
436
- }
437
- }, [messageId, sessionId])
438
-
439
- const onCopy = () => {
440
- if (copied) return
441
- writeClipboard(text).then(() => {
442
- setCopied(true)
443
- const timer = ctx.get('timer')
444
- if (timer && typeof timer.timeout === 'function') {
445
- timer.timeout(() => setCopied(false), 1200)
446
- } else {
447
- setTimeout(() => setCopied(false), 1200)
448
- }
449
- })
450
- }
451
-
452
- const openPreview = () => {
453
- if (recall.stage === 'loading' || recall.stage === 'executing') return
454
- setRecall({ stage: 'loading' })
455
- api('preview', { messageId, sessionId }).then((res) => {
456
- if (!res || !res.ok) {
457
- setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '无法获取快照' })
458
- return
459
- }
460
- setRecall({
461
- stage: 'confirm',
462
- changes: res.changes || [],
463
- total: typeof res.total === 'number' ? res.total : (res.changes || []).length,
464
- truncated: Boolean(res.truncated),
465
- time: res.time || null,
466
- cutSeq: typeof res.cutSeq === 'number' ? res.cutSeq : null
467
- })
468
- }).catch((error) => {
469
- setRecall({ stage: 'error', message: String(error) })
470
- })
471
- }
472
-
473
- const executeRecall = () => {
474
- if (recall.stage !== 'confirm') return
475
- const changes = recall.changes || []
476
- const previewCut = typeof recall.cutSeq === 'number' ? recall.cutSeq : null
477
- // P0-3:携带预览摘要(total 是完整计数,与 Host diffFor
478
- // total 对齐;changes 截断到 500 条,不能用来比对)。Host
479
- // 据此在 execute 时校验「预览后文件集是否变化」,变了则返回
480
- // STALE 拒绝执行,防止把工作区改到用户没看过的新状态。
481
- const previewTotal = typeof recall.total === 'number' ? recall.total : changes.length
482
- setRecall({ stage: 'executing', changes })
483
- api('execute', { messageId, sessionId, previewTotal, previewAt: Date.now() }).then(async (res) => {
484
- if (!res || !res.ok) {
485
- // STALE:预览后文件变了——自动重新拉一次最新清单回到确认
486
- // 阶段让用户看新内容,而不是停在错误面板
487
- if (res && res.code === 'STALE') {
488
- setRecall({ stage: 'loading' })
489
- api('preview', { messageId, sessionId }).then((res2) => {
490
- if (!res2 || !res2.ok) {
491
- setRecall({ stage: 'error', message: (res2 && (res2.message || res2.error)) || '无法获取快照' })
492
- return
493
- }
494
- setRecall({
495
- stage: 'confirm',
496
- changes: res2.changes || [],
497
- total: typeof res2.total === 'number' ? res2.total : (res2.changes || []).length,
498
- truncated: Boolean(res2.truncated),
499
- time: res2.time || null,
500
- cutSeq: typeof res2.cutSeq === 'number' ? res2.cutSeq : null
501
- })
502
- }).catch((error) => {
503
- setRecall({ stage: 'error', message: String(error) })
504
- })
505
- return
506
- }
507
- setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '回退失败' })
508
- return
509
- }
510
- // 文件已回退;对话回退独立进行,失败只降级为“仅文件回退”而不是整体失败
511
- const cutSeq = typeof res.cutSeq === 'number' ? res.cutSeq : previewCut
512
- let chatReverted = false
513
- let chatError = ''
514
- // 回填目标会话:对话回退成功 → fork 出的新会话(视图已切过去);
515
- // 失败/无切点 → 当前会话
516
- let fillTarget = sessionId
517
- if (cutSeq !== null && sessionsSvc && typeof sessionsSvc.fork === 'function') {
518
- try {
519
- // 撤回语义是「回退」而非「复制」:新会话顶替原会话(原会话已
520
- // 归档),必须原样继承标题。increaseTitle 是官方侧栏「复制会话」
521
- // 用来区分新旧会话的,会把标题改成「xxx 2」且多次撤回时数字
522
- // 不断递增,与「同一会话回退」的用户观感相悖,故不传。
523
- const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq })
524
- if (childId) {
525
- if (typeof sessionsSvc.open === 'function') sessionsSvc.open(childId)
526
- chatReverted = true
527
- fillTarget = childId
528
- // 回退前的原会话归档(可关,见设置页 archiveOriginal):只是从列表
529
- // 隐藏、可恢复,避免侧栏出现两个近似会话;关闭时原会话
530
- // 保留供对照
531
- if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === 'function') {
532
- workspacesSvc.archiveSession(sessionId).catch(() => {})
533
- }
534
- } else {
535
- chatError = '未返回新会话'
536
- }
537
- } catch (error) {
538
- chatError = String(error)
539
- }
540
- }
541
- // 把被撤回的消息文本回填到输入框(可在设置页关闭):方便用户
542
- // 修改后重新发送。拿不到官方 conversation 服务时静默跳过,
543
- // 绝不阻塞撤回主流程。
544
- if (pluginConfig.refillDraft) fillDraft(fillTarget, text)
545
- setHasSnapshot(false)
546
- // 注:快照 tag Host 侧有意保留(幂等回退),刷新页面后
547
- // 该消息的撤回按钮会重新出现——这是「可再次回退到同一点」
548
- // 的特性而非 bug;此处 setHasSnapshot(false) 只是当前视图
549
- // 的即时反馈,不代表快照已删。
550
- setRecall({ stage: 'done', count: typeof res.count === 'number' ? res.count : changes.length, chatReverted, chatError })
551
- }).catch((error) => {
552
- setRecall({ stage: 'error', message: String(error) })
553
- })
554
- }
555
-
556
- const closePanel = () => setRecall({ stage: 'idle' })
557
-
558
- // 撤回后把被撤回消息的文本回填到输入框,方便用户修改后重新发送。
559
- // 官方 conversation 服务提供 input(InputHub)→ per-session shell
560
- // → actions.setDraft,走与输入框自身同一条官方写入通道(draft
561
- // 镜像同步,输入框即时刷新)。fork + open 之后 shell 可能需要
562
- // 一个 tick 才就绪(binding 异步解析),做有界重试:最多 8 次、
563
- // 间隔 150ms,覆盖常规竞态又不无限空转;拿不到服务时静默跳过。
564
- function fillDraft(targetSessionId, draftText) {
565
- if (!draftText || !targetSessionId) return
566
- let attempts = 0
567
- const attempt = () => {
568
- try {
569
- const conversation = ctx.get('conversation')
570
- if (conversation && conversation.input && typeof conversation.input.shell === 'function') {
571
- const shell = conversation.input.shell(targetSessionId)
572
- if (shell) {
573
- if (shell.actions && typeof shell.actions.setDraft === 'function') {
574
- shell.actions.setDraft(draftText)
575
- return
576
- }
577
- if (typeof shell.setDraft === 'function') {
578
- shell.setDraft(draftText)
579
- return
580
- }
581
- }
582
- }
583
- } catch (e) { /* fall through to retry */ }
584
- if (attempts++ < 8) setTimeout(attempt, 150)
585
- }
586
- attempt()
587
- }
588
-
589
- const bubbleChildren = []
590
- // 图片在上、气泡在下:布局顺序对齐官方 UserStyleBubble(历史会话
591
- // 里用户消息的观感与官方一致)。Fragment 包一层仅为挂 key。
592
- if (imageBlocks.length && typeof renderMessageImages === 'function') {
593
- bubbleChildren.push(React.createElement(React.Fragment, { key: 'images' },
594
- renderMessageImages({ images: imageBlocks, align: 'end' })
595
- ))
596
- }
597
- if (text !== '') bubbleChildren.push(React.createElement('div', { className: 'dsh-recall-bubble', key: 'text' }, text))
598
- for (let i = 0; i < rest.length; i++) {
599
- bubbleChildren.push(React.createElement('pre', { className: 'dsh-recall-json', key: 'rest-' + i }, JSON.stringify(rest[i], null, 2)))
600
- }
601
-
602
- const actions = []
603
- actions.push(React.createElement('span', { className: 'dsh-recall-time', key: 'time' }, clockText(data.time)))
604
- actions.push(React.createElement('button', {
605
- key: 'copy',
606
- type: 'button',
607
- className: 'dsh-recall-action',
608
- 'aria-label': copied ? '已复制' : '复制',
609
- title: copied ? '已复制' : '复制',
610
- onClick: onCopy
611
- }, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})))
612
- if (hasSnapshot) {
613
- actions.push(React.createElement('button', {
614
- key: 'recall',
615
- type: 'button',
616
- className: 'dsh-recall-action',
617
- 'aria-label': '撤回',
618
- title: '整段回退:文件与对话一并回到该消息之前',
619
- onClick: openPreview
620
- }, React.createElement(UndoIcon, {})))
621
- }
622
-
623
- return React.createElement('div', { className: 'dsh-recall-row', 'data-time-hover-root': true },
624
- bubbleChildren.length > 0 ? React.createElement('div', { className: 'dsh-recall-stack', key: 'stack' }, ...bubbleChildren) : null,
625
- React.createElement('div', { className: 'dsh-recall-actions', key: 'actions' }, ...actions),
626
- recallPanel(recall, closePanel, executeRecall)
627
- )
628
- }
629
-
630
- // ---- 设置页「撤回设置」标签:exclude.txt 可视化快速编辑 ----
631
-
632
- // 常用排除建议:一键追加的高频项,覆盖构建产物/日志/密钥三类最
633
- // 常见诉求;已存在的条目自动从候选里滤掉,避免重复点击堆叠。
634
- const EXCLUDE_SUGGESTIONS = ['dist/', 'build/', 'out/', 'coverage/', '*.log', '.env']
635
-
636
- // 单个 exclude 文件的编辑卡片。draft/baseline 分离实现「未保存
637
- // 修改」判定(textarea 所见即将保存的原文,不偷偷规范化);
638
- // key=file.path 挂载,父级重载列表时整卡重建、草稿随之丢弃。
639
- function ExcludeCard(props) {
640
- const file = props.file
641
- const [draft, setDraft] = React.useState(file.content || '')
642
- const [baseline, setBaseline] = React.useState(file.content || '')
643
- const [quick, setQuick] = React.useState('')
644
- const [state, setState] = React.useState({ busy: false, message: '', error: false })
645
- const dirty = draft !== baseline
646
-
647
- // 追加一条模式:先补齐行尾换行再拼接,保证每条模式独占一行
648
- // (exclude.txt 按行解析,两条挤一行会双双失效)
649
- function appendPattern(pattern) {
650
- setDraft((d) => (d && !d.endsWith('\n') ? d + '\n' : d) + pattern + '\n')
651
- }
652
-
653
- function addQuick() {
654
- const t = quick.trim()
655
- if (!t) return
656
- appendPattern(t)
657
- setQuick('')
658
- }
659
-
660
- function save() {
661
- if (state.busy || !dirty) return
662
- setState({ busy: true, message: '保存中…', error: false })
663
- api('exclude-set', { path: file.path, content: draft }).then((res) => {
664
- if (res && res.ok) {
665
- setBaseline(draft)
666
- setState({ busy: false, message: '已保存,下一次快照 / 预览 / 回退时生效', error: false })
667
- } else {
668
- setState({ busy: false, message: (res && (res.message || res.error)) || '保存失败', error: true })
669
- }
670
- }).catch((error) => {
671
- setState({ busy: false, message: String(error), error: true })
672
- })
673
- }
674
-
675
- function discard() {
676
- if (state.busy) return
677
- setDraft(baseline)
678
- setState({ busy: false, message: '', error: false })
679
- }
680
-
681
- const draftLines = draft.split('\n').map((l) => l.trim())
682
- const suggestions = EXCLUDE_SUGGESTIONS.filter((s) => draftLines.indexOf(s) < 0)
683
-
684
- return React.createElement('div', { className: 'dsh-recall-ex-card' },
685
- React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照排除项'),
686
- React.createElement('div', { className: 'dsh-recall-ex-note' },
687
- file.home
688
- ? '此配置全局共享,对所有工作区的快照生效(存储位置:' + file.path + ')。'
689
- : 'home 目录不可写时此工作区降级存储,排除配置独立生效(存储位置:' + file.path + ')。'
690
- ),
691
- React.createElement('div', { className: 'dsh-recall-ex-note' }, 'gitignore 语法,一行一条,支持 # 注释;命中排除的文件与目录不进入快照,也不会被回退触碰。'),
692
- React.createElement('textarea', {
693
- className: 'dsh-recall-ex-area',
694
- value: draft,
695
- spellCheck: false,
696
- onChange: (e) => setDraft(e.target.value)
697
- }),
698
- React.createElement('div', { className: 'dsh-recall-ex-quick' },
699
- React.createElement('input', {
700
- className: 'dsh-recall-ex-input',
701
- value: quick,
702
- placeholder: '输入路径或模式,回车快速添加',
703
- onChange: (e) => setQuick(e.target.value),
704
- onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); addQuick() } }
705
- }),
706
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: addQuick }, '添加'),
707
- ...suggestions.map((s) => React.createElement('button', {
708
- key: 'chip-' + s,
709
- type: 'button',
710
- className: 'dsh-recall-ex-chip',
711
- title: '点击追加 ' + s,
712
- onClick: () => appendPattern(s)
713
- }, s))
714
- ),
715
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
716
- state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
717
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: !dirty || state.busy, onClick: discard }, '放弃修改'),
718
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', disabled: !dirty || state.busy, onClick: save }, '保存')
719
- )
720
- )
721
- }
722
-
723
- // 快照管理卡片:列表(时间倒序)/ 磁盘占用 / 单条删除 / 手动 gc /
724
- // 最近错误。此前用户唯一的治理手段是等 gc 节流联动,无任何自助
725
- // 清理与排障入口;全部操作走 Host 的 manage/status 端点(串行队列
726
- // 在 Host 侧保证,卡片只管发请求)。
727
- function ManageCard(props) {
728
- const [items, setItems] = React.useState(null)
729
- const [usage, setUsage] = React.useState(null)
730
- const [errors, setErrors] = React.useState(null)
731
- const [state, setState] = React.useState({ busy: false, message: '', error: false })
732
- // 快照全量计数与当前拉取上限(S1-2):Host 按 limit 切片返回,
733
- // total 是全量——树默认折叠,全量加载到 2000 上限对 DOM 无压力,
734
- // 分页与树形组装互斥,故用「加载更多」而非翻页。
735
- const [limit, setLimit] = React.useState(200)
736
- const [total, setTotal] = React.useState(0)
737
- // 存储健康(S2-4):git 可用性 + home/降级分区计数,来自 usage
738
- // 端点的扩展响应;git 不可用/降级是消息流一次性 toast 之外常驻
739
- // 可见的排障信息。
740
- const [health, setHealth] = React.useState(null)
741
- // 树搜索关键字(S3-4):纯客户端过滤已加载条目,按工作区名/会话
742
- // 标题/消息文本/快照 ID 匹配;只过滤当前已加载数据,与 S1-2
743
- // limit 正交。
744
- const [query, setQuery] = React.useState('')
745
- // 最近错误展开态(S3-5):默认 5 条,可展开全部 20 条/收起
746
- const [showAllErrors, setShowAllErrors] = React.useState(false)
747
- // 冷会话标题在服务端要整日志解压(10 秒级),首屏不等它:列表
748
- // 先出(live/缓存标题),拿到后对缺标题的会话异步补拉再合并。
749
- const [titlesPending, setTitlesPending] = React.useState(false)
750
-
751
- function sizeText(bytes) {
752
- if (!bytes || bytes <= 0) return '0 MB'
753
- if (bytes < 1048576) return (bytes / 1024).toFixed(0) + ' KB'
754
- if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'
755
- return (bytes / 1073741824).toFixed(2) + ' GB'
756
- }
757
-
758
- function fetchTitles(list) {
759
- const missing = Array.from(new Set(
760
- (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
761
- )).slice(0, 100)
762
- if (!missing.length) { setTitlesPending(false); return }
763
- setTitlesPending(true)
764
- api('manage', { op: 'titles', sessionIds: missing }).then((res) => {
765
- const map = res && res.ok ? res.titles : null
766
- if (map) {
767
- setItems((prev) => (prev || []).map((it) => (
768
- it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it
769
- )))
770
- }
771
- setTitlesPending(false)
772
- }).catch(() => setTitlesPending(false))
773
- }
774
-
775
- // 消息文本补齐:只请求 live 拿不到文本的快照;同一会话多条消息
776
- // Host 端共享一次 readSession,避免为每条消息重复解压大日志。
777
- function fetchMessages(list) {
778
- const requests = (list || [])
779
- // 只补「还没拿到结论」的快照:live 没命中时不带 messageText 字段,
780
- // 冷查补齐后字段存在(字符串或 null),null 表示确实无文本不再请求。
781
- .filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, 'messageText'))
782
- .map((it) => ({ sessionId: it.sessionId, messageId: it.id }))
783
- .slice(0, 200)
784
- if (!requests.length) return
785
- api('manage', { op: 'messages', requests }).then((res) => {
786
- const map = res && res.ok ? res.messageTexts : null
787
- if (map) {
788
- setItems((prev) => (prev || []).map((it) => (
789
- it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it
790
- )))
791
- }
792
- }).catch(() => {})
793
- }
794
-
795
- function refresh(overLimit) {
796
- const useLimit = overLimit || limit
797
- api('manage', { op: 'list', limit: useLimit }).then((res) => {
798
- if (res && res.ok) {
799
- setItems(res.items || [])
800
- setTotal(typeof res.total === 'number' ? res.total : (res.items || []).length)
801
- fetchTitles(res.items || [])
802
- fetchMessages(res.items || [])
803
- }
804
- // 列表返回后再补 usage/status:首次冷启动时磁盘占用和错误日志
805
- // 都各要一条 shell,和 list 并发会抢资源拖慢首屏;延后到列表
806
- // 渲染后,用户先看到树形内容,占用再异步补上(usage 不带
807
- // sessionId,Host 汇总全部工作区)。
808
- api('manage', { op: 'usage' }).then((res) => {
809
- if (res && res.ok) {
810
- setUsage(res.bytes || 0)
811
- setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
812
- }
813
- }).catch(() => {})
814
- api('status', {}).then((res) => {
815
- if (res && res.ok) setErrors(res.errors || [])
816
- }).catch(() => {})
817
- }).catch(() => {
818
- // list 失败时仍尝试补 usage/status,避免整卡全空
819
- api('manage', { op: 'usage' }).then((res) => {
820
- if (res && res.ok) {
821
- setUsage(res.bytes || 0)
822
- setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
823
- }
824
- }).catch(() => {})
825
- api('status', {}).then((res) => {
826
- if (res && res.ok) setErrors(res.errors || [])
827
- }).catch(() => {})
828
- })
829
- }
830
-
831
- React.useEffect(() => { refresh() }, [])
832
-
833
- // S3-5:清空 Host 侧错误缓冲并同步本地状态;Host 已清后下次
834
- // refresh 拉回为空,不重复请求。
835
- function clearErrors() {
836
- setErrors([])
837
- api('status', { op: 'clear' }).catch(() => {})
838
- }
839
-
840
- function run(op, extra, doneText) {
841
- if (state.busy) return
842
- setState({ busy: true, message: '执行中…', error: false })
843
- api('manage', Object.assign({ op }, extra || {})).then((res) => {
844
- if (res && res.ok) {
845
- setState({ busy: false, message: typeof res.deleted === 'number' ? '已删除 ' + res.deleted + ' 条快照' : doneText, error: false })
846
- refresh()
847
- } else {
848
- setState({ busy: false, message: (res && (res.message || res.error)) || '操作失败', error: true })
849
- }
850
- }).catch((e) => setState({ busy: false, message: String(e), error: true }))
851
- }
852
-
853
- // 全局删除独立于树节点:后端直接枚举每个 store 的真实 snap-* tag,
854
- // 即使历史 index.json 已丢失也能清理。确认状态与节点删除共用,
855
- // 但全局操作不需要构造 root/session 参数。
856
- function renderDeleteAllConfirm() {
857
- if (!confirming || confirming.kind !== 'all') return null
858
- return React.createElement('div', { className: 'dsh-recall-tree-confirm' },
859
- '确认删除所有工作区的全部快照?此操作不可恢复。',
860
- React.createElement('button', {
861
- type: 'button',
862
- className: 'dsh-recall-btn dsh-recall-btn-danger',
863
- onClick: () => {
864
- setConfirming(null)
865
- run('deleteAll', {}, '已清空全部快照')
866
- }
867
- }, '确认全部删除'),
868
- React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirming(null) }, '取消')
869
- )
870
- }
871
-
872
- // 树形管理:工作区 会话 → 快照三级。折叠状态用 Set<key> 记录,
873
- // 工作区 key root 路径(可能含特殊字符,但 Set 按字符串精确匹配);
874
- // 会话 key sessionId;叶子无子节点。删除走行内二次确认:
875
- // confirming 存 { kind, key, extra, text },行内原地展开确认。
876
- const [expanded, setExpanded] = React.useState(() => new Set())
877
- const [confirming, setConfirming] = React.useState(null)
878
- function toggle(key) {
879
- setExpanded((prev) => {
880
- const next = new Set(prev)
881
- if (next.has(key)) next.delete(key)
882
- else next.add(key)
883
- return next
884
- })
885
- }
886
- // 把扁平列表组装成树。同一快照只属于一个工作区/会话,root 或
887
- // sessionId 缺失时归入「未知」节点,避免行凭空消失。
888
- function buildTree(list) {
889
- const workspaces = new Map()
890
- for (const it of list || []) {
891
- const rootKey = it.root || 'unknown-root'
892
- if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || '未知工作区', sessions: new Map() })
893
- const ws = workspaces.get(rootKey)
894
- const sidKey = it.sessionId || 'unknown-session'
895
- if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] })
896
- ws.sessions.get(sidKey).items.push(it)
897
- }
898
- const wsList = Array.from(workspaces.values())
899
- wsList.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
900
- for (const ws of wsList) {
901
- ws.sessions = Array.from(ws.sessions.values())
902
- ws.sessions.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
903
- for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0))
904
- }
905
- return wsList
906
- }
907
- const q = query.trim().toLowerCase()
908
- const filteredItems = q
909
- ? (items || []).filter((it) =>
910
- (it.workspace || '').toLowerCase().indexOf(q) >= 0 ||
911
- (it.sessionTitle || '').toLowerCase().indexOf(q) >= 0 ||
912
- (it.messageText || '').toLowerCase().indexOf(q) >= 0 ||
913
- String(it.id || '').toLowerCase().indexOf(q) >= 0
914
- )
915
- : items
916
- const tree = buildTree(filteredItems)
917
- // 每个节点的删除按钮统一走 run();workspace/session 用新端点 scope
918
- // 批量删,叶子沿用原单条删除。二次确认文案按节点层级区分。
919
- function confirmDelete(kind, key, extra, text) {
920
- setConfirming({ kind, key, extra, text })
921
- }
922
- function renderConfirm(kind, key, extra, text) {
923
- if (!confirming || confirming.kind !== kind || confirming.key !== key) return null
924
- return React.createElement('div', { className: 'dsh-recall-tree-confirm' },
925
- text,
926
- React.createElement('button', {
927
- type: 'button',
928
- className: 'dsh-recall-ex-chip',
929
- onClick: () => {
930
- const c = confirming
931
- setConfirming(null)
932
- run('delete', c.extra, '已删除')
933
- }
934
- }, '确认'),
935
- React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirming(null) }, '取消')
936
- )
937
- }
938
- // 叶子节点:展开箭头占位 + 时间 + 消息内容摘要 + 截断 ID。
939
- // 悬停 title 显示完整消息内容(优先)或完整 ID(无内容时)。
940
- // 消息文本来自 Host live 事件或冷日志异步补齐;取到前/确实没有时
941
- // 退化为显示 ID,避免假“加载中”占位。
942
- function renderLeaf(it) {
943
- const key = 'snap-' + it.id
944
- const text = it.messageText
945
- const title = text || it.id
946
- const label = text
947
- ? clockText(it.time) + ' ' + text
948
- : clockText(it.time) + ' ' + it.id.slice(0, 12) + '…'
949
- return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
950
- React.createElement('div', { className: 'dsh-recall-tree-row', title: title },
951
- React.createElement('span', { className: 'dsh-recall-tree-toggle-placeholder' }),
952
- React.createElement('span', { className: 'dsh-recall-tree-label' },
953
- React.createElement('span', { className: 'dsh-recall-tree-title' }, label)
954
- ),
955
- React.createElement('button', {
956
- type: 'button',
957
- className: 'dsh-recall-ex-chip',
958
- title: '删除该快照(tag 与索引条目)',
959
- onClick: () => confirmDelete('snapshot', key, { messageId: it.id, root: it.root || null }, '确认删除该快照?此操作不可恢复。')
960
- }, '删除')
961
- ),
962
- renderConfirm('snapshot', key, { messageId: it.id, root: it.root || null }, '确认删除该快照?此操作不可恢复。')
963
- )
964
- }
965
- // 会话节点:折叠按钮 + 标题 + 快照数 + 删除按钮;子节点为叶子。
966
- function renderSession(s) {
967
- const key = 'session-' + (s.root || '') + '-' + s.sessionId
968
- const open = expanded.has(key)
969
- const label = s.title || (titlesPending && s.sessionId ? '…' : '(已删除会话)')
970
- return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
971
- React.createElement('div', { className: 'dsh-recall-tree-row' },
972
- React.createElement('span', {
973
- className: 'dsh-recall-tree-toggle',
974
- onClick: () => toggle(key)
975
- }, open ? '▾' : '▸'),
976
- React.createElement('span', { className: 'dsh-recall-tree-label', title: s.sessionId || '' },
977
- React.createElement('span', { className: 'dsh-recall-tree-title' }, label),
978
- React.createElement('span', { className: 'dsh-recall-tree-meta' }, s.items.length + ' 条')
979
- ),
980
- s.sessionId ? React.createElement('button', {
981
- type: 'button',
982
- className: 'dsh-recall-ex-chip',
983
- title: '删除该会话全部快照',
984
- onClick: () => confirmDelete('session', key, { scope: 'session', sessionId: s.sessionId, root: s.root || null }, '确认删除该会话全部快照?此操作不可恢复。')
985
- }, '删除') : null
986
- ),
987
- open ? React.createElement('div', { className: 'dsh-recall-tree-children' }, ...s.items.map(renderLeaf)) : null,
988
- s.sessionId ? renderConfirm('session', key, { scope: 'session', sessionId: s.sessionId, root: s.root || null }, '确认删除该会话全部快照?此操作不可恢复。') : null
989
- )
990
- }
991
- // 工作区节点:折叠按钮 + 文件夹名 + 会话数/快照数 + 删除按钮;
992
- // 子节点为会话。叶子不展开箭头。
993
- function renderWorkspace(ws) {
994
- const key = 'ws-' + ws.root
995
- const open = expanded.has(key)
996
- const sessionCount = ws.sessions.length
997
- const snapCount = ws.sessions.reduce((n, s) => n + s.items.length, 0)
998
- return React.createElement('div', { className: 'dsh-recall-tree-node', key: key },
999
- React.createElement('div', { className: 'dsh-recall-tree-row' },
1000
- React.createElement('span', {
1001
- className: 'dsh-recall-tree-toggle',
1002
- onClick: () => toggle(key)
1003
- }, open ? '▾' : '▸'),
1004
- React.createElement('span', { className: 'dsh-recall-tree-label', title: ws.root || '' },
1005
- React.createElement('span', { className: 'dsh-recall-tree-name' }, ws.name),
1006
- React.createElement('span', { className: 'dsh-recall-tree-meta' }, sessionCount + ' 会话 / ' + snapCount + ' 快照')
1007
- ),
1008
- ws.root ? React.createElement('button', {
1009
- type: 'button',
1010
- className: 'dsh-recall-ex-chip',
1011
- title: '删除该工作区全部快照',
1012
- onClick: () => confirmDelete('workspace', key, { scope: 'workspace', root: ws.root }, '确认删除该工作区全部快照?此操作不可恢复。')
1013
- }, '删除') : null
1014
- ),
1015
- open ? React.createElement('div', { className: 'dsh-recall-tree-children' }, ...ws.sessions.map(renderSession)) : null,
1016
- ws.root ? renderConfirm('workspace', key, { scope: 'workspace', root: ws.root }, '确认删除该工作区全部快照?此操作不可恢复。') : null
1017
- )
1018
- }
1019
- const treeNodes = tree.map(renderWorkspace)
1020
-
1021
- // 计数用 Host 返回的全量 total 而非已加载条数(S1-2):loaded
1022
- // null 表示首屏加载中(显示 …),已加载但 total 超过当前 limit
1023
- // 时提示「当前显示最新 X 条」与加载更多入口。
1024
- const loaded = items ? items.length : null
1025
- const countText = loaded === null
1026
- ? '共 条快照'
1027
- : '共 ' + total + ' 条快照' + (limit < total ? '(当前显示最新 ' + loaded + ' 条)' : '')
1028
-
1029
- // 加载更多:上限 2000 与 Host 端钳制一致;total 已含全量计数,
1030
- // 直接按 total 一次拉满(再往上就是 2000 封顶)
1031
- function loadMore() {
1032
- const next = Math.min(Math.max(total, limit), 2000)
1033
- if (next <= limit) return
1034
- setLimit(next)
1035
- refresh(next)
1036
- }
1037
-
1038
- return React.createElement('div', { className: 'dsh-recall-ex-card' },
1039
- React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照管理'),
1040
- React.createElement('div', { className: 'dsh-recall-ex-note' },
1041
- usage === null
1042
- ? countText + '。'
1043
- : countText + ',全部工作区快照存储占用 ' + sizeText(usage) + '。'
1044
- ),
1045
- health ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'health' },
1046
- React.createElement('span', {
1047
- className: health.gitAvailable ? '' : 'dsh-recall-ex-status-error'
1048
- }, health.gitAvailable ? 'git 可用' : 'git 不可用(快照引擎依赖 git)'),
1049
- ' · 快照存储:home ' + health.homeStores + ' 个工作区' + (health.fallbackStores ? ',降级 ' + health.fallbackStores + ' 个' : '')
1050
- ) : null,
1051
- React.createElement('input', {
1052
- className: 'dsh-recall-ex-input',
1053
- placeholder: '搜索工作区 / 会话标题 / 消息内容 / ID',
1054
- value: query,
1055
- spellCheck: false,
1056
- onChange: (e) => setQuery(e.target.value),
1057
- }),
1058
- treeNodes.length > 0 ? React.createElement('div', { className: 'dsh-recall-tree' }, ...treeNodes) : null,
1059
- items && items.length === 0 && !q
1060
- ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'empty' }, '在任意工作区发送一条消息后,这里会出现快照。')
1061
- : null,
1062
- q && filteredItems && filteredItems.length === 0
1063
- ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'no-match' }, '无匹配快照')
1064
- : null,
1065
- renderDeleteAllConfirm(),
1066
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
1067
- state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
1068
- limit < total ? React.createElement('button', {
1069
- type: 'button',
1070
- className: 'dsh-recall-btn',
1071
- disabled: state.busy,
1072
- onClick: loadMore
1073
- }, '加载更多') : null,
1074
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy, onClick: refresh }, '刷新'),
1075
- React.createElement('button', {
1076
- type: 'button',
1077
- className: 'dsh-recall-btn dsh-recall-btn-danger',
1078
- disabled: state.busy,
1079
- title: '删除全部工作区的所有快照;会直接核对并删除 git tag(即使列表为空也可清理残留)',
1080
- onClick: () => setConfirming({ kind: 'all' })
1081
- }, '全部删除'),
1082
- React.createElement('button', {
1083
- type: 'button',
1084
- className: 'dsh-recall-btn',
1085
- disabled: state.busy,
1086
- title: '立即对全部工作区执行一次 git gc(压缩对象库释放空间)',
1087
- onClick: () => run('gc', {}, 'gc 完成')
1088
- }, '立即 gc')
1089
- ),
1090
- errors && errors.length > 0
1091
- ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'errors' },
1092
- React.createElement('div', { className: 'dsh-recall-ex-status' },
1093
- '最近错误:',
1094
- (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement('div', { key: i, className: 'dsh-recall-ex-note' }, clockText(e.time) + ' ' + e.message))
1095
- ),
1096
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
1097
- errors.length > 5 ? React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? '收起' : '展开全部 (' + errors.length + ')') : null,
1098
- React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: clearErrors }, '清空')
1099
- )
1100
- )
1101
- : null
1102
- )
1103
- }
1104
-
1105
- // 排除配置分区:拉取 Host 枚举的 exclude 文件列表(home 存储通常
1106
- // 合并为一条,降级工作区各一条)。折叠展开时由设置外壳保持挂载,
1107
- // 本地草稿在折叠期间不丢失。
1108
- function ExcludeFilesSection() {
1109
- const [files, setFiles] = React.useState(null)
1110
- const [error, setError] = React.useState('')
1111
-
1112
- function load() {
1113
- api('exclude-get', {}).then((res) => {
1114
- if (res && res.ok) { setFiles(res.files || []); setError(''); return }
1115
- if (res && res.unsupported) { setError('当前平台不支持快照功能,排除配置不可用。'); return }
1116
- setError((res && (res.message || res.error)) || '无法读取排除配置')
1117
- }).catch((e) => setError(String(e)))
1118
- }
1119
-
1120
- React.useEffect(() => { load() }, [])
1121
-
1122
- if (error) {
1123
- return React.createElement('div', { className: 'dsh-recall-ex-card' },
1124
- React.createElement('div', { className: 'dsh-recall-ex-note' }, error),
1125
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
1126
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: load }, '重试')
1127
- )
1128
- )
1129
- }
1130
- if (files === null) {
1131
- return React.createElement('div', { className: 'dsh-recall-ex-note' }, '正在加载排除配置…')
1132
- }
1133
- if (!files.length) {
1134
- // 全新安装且从未发过消息:store 还没建,先告诉用户怎么触发
1135
- return React.createElement('div', { className: 'dsh-recall-ex-note' }, '尚未创建任何快照存储:在任意工作区发送一条消息后,这里会出现可编辑的排除配置。')
1136
- }
1137
- return React.createElement('div', { className: 'dsh-recall-ex-card' },
1138
- ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1139
- )
1140
- }
1141
-
1142
- // ConfigForm 的字节↔MB 换算:持久化与 schema 都是字节(config-get
1143
- // 下发原值),只在 display/input 层换算成人工友好的 MB 小数;round
1144
- // 2 位小数去尾零,避免默认值 104857600 裸奔成一长串数字
1145
- // (plan-settings-ux S1-1)。
1146
- function bytesToMb(bytes) {
1147
- const n = Number(bytes)
1148
- if (!Number.isFinite(n) || n <= 0) return ''
1149
- return String(Math.round((n / 1048576) * 100) / 100)
1150
- }
1151
-
1152
- // 插件配置表单:值经 Host settings namespace「dsh-recall」读写
1153
- // (config-get / config-set 端点),保存即持久化进用户 settings 文档
1154
- // 并热生效(Host watch 链路原地更新 cfg),无需重启。只提交相对
1155
- // 基线修改过的字段,避免一次保存把全部字段标成「用户覆盖」。
1156
- function ConfigForm() {
1157
- const [baseline, setBaseline] = React.useState(null)
1158
- const [draft, setDraft] = React.useState(null)
1159
- const [envLocks, setEnvLocks] = React.useState({})
1160
- const [overridden, setOverridden] = React.useState({})
1161
- const [writable, setWritable] = React.useState(true)
1162
- const [state, setState] = React.useState({ busy: false, message: '', error: false })
1163
- // 「高级:基础排除表」折叠态(S3-2):与 exclude.txt 同为 gitignore
1164
- // 编辑器,叠放会造成认知负担——内置规则收进折叠,只留高频项在首屏
1165
- const [showAdvanced, setShowAdvanced] = React.useState(false)
1166
-
1167
- function load() {
1168
- api('config-get', {}).then((res) => {
1169
- if (res && res.ok) {
1170
- const v = res.values || {}
1171
- const next = {
1172
- gcSnaps: String(v.gcSnaps == null ? '' : v.gcSnaps),
1173
- gcHours: String(v.gcHours == null ? '' : v.gcHours),
1174
- maxFileBytes: bytesToMb(v.maxFileBytes),
1175
- maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? '' : v.maxSnapshotsPerWorkspace),
1176
- baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join('\n') : '',
1177
- refillDraft: v.refillDraft !== false,
1178
- snapshotEnabled: v.snapshotEnabled !== false,
1179
- archiveOriginal: v.archiveOriginal !== false,
1180
- retentionDays: String(v.retentionDays == null ? '' : v.retentionDays),
1181
- }
1182
- setDraft(next)
1183
- setBaseline(next)
1184
- setEnvLocks(res.envLocks || {})
1185
- setOverridden(res.overridden || {})
1186
- setWritable(res.writable !== false)
1187
- } else {
1188
- setState({ busy: false, message: (res && (res.message || res.error)) || '无法读取配置', error: true })
1189
- }
1190
- }).catch((e) => setState({ busy: false, message: String(e), error: true }))
1191
- }
1192
-
1193
- React.useEffect(() => { load() }, [])
1194
-
1195
- function edit(key, value) {
1196
- setDraft((d) => Object.assign({}, d, { [key]: value }))
1197
- }
1198
-
1199
- function save() {
1200
- if (state.busy || !draft || !baseline) return
1201
- const patch = {}
1202
- for (const key of ['gcSnaps', 'gcHours', 'maxFileBytes', 'maxSnapshotsPerWorkspace', 'baseExcludes', 'refillDraft', 'snapshotEnabled', 'archiveOriginal', 'retentionDays']) {
1203
- if (draft[key] !== baseline[key]) patch[key] = draft[key]
1204
- }
1205
- if (!Object.keys(patch).length) {
1206
- setState({ busy: false, message: '没有修改', error: false })
1207
- return
1208
- }
1209
- const clean = {}
1210
- if (patch.gcSnaps !== undefined) {
1211
- const n = parseInt(patch.gcSnaps, 10)
1212
- if (!Number.isFinite(n) || n < 1) { setState({ busy: false, message: '快照条数阈值必须是 >= 1 的整数', error: true }); return }
1213
- clean.gcSnaps = n
1214
- }
1215
- if (patch.gcHours !== undefined) {
1216
- const n = parseInt(patch.gcHours, 10)
1217
- if (!Number.isFinite(n) || n < 1) { setState({ busy: false, message: 'gc 小时阈值必须是 >= 1 的整数', error: true }); return }
1218
- clean.gcHours = n
1219
- }
1220
- if (patch.maxFileBytes !== undefined) {
1221
- // display 层是 MB 小数,持久化仍是字节(Math.round 取整):
1222
- // model 侧不变,config-get/config-set 往返零改动
1223
- const mb = Number(patch.maxFileBytes)
1224
- if (!Number.isFinite(mb) || mb < 0.01) { setState({ busy: false, message: '文件大小上限至少 0.01 MB', error: true }); return }
1225
- clean.maxFileBytes = Math.round(mb * 1048576)
1226
- }
1227
- if (patch.maxSnapshotsPerWorkspace !== undefined) {
1228
- const n = parseInt(patch.maxSnapshotsPerWorkspace, 10)
1229
- if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '快照总量上限必须是 >= 0 的整数(0 表示不限制)', error: true }); return }
1230
- clean.maxSnapshotsPerWorkspace = n
1231
- }
1232
- if (patch.refillDraft !== undefined) clean.refillDraft = Boolean(patch.refillDraft)
1233
- if (patch.snapshotEnabled !== undefined) clean.snapshotEnabled = Boolean(patch.snapshotEnabled)
1234
- if (patch.archiveOriginal !== undefined) clean.archiveOriginal = Boolean(patch.archiveOriginal)
1235
- if (patch.retentionDays !== undefined) {
1236
- const n = parseInt(patch.retentionDays, 10)
1237
- if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '保留天数必须是 >= 0 的整数(0 表示不启用)', error: true }); return }
1238
- clean.retentionDays = n
1239
- }
1240
- if (patch.baseExcludes !== undefined) {
1241
- clean.baseExcludes = String(patch.baseExcludes).split('\n').map((l) => l.trim()).filter(Boolean)
1242
- }
1243
- setState({ busy: true, message: '保存中…', error: false })
1244
- api('config-set', { patch: clean }).then((res) => {
1245
- if (res && res.ok) {
1246
- setState({ busy: false, message: '已保存并即时生效', error: false })
1247
- load()
1248
- } else {
1249
- setState({ busy: false, message: (res && (res.message || res.error)) || '保存失败', error: true })
1250
- }
1251
- }).catch((e) => setState({ busy: false, message: String(e), error: true }))
1252
- }
1253
-
1254
- function numRow(key, label, hint, opts) {
1255
- const locked = Boolean(envLocks && envLocks[key])
1256
- const changed = Boolean(draft && baseline && draft[key] !== baseline[key])
1257
- return React.createElement('div', { className: 'dsh-recall-cfg-row', key: key },
1258
- React.createElement('div', { className: 'dsh-recall-cfg-line' },
1259
- React.createElement('label', { className: 'dsh-recall-cfg-label' }, label),
1260
- React.createElement('input', {
1261
- className: 'dsh-recall-cfg-input',
1262
- type: 'number',
1263
- value: draft ? draft[key] : '',
1264
- disabled: locked || !writable,
1265
- min: opts && opts.min,
1266
- step: opts && opts.step,
1267
- onChange: (e) => edit(key, e.target.value),
1268
- }),
1269
- opts && opts.suffix ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, opts.suffix) : null,
1270
- changed && !locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
1271
- overridden && overridden[key] !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null,
1272
- locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '环境变量锁定') : null
1273
- ),
1274
- React.createElement('div', { className: 'dsh-recall-cfg-hint' }, hint)
1275
- )
1276
- }
1277
-
1278
- function resetDefaults() {
1279
- if (state.busy || !writable) return
1280
- setState({ busy: true, message: '恢复默认中…', error: false })
1281
- api('config-reset', {}).then((res) => {
1282
- if (res && res.ok) {
1283
- load()
1284
- setState({ busy: false, message: '已恢复默认值', error: false })
1285
- } else {
1286
- setState({ busy: false, message: (res && (res.message || res.error)) || '恢复默认失败', error: true })
1287
- }
1288
- }).catch((e) => setState({ busy: false, message: String(e), error: true }))
1289
- }
1290
-
1291
- if (!draft) {
1292
- return React.createElement('div', { className: 'dsh-recall-ex-note' }, state.message || '正在读取配置…')
1293
- }
1294
-
1295
- return React.createElement('div', { className: 'dsh-recall-ex-card' },
1296
- React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'snapshotEnabled' },
1297
- React.createElement('div', { className: 'dsh-recall-cfg-line' },
1298
- React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-snapshot' }, '启用快照'),
1299
- React.createElement('input', {
1300
- id: 'dsh-recall-cfg-snapshot',
1301
- type: 'checkbox',
1302
- checked: Boolean(draft.snapshotEnabled),
1303
- disabled: !writable,
1304
- onChange: (e) => edit('snapshotEnabled', e.target.checked),
1305
- }),
1306
- draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
1307
- overridden && overridden.snapshotEnabled !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
1308
- ),
1309
- React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '关闭后不再新建快照(已有快照仍可撤回),适合临时禁用快照的场合')
1310
- ),
1311
- numRow('gcSnaps', 'gc 触发条数', '每积累多少条快照触发一次 git gc', { min: 1, step: 1 }),
1312
- numRow('gcHours', 'gc 触发小时', '距上次 gc 超过多少小时触发(与条数先到先触发)', { min: 1, step: 1 }),
1313
- numRow('maxFileBytes', '文件大小上限', '超过该大小的文件不进快照、不被回退触碰(单位 MB,支持小数)', { suffix: 'MB', min: 0.01, step: 0.5 }),
1314
- numRow('maxSnapshotsPerWorkspace', '快照总量上限', '每个工作区保留的最大快照数,超限自动删除最旧的;填 0 表示不限制', { min: 0, step: 1 }),
1315
- numRow('retentionDays', '快照保留天数', '按天数保留快照,超期自动删除最旧的;填 0 表示不启用(与快照总数上限各自生效)', { min: 0, step: 1 }),
1316
- React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'refillDraft' },
1317
- React.createElement('div', { className: 'dsh-recall-cfg-line' },
1318
- React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-refill' }, '撤回后回填输入框'),
1319
- React.createElement('input', {
1320
- id: 'dsh-recall-cfg-refill',
1321
- type: 'checkbox',
1322
- checked: Boolean(draft.refillDraft),
1323
- disabled: !writable,
1324
- onChange: (e) => edit('refillDraft', e.target.checked),
1325
- }),
1326
- draft.refillDraft !== baseline.refillDraft ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
1327
- overridden && overridden.refillDraft !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
1328
- ),
1329
- React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回成功后把被撤回的消息文本回填到输入框,方便修改后重新发送')
1330
- ),
1331
- React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'archiveOriginal' },
1332
- React.createElement('div', { className: 'dsh-recall-cfg-line' },
1333
- React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-archive' }, '撤回后归档原会话'),
1334
- React.createElement('input', {
1335
- id: 'dsh-recall-cfg-archive',
1336
- type: 'checkbox',
1337
- checked: Boolean(draft.archiveOriginal),
1338
- disabled: !writable,
1339
- onChange: (e) => edit('archiveOriginal', e.target.checked),
1340
- }),
1341
- draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
1342
- overridden && overridden.archiveOriginal !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
1343
- ),
1344
- React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回后原会话从列表归档隐藏(可从归档找回);关闭则保留在列表中,方便对照回退前后的上下文')
1345
- ),
1346
- React.createElement(SectionToggle, { title: '高级:基础排除表', open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
1347
- showAdvanced ? React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'baseExcludes' },
1348
- React.createElement('div', { className: 'dsh-recall-cfg-line' },
1349
- React.createElement('label', { className: 'dsh-recall-cfg-label' }, '基础排除表'),
1350
- draft.baseExcludes !== baseline.baseExcludes ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
1351
- overridden && overridden.baseExcludes !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
1352
- ),
1353
- React.createElement('textarea', {
1354
- className: 'dsh-recall-cfg-area',
1355
- rows: 4,
1356
- value: draft.baseExcludes,
1357
- disabled: !writable,
1358
- onChange: (e) => edit('baseExcludes', e.target.value),
1359
- }),
1360
- React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '内置规则,每个工作区共享,建议保持默认;gitignore 语法每行一条,优先级低于「排除配置」里的 exclude.txt(S3-2 折叠)')
1361
- ) : null,
1362
- React.createElement('div', { className: 'dsh-recall-panel-actions' },
1363
- state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
1364
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, '放弃修改'),
1365
- React.createElement('button', {
1366
- type: 'button',
1367
- className: 'dsh-recall-btn',
1368
- disabled: state.busy || !writable,
1369
- title: '把所有字段恢复到插件出厂默认值',
1370
- onClick: resetDefaults
1371
- }, '恢复默认'),
1372
- React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: save }, '保存'),
1373
- !writable ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '只读设置源') : null
1374
- )
1375
- )
1376
- }
1377
-
1378
- // 分区折叠头:官方卡片列表纵向排布,排除配置/快照管理是重内容,
1379
- // 默认折叠、按需展开(展开后由设置外壳保持挂载,草稿不丢)。
1380
- function SectionToggle(props) {
1381
- return React.createElement('button', {
1382
- type: 'button',
1383
- className: 'dsh-recall-cardbtn',
1384
- 'aria-expanded': props.open,
1385
- onClick: props.onToggle,
1386
- },
1387
- React.createElement('span', { className: 'dsh-recall-tree-toggle' }, props.open ? '▾' : '▸'),
1388
- React.createElement('span', { style: { fontWeight: 600, fontSize: '14px', lineHeight: '22px' } }, props.title),
1389
- props.meta ? React.createElement('span', { className: 'dsh-recall-tree-meta' }, props.meta) : null
1390
- )
1391
- }
1392
-
1393
- // 「插件配置」分区里的撤回卡片(settings.plugin.item keyed slot,
1394
- // key = Host 端注册的 settings namespace 'dsh-recall')。整卡默认
1395
- // 收起、点卡片头展开,视觉规格对齐官方 PluginCard(ui-settings-
1396
- // plugins 的同名 CSS:bg-layer-3 底、展开变 bg-layer-2 + 边框
1397
- // label-dimmed、header 14px 16px、标题 15px/600、描述 13px、
1398
- // 内容区上边框 + margin 0 16px 分隔)。展开后内含三段:插件配置
1399
- // 表单 + 排除配置(折叠)+ 快照管理(折叠)。
1400
- function RecallSettingsCard() {
1401
- const [open, setOpen] = React.useState(false)
1402
- const [sections, setSections] = React.useState({ exclude: false, manage: false })
1403
- function toggle(key) {
1404
- setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }))
1405
- }
1406
- return React.createElement('li', { className: 'dsh-recall-card' + (open ? ' dsh-recall-card-open' : '') },
1407
- React.createElement('button', {
1408
- type: 'button',
1409
- className: 'dsh-recall-cardbtn',
1410
- 'aria-expanded': open,
1411
- 'aria-label': (open ? '收起' : '展开') + ': 撤回插件',
1412
- onClick: () => setOpen((v) => !v),
1413
- },
1414
- React.createElement('span', { className: 'dsh-recall-card-head' },
1415
- React.createElement('span', { className: 'dsh-recall-card-name' }, '撤回插件'),
1416
- React.createElement('span', { className: 'dsh-recall-card-desc' }, '消息撤回(文件快照 + 对话回退)的阈值与治理')
1417
- ),
1418
- React.createElement('svg', {
1419
- width: 14, height: 14, viewBox: '0 0 16 16',
1420
- style: { color: 'var(--dsw-alias-label-tertiary)', flex: 'none', transition: 'transform .16s', transform: open ? 'rotate(180deg)' : 'none' }
1421
- }, React.createElement('path', { d: 'M4 6l4 4 4-4', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' }))
1422
- ),
1423
- open ? React.createElement('div', { className: 'dsh-recall-card-body' },
1424
- React.createElement(ConfigForm),
1425
- React.createElement(SectionToggle, { title: '排除配置(exclude.txt)', open: sections.exclude, onToggle: () => toggle('exclude') }),
1426
- sections.exclude ? React.createElement(ExcludeFilesSection) : null,
1427
- React.createElement(SectionToggle, { title: '快照管理', open: sections.manage, onToggle: () => toggle('manage') }),
1428
- sections.manage ? React.createElement(ManageCard) : null
1429
- ) : null
1430
- )
1431
- }
1432
-
1433
- // keyed slot 显式用低于默认 0 的 priority 注册:默认 0 通常被平台/
1434
- // 其他插件的渲染器占据,不指定 priority 会因 keyed slot 冲突拒载
1435
- // 整个插件;lowest renders,负值恰好覆盖默认渲染器实现撤回 UI。
1436
- // priority -1 也可能被别的机器上的插件占用(同样抛冲突),所以
1437
- // 递减重试三次——最坏情况只是撤回按钮不渲染,绝不让插件加载失败。
1438
- // chat.node 的 keyed key 与节点 UI 投影 kind 对齐:'user' 是常规
1439
- // 用户消息;'steering' 是 agent 运行中插入的转向指令——官方仍按
1440
- // 用户气泡回显,但 keyed 'user' 不命中,会落到默认渲染、撤回按钮
1441
- // 缺失(实测存储层 role 一致、仅 UI 投影 kind 不同)。两个 key
1442
- // 各自独立注册,互不抢占。
1443
- for (const slotKey of ['user', 'steering']) {
1444
- let mounted = false
1445
- for (let priority = -1; priority >= -3 && !mounted; priority--) {
1446
- try {
1447
- slots.inject('conversation.chat.node', () => slots.register(
1448
- { name: 'conversation.chat.node', key: slotKey, priority },
1449
- UserRecallNode
1450
- ))
1451
- mounted = true
1452
- } catch (error) {
1453
- if (priority === -3) console.error('[dsh-recall-plugin] slot register failed (' + slotKey + '):', error)
1454
- }
1455
- }
1456
- }
1457
-
1458
- // 「插件配置」分区挂撤回卡片:settings.plugin.item 是 root 级
1459
- // keyed slot(官方 ui-settings-plugins 的 configurable 标签页声明,
1460
- // 按 settings namespace 作为 entryKey 分发)。key 必须与 Host 端
1461
- // 注册的 namespace 'dsh-recall' 一致——卡片只渲染「Host 服务的
1462
- // namespace」与「slot 注册的卡片」的交集,Host 端
1463
- // installSettingsSection 完成注册后 describe 命中、卡片即出现。
1464
- // 各 namespace 独占自己的 key,无同 key 抢占,不需要 priority
1465
- // (与 conversation.chat.node 覆盖默认渲染器是两套语义)。该 slot
1466
- // 未组装的部署里 inject 只是永不匹配的待定注册,天然无害;
1467
- // try/catch 是防御性兜底——注册失败绝不该拖垮撤回按钮的主功能。
1468
- try {
1469
- slots.inject('settings.plugin.item', () => slots.register(
1470
- { name: 'settings.plugin.item', key: 'dsh-recall' },
1471
- RecallSettingsCard
1472
- ))
1473
- } catch (error) {
1474
- console.error('[dsh-recall-plugin] settings card register failed:', error)
1475
- }
1476
- }
1477
- }
1478
- }
1479
- })
1
+ // src/client/css.js
2
+ var CSS = [
3
+ ".dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}",
4
+ ".dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}",
5
+ ".dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}",
6
+ ".dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}",
7
+ ".dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}",
8
+ ".dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}",
9
+ ".dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}",
10
+ ".dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
11
+ "@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}",
12
+ ".dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}",
13
+ ".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
14
+ ".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
15
+ ".dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}",
16
+ ".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}",
17
+ ".dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}",
18
+ ".dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}",
19
+ ".dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
20
+ ".dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}",
21
+ ".dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}",
22
+ ".dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}",
23
+ ".dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
24
+ ".dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}",
25
+ ".dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}",
26
+ ".dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}",
27
+ ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
28
+ ".dsh-recall-toast.dsh-recall-toast-in{opacity:1}",
29
+ ".dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
30
+ ".dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}",
31
+ ".dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
32
+ ".dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
33
+ ".dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:20px;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}",
34
+ ".dsh-recall-ex-quick{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
35
+ ".dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:20px}",
36
+ ".dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:18px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
37
+ ".dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}",
38
+ ".dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
39
+ ".dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}",
40
+ ".dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}",
41
+ ".dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
42
+ ".dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}",
43
+ ".dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}",
44
+ ".dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
45
+ ".dsh-recall-tree-toggle{flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:11px;line-height:18px;user-select:none}",
46
+ ".dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
47
+ ".dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}",
48
+ ".dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px;overflow:hidden}",
49
+ ".dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}",
50
+ ".dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
51
+ ".dsh-recall-tree-meta{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
52
+ ".dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:16px;border-left:1px solid var(--dsw-alias-border-l1);padding-left:8px}",
53
+ ".dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
54
+ ".dsh-recall-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s;display:flex;flex-direction:column;text-align:left}",
55
+ ".dsh-recall-card.dsh-recall-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
56
+ ".dsh-recall-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
57
+ ".dsh-recall-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
58
+ ".dsh-recall-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
59
+ ".dsh-recall-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
60
+ ".dsh-recall-cardbtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
61
+ ".dsh-recall-cfg-row{display:flex;flex-direction:column;gap:4px}",
62
+ ".dsh-recall-cfg-line{display:flex;align-items:center;gap:8px}",
63
+ ".dsh-recall-cfg-label{flex:none;width:130px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}",
64
+ ".dsh-recall-cfg-input{flex:1;min-width:0;box-sizing:border-box;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px}",
65
+ ".dsh-recall-cfg-input:disabled{opacity:.5}",
66
+ ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;min-height:64px}",
67
+ ".dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding-left:138px}",
68
+ ".dsh-recall-cfg-tag{flex:none;font-size:11px;line-height:16px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}"
69
+ ].join("");
70
+
71
+ // src/client/util.js
72
+ function clockText(ms) {
73
+ try {
74
+ if (!ms || isNaN(new Date(ms).getTime())) return "";
75
+ const d = new Date(ms);
76
+ const now = /* @__PURE__ */ new Date();
77
+ const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
78
+ const hh = String(d.getHours()).padStart(2, "0");
79
+ const mm = String(d.getMinutes()).padStart(2, "0");
80
+ return sameDay ? hh + ":" + mm : d.getMonth() + 1 + "/" + d.getDate() + " " + hh + ":" + mm;
81
+ } catch (e) {
82
+ return "";
83
+ }
84
+ }
85
+ function sizeText(bytes) {
86
+ if (!bytes || bytes <= 0) return "0 MB";
87
+ if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
88
+ if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
89
+ return (bytes / 1073741824).toFixed(2) + " GB";
90
+ }
91
+ function bytesToMb(bytes) {
92
+ const n = Number(bytes);
93
+ if (!Number.isFinite(n) || n <= 0) return "";
94
+ return String(Math.round(n / 1048576 * 100) / 100);
95
+ }
96
+ function buildTree(list) {
97
+ const workspaces = /* @__PURE__ */ new Map();
98
+ for (const it of list || []) {
99
+ const rootKey = it.root || "unknown-root";
100
+ if (!workspaces.has(rootKey)) workspaces.set(rootKey, { root: it.root || null, name: it.workspace || "\u672A\u77E5\u5DE5\u4F5C\u533A", sessions: /* @__PURE__ */ new Map() });
101
+ const ws = workspaces.get(rootKey);
102
+ const sidKey = it.sessionId || "unknown-session";
103
+ if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] });
104
+ ws.sessions.get(sidKey).items.push(it);
105
+ }
106
+ const wsList = Array.from(workspaces.values());
107
+ wsList.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
108
+ for (const ws of wsList) {
109
+ ws.sessions = Array.from(ws.sessions.values());
110
+ ws.sessions.sort((a, b) => (a.title || "").localeCompare(b.title || ""));
111
+ for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0));
112
+ }
113
+ return wsList;
114
+ }
115
+ function buildUtil() {
116
+ function api(name, args) {
117
+ return fetch("/api/recall/" + name, {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json" },
120
+ body: JSON.stringify(args || {})
121
+ }).then((r) => r.json());
122
+ }
123
+ const CODE_TEXT = {
124
+ STALE: "\u9884\u89C8\u540E\u9879\u76EE\u6587\u4EF6\u53D1\u751F\u4E86\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u9884\u89C8\u786E\u8BA4",
125
+ AGENT_BUSY: "Agent \u6B63\u5728\u8FD0\u884C\u4E2D\uFF0C\u8BF7\u5148\u505C\u6B62\u540E\u518D\u64A4\u56DE",
126
+ NO_SNAPSHOT: "\u8BE5\u6D88\u606F\u6CA1\u6709\u53EF\u7528\u7684\u9879\u76EE\u5FEB\u7167",
127
+ NO_STORE: "\u5FEB\u7167\u5B58\u50A8\u4E0D\u53EF\u7528",
128
+ ROLLBACK_FAILED: "\u56DE\u9000\u5931\u8D25"
129
+ };
130
+ function messageFor(res, fallback) {
131
+ if (!res) return fallback;
132
+ const code = res && res.code;
133
+ if (code && CODE_TEXT[code]) return CODE_TEXT[code];
134
+ return res && (res.message || res.error) || fallback;
135
+ }
136
+ const noticeShown = /* @__PURE__ */ new Set();
137
+ function mountToast(text) {
138
+ if (typeof document === "undefined") return;
139
+ try {
140
+ let dismiss = function() {
141
+ if (dismissed) return;
142
+ dismissed = true;
143
+ clearTimeout(timer);
144
+ el.classList.remove("dsh-recall-toast-in");
145
+ setTimeout(() => el.remove(), 300);
146
+ };
147
+ const el = document.createElement("div");
148
+ el.className = "dsh-recall-toast";
149
+ const tag = document.createElement("span");
150
+ tag.className = "dsh-recall-toast-tag";
151
+ tag.textContent = "\u64A4\u56DE\u63D2\u4EF6";
152
+ const body = document.createElement("span");
153
+ body.textContent = text;
154
+ el.appendChild(tag);
155
+ el.appendChild(body);
156
+ el.addEventListener("click", () => dismiss(), { once: true });
157
+ document.body.appendChild(el);
158
+ requestAnimationFrame(() => el.classList.add("dsh-recall-toast-in"));
159
+ const timer = setTimeout(dismiss, 7e3);
160
+ let dismissed = false;
161
+ } catch (e) {
162
+ }
163
+ }
164
+ function showNotice(kind, text) {
165
+ if (noticeShown.has(kind)) return;
166
+ noticeShown.add(kind);
167
+ mountToast(text);
168
+ }
169
+ const toastLastShown = /* @__PURE__ */ new Map();
170
+ function showThrottledToast(text) {
171
+ const key = String(text).slice(0, 80);
172
+ const now = Date.now();
173
+ if (now - (toastLastShown.get(key) || 0) < 10 * 60 * 1e3) return;
174
+ if (toastLastShown.size > 50) toastLastShown.clear();
175
+ toastLastShown.set(key, now);
176
+ mountToast(text);
177
+ }
178
+ const pluginConfig = { refillDraft: true, archiveOriginal: true };
179
+ const initMap = /* @__PURE__ */ new Map();
180
+ function ensureInit(sessionId) {
181
+ if (!sessionId) return Promise.resolve();
182
+ const cached = initMap.get(sessionId);
183
+ if (cached) return cached;
184
+ const done = api("init", { sessionId }).then((res) => {
185
+ if (res && res.config && typeof res.config === "object") {
186
+ if (typeof res.config.refillDraft === "boolean") pluginConfig.refillDraft = res.config.refillDraft;
187
+ if (typeof res.config.archiveOriginal === "boolean") pluginConfig.archiveOriginal = res.config.archiveOriginal;
188
+ }
189
+ const notice = res && res.notice;
190
+ if (notice && notice.unsupported) {
191
+ showNotice("unsupported", "\u64A4\u56DE\u63D2\u4EF6\u4EC5\u652F\u6301 Windows / Linux / macOS\uFF0C\u5F53\u524D\u5E73\u53F0\u7684\u5FEB\u7167\u4E0D\u53EF\u7528\u3002");
192
+ }
193
+ if (notice && notice.gitMissing) {
194
+ showNotice("git", "\u672A\u68C0\u6D4B\u5230 git CLI\uFF0C\u64A4\u56DE\u529F\u80FD\u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09\u3002\u5B89\u88C5 git \u5E76\u91CD\u542F DSH \u540E\u5373\u53EF\u4F7F\u7528\u3002");
195
+ }
196
+ if (notice && notice.homeFallback) {
197
+ showNotice("home", "home \u76EE\u5F55\u4E0D\u53EF\u5199\uFF0C\u5FEB\u7167\u5DF2\u964D\u7EA7\u5B58\u50A8\u5230\u9879\u76EE\u5185 .dsh-recall-snapshots \u76EE\u5F55\u3002");
198
+ }
199
+ }).catch(() => {
200
+ initMap.delete(sessionId);
201
+ });
202
+ initMap.set(sessionId, done);
203
+ return done;
204
+ }
205
+ function writeClipboard(text) {
206
+ try {
207
+ if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
208
+ return navigator.clipboard.writeText(text).then(() => true, () => false);
209
+ }
210
+ } catch (e) {
211
+ }
212
+ try {
213
+ if (typeof document !== "undefined" && typeof document.execCommand === "function") {
214
+ const el = document.createElement("textarea");
215
+ el.value = text;
216
+ el.setAttribute("readonly", "");
217
+ el.style.position = "fixed";
218
+ el.style.left = "-9999px";
219
+ document.body.appendChild(el);
220
+ el.select();
221
+ try {
222
+ return Promise.resolve(document.execCommand("copy"));
223
+ } finally {
224
+ el.remove();
225
+ }
226
+ }
227
+ } catch (e) {
228
+ }
229
+ return Promise.resolve(false);
230
+ }
231
+ return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, pluginConfig };
232
+ }
233
+
234
+ // src/client/recall-node.js
235
+ var KIND_INFO = {
236
+ modified: { label: "\u4FEE\u6539", cls: "modified" },
237
+ restored: { label: "\u6062\u590D", cls: "restored" },
238
+ added: { label: "\u5220\u9664", cls: "added" }
239
+ };
240
+ function summaryText(counts) {
241
+ const parts = [];
242
+ for (const kind of Object.keys(KIND_INFO)) {
243
+ if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + " " + counts[kind]);
244
+ }
245
+ return parts.join(" \xB7 ");
246
+ }
247
+ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
248
+ const { api, ensureInit, showThrottledToast, writeClipboard, clockText: clockText2, pluginConfig, messageFor } = util;
249
+ function CopyIcon() {
250
+ return React.createElement(
251
+ "svg",
252
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, "aria-hidden": true },
253
+ React.createElement("rect", { x: 5.5, y: 5.5, width: 8, height: 8, rx: 1.5 }),
254
+ React.createElement("path", { d: "M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5" })
255
+ );
256
+ }
257
+ function CheckIcon() {
258
+ return React.createElement(
259
+ "svg",
260
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
261
+ React.createElement("path", { d: "m3 8.5 3.2 3.2L13 5" })
262
+ );
263
+ }
264
+ function UndoIcon() {
265
+ return React.createElement(
266
+ "svg",
267
+ { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
268
+ React.createElement("path", { d: "M7.5 3.5 3.5 7.5l4 4" }),
269
+ React.createElement("path", { d: "M4.5 7.5h5a3 3 0 0 1 0 6H8" })
270
+ );
271
+ }
272
+ function recallPanel(recall, closePanel, executeRecall) {
273
+ if (recall.stage === "loading") {
274
+ return React.createElement(
275
+ "div",
276
+ { className: "dsh-recall-panel" },
277
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u8BA1\u7B97\u53D8\u66F4\u2026")
278
+ );
279
+ }
280
+ if (recall.stage === "error") {
281
+ return React.createElement(
282
+ "div",
283
+ { className: "dsh-recall-panel" },
284
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u65E0\u6CD5\u56DE\u9000"),
285
+ React.createElement("div", { className: "dsh-recall-panel-note" }, recall.message || ""),
286
+ React.createElement(
287
+ "div",
288
+ { className: "dsh-recall-panel-actions" },
289
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
290
+ )
291
+ );
292
+ }
293
+ if (recall.stage === "confirm") {
294
+ const changes = recall.changes || [];
295
+ const total = typeof recall.total === "number" ? recall.total : changes.length;
296
+ const counts = { modified: 0, restored: 0, added: 0 };
297
+ for (const c of changes) {
298
+ if (c && counts[c.kind] !== void 0) counts[c.kind]++;
299
+ }
300
+ const rows = changes.map((c, i) => {
301
+ const info = KIND_INFO[c.kind];
302
+ return React.createElement(
303
+ "div",
304
+ { className: "dsh-recall-file", key: i },
305
+ React.createElement("span", { className: "dsh-recall-badge dsh-recall-badge-" + (info ? info.cls : "") }, info ? info.label : c.kind || ""),
306
+ React.createElement("span", { className: "dsh-recall-rel" }, c.rel || "")
307
+ );
308
+ });
309
+ if (recall.truncated) {
310
+ rows.push(React.createElement("div", { className: "dsh-recall-panel-note", key: "truncated" }, "\u2026\u4EC5\u663E\u793A\u524D " + changes.length + " \u6761\uFF0C\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4"));
311
+ }
312
+ const canRevertChat = typeof recall.cutSeq === "number";
313
+ return React.createElement(
314
+ "div",
315
+ { className: "dsh-recall-panel" },
316
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6574\u6BB5\u56DE\u9000"),
317
+ React.createElement(
318
+ "div",
319
+ { className: "dsh-recall-panel-note" },
320
+ "\u5C06\u9879\u76EE\u6062\u590D\u5230" + (recall.time ? " " + clockText2(recall.time) + " " : " ") + "\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002\u5171 " + total + " \u4E2A\u6587\u4EF6\u5C06\u53D8\u66F4" + (summaryText(counts) ? "\uFF08" + summaryText(counts) + "\uFF09" : "") + "\u3002\u6B64\u64CD\u4F5C\u4F1A\u8986\u76D6\u5F53\u524D\u6587\u4EF6\u5185\u5BB9\uFF1B\u56DE\u9000\u524D\u4F1A\u81EA\u52A8\u4FDD\u5B58\u4E00\u4EFD\u5F53\u524D\u72B6\u6001\u7684\u5B89\u5168\u5FEB\u7167\uFF08\u4E0D\u542B\u5728\u4E0B\u65B9\u6E05\u5355\u5185\uFF09\u3002"
321
+ ),
322
+ React.createElement(
323
+ "div",
324
+ { className: "dsh-recall-panel-note" },
325
+ canRevertChat ? "\u5BF9\u8BDD\u5C06\u4E00\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\uFF1A\u8BE5\u6D88\u606F\u53CA\u4E4B\u540E\u7684\u5168\u90E8\u5BF9\u8BDD\u4F1A\u4ECE\u5F53\u524D\u89C6\u56FE\u79FB\u9664\uFF0C\u539F\u4F1A\u8BDD\u5F52\u6863\u4FDD\u5B58\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u8BE5\u6D88\u606F\u662F\u672C\u4F1A\u8BDD\u4E2D\u7B2C\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF0C\u65E0\u6CD5\u56DE\u9000\u5BF9\u8BDD\uFF1B\u786E\u8BA4\u540E\u4EC5\u56DE\u9000\u9879\u76EE\u6587\u4EF6\u3002"
326
+ ),
327
+ changes.length > 0 ? React.createElement("div", { className: "dsh-recall-list" }, ...rows) : null,
328
+ React.createElement(
329
+ "div",
330
+ { className: "dsh-recall-panel-actions" },
331
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u53D6\u6D88"),
332
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", onClick: executeRecall }, "\u786E\u8BA4\u56DE\u9000")
333
+ )
334
+ );
335
+ }
336
+ if (recall.stage === "executing") {
337
+ return React.createElement(
338
+ "div",
339
+ { className: "dsh-recall-panel" },
340
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u6B63\u5728\u56DE\u9000\u2026")
341
+ );
342
+ }
343
+ if (recall.stage === "done") {
344
+ return React.createElement(
345
+ "div",
346
+ { className: "dsh-recall-panel" },
347
+ React.createElement("div", { className: "dsh-recall-panel-title" }, "\u56DE\u9000\u5B8C\u6210"),
348
+ React.createElement(
349
+ "div",
350
+ { className: "dsh-recall-panel-note" },
351
+ recall.chatReverted ? "\u9879\u76EE\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u5DF2\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D\u3002\u65B0\u4F1A\u8BDD\u5DF2\u6253\u5F00\uFF0C\u539F\u4F1A\u8BDD\u5DF2\u5F52\u6863\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\u3002" : "\u9879\u76EE\u5DF2\u6062\u590D\u5230\u53D1\u9001\u8BE5\u6D88\u606F\u65F6\u7684\u72B6\u6001\u3002" + (recall.chatError ? " \u5BF9\u8BDD\u56DE\u9000\u5931\u8D25\uFF1A" + recall.chatError : "")
352
+ ),
353
+ React.createElement(
354
+ "div",
355
+ { className: "dsh-recall-panel-actions" },
356
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: closePanel }, "\u5173\u95ED")
357
+ )
358
+ );
359
+ }
360
+ return null;
361
+ }
362
+ function fillDraft(targetSessionId, draftText) {
363
+ if (!draftText || !targetSessionId) return;
364
+ let attempts = 0;
365
+ const attempt = () => {
366
+ try {
367
+ const conversation = ctx.get("conversation");
368
+ if (conversation && conversation.input && typeof conversation.input.shell === "function") {
369
+ const shell = conversation.input.shell(targetSessionId);
370
+ if (shell) {
371
+ if (shell.actions && typeof shell.actions.setDraft === "function") {
372
+ shell.actions.setDraft(draftText);
373
+ return;
374
+ }
375
+ if (typeof shell.setDraft === "function") {
376
+ shell.setDraft(draftText);
377
+ return;
378
+ }
379
+ }
380
+ }
381
+ } catch (e) {
382
+ }
383
+ if (attempts++ < 8) setTimeout(attempt, 150);
384
+ };
385
+ attempt();
386
+ }
387
+ function UserRecallNode(props) {
388
+ const node = props && props.node;
389
+ const renderMessageImages = props && props.renderMessageImages;
390
+ const sessionId = props && props.sessionId;
391
+ const data = node && node.data ? node.data : {};
392
+ const messageId = node ? String(node.id || node.key || "") : "";
393
+ const blocks = Array.isArray(data.content) ? data.content : [];
394
+ const text = blocks.filter((b) => b && b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
395
+ const imageBlocks = blocks.filter((b) => b && b.type === "image" && b.attachment).map((b) => ({ attachment: b.attachment }));
396
+ const rest = blocks.filter((b) => !b || !(b.type === "text" && typeof b.text === "string") && !(b.type === "image" && b.attachment));
397
+ const [copied, setCopied] = React.useState(false);
398
+ const [hasSnapshot, setHasSnapshot] = React.useState(false);
399
+ const [recall, setRecall] = React.useState({ stage: "idle" });
400
+ React.useEffect(() => {
401
+ let alive = true;
402
+ let timer = null;
403
+ let attempts = 0;
404
+ const RETRY_WINDOW_MS = 5 * 60 * 1e3;
405
+ const MAX_ATTEMPTS = 20;
406
+ const RETRY_MS = 1e3;
407
+ const msgTime = data && typeof data.time === "number" ? data.time : NaN;
408
+ const recent = !isNaN(msgTime) && Date.now() - msgTime <= RETRY_WINDOW_MS;
409
+ function schedule() {
410
+ if (!alive || !messageId) return;
411
+ attempts++;
412
+ api("snapshot-info", { messageId, sessionId }).then((res) => {
413
+ if (!alive) return;
414
+ if (res && res.has) {
415
+ if (recent && Array.isArray(res.skipped) && res.skipped.length) {
416
+ const names = res.skipped.slice(0, 5).join("\u3001") + (res.skipped.length > 5 ? " \u7B49 " + res.skipped.length + " \u9879" : "");
417
+ showThrottledToast("\u5FEB\u7167\u5DF2\u8DF3\u8FC7\u672A\u7EB3\u5165\u7684\u8DEF\u5F84\uFF1A" + names + "\uFF08\u64A4\u56DE\u4E0D\u4F1A\u6062\u590D\u6216\u5220\u9664\u8FD9\u4E9B\u8DEF\u5F84\uFF09");
418
+ }
419
+ setHasSnapshot(true);
420
+ return;
421
+ }
422
+ if (res && res.failed) {
423
+ if (recent) showThrottledToast("\u5FEB\u7167\u5931\u8D25\uFF1A" + String(res.error || "\u672A\u77E5\u539F\u56E0").slice(0, 140));
424
+ return;
425
+ }
426
+ if (recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
427
+ }).catch(() => {
428
+ if (alive && recent && attempts < MAX_ATTEMPTS) timer = setTimeout(schedule, RETRY_MS);
429
+ });
430
+ }
431
+ ensureInit(sessionId).then(() => {
432
+ if (!messageId || !alive) return;
433
+ schedule();
434
+ }).catch(() => {
435
+ if (alive && messageId) timer = setTimeout(schedule, RETRY_MS);
436
+ });
437
+ return () => {
438
+ alive = false;
439
+ if (timer !== null) clearTimeout(timer);
440
+ };
441
+ }, [messageId, sessionId]);
442
+ const onCopy = () => {
443
+ if (copied) return;
444
+ writeClipboard(text).then(() => {
445
+ setCopied(true);
446
+ const timer = ctx.get("timer");
447
+ if (timer && typeof timer.timeout === "function") {
448
+ timer.timeout(() => setCopied(false), 1200);
449
+ } else {
450
+ setTimeout(() => setCopied(false), 1200);
451
+ }
452
+ });
453
+ };
454
+ const openPreview = () => {
455
+ if (recall.stage === "loading" || recall.stage === "executing") return;
456
+ setRecall({ stage: "loading" });
457
+ api("preview", { messageId, sessionId }).then((res) => {
458
+ if (!res || !res.ok) {
459
+ setRecall({ stage: "error", message: messageFor(res, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
460
+ return;
461
+ }
462
+ setRecall({
463
+ stage: "confirm",
464
+ changes: res.changes || [],
465
+ total: typeof res.total === "number" ? res.total : (res.changes || []).length,
466
+ truncated: Boolean(res.truncated),
467
+ time: res.time || null,
468
+ cutSeq: typeof res.cutSeq === "number" ? res.cutSeq : null
469
+ });
470
+ }).catch((error) => {
471
+ setRecall({ stage: "error", message: String(error) });
472
+ });
473
+ };
474
+ const executeRecall = () => {
475
+ if (recall.stage !== "confirm") return;
476
+ const changes = recall.changes || [];
477
+ const previewCut = typeof recall.cutSeq === "number" ? recall.cutSeq : null;
478
+ const previewTotal = typeof recall.total === "number" ? recall.total : changes.length;
479
+ setRecall({ stage: "executing", changes });
480
+ api("execute", { messageId, sessionId, previewTotal, previewAt: Date.now() }).then(async (res) => {
481
+ if (!res || !res.ok) {
482
+ if (res && res.code === "STALE") {
483
+ setRecall({ stage: "loading" });
484
+ api("preview", { messageId, sessionId }).then((res2) => {
485
+ if (!res2 || !res2.ok) {
486
+ setRecall({ stage: "error", message: messageFor(res2, "\u65E0\u6CD5\u83B7\u53D6\u5FEB\u7167") });
487
+ return;
488
+ }
489
+ setRecall({
490
+ stage: "confirm",
491
+ changes: res2.changes || [],
492
+ total: typeof res2.total === "number" ? res2.total : (res2.changes || []).length,
493
+ truncated: Boolean(res2.truncated),
494
+ time: res2.time || null,
495
+ cutSeq: typeof res2.cutSeq === "number" ? res2.cutSeq : null
496
+ });
497
+ }).catch((error) => {
498
+ setRecall({ stage: "error", message: String(error) });
499
+ });
500
+ return;
501
+ }
502
+ setRecall({ stage: "error", message: messageFor(res, "\u56DE\u9000\u5931\u8D25") });
503
+ return;
504
+ }
505
+ const cutSeq = typeof res.cutSeq === "number" ? res.cutSeq : previewCut;
506
+ let chatReverted = false;
507
+ let chatError = "";
508
+ let fillTarget = sessionId;
509
+ if (cutSeq !== null && sessionsSvc && typeof sessionsSvc.fork === "function") {
510
+ try {
511
+ const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq });
512
+ if (childId) {
513
+ if (typeof sessionsSvc.open === "function") sessionsSvc.open(childId);
514
+ chatReverted = true;
515
+ fillTarget = childId;
516
+ api("lineage-record", { childId, parentId: sessionId }).catch(() => {
517
+ });
518
+ if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === "function") {
519
+ workspacesSvc.archiveSession(sessionId).catch(() => {
520
+ });
521
+ }
522
+ } else {
523
+ chatError = "\u672A\u8FD4\u56DE\u65B0\u4F1A\u8BDD";
524
+ }
525
+ } catch (error) {
526
+ chatError = String(error);
527
+ }
528
+ }
529
+ if (pluginConfig.refillDraft) fillDraft(fillTarget, text);
530
+ setHasSnapshot(false);
531
+ setRecall({ stage: "done", count: typeof res.count === "number" ? res.count : changes.length, chatReverted, chatError });
532
+ }).catch((error) => {
533
+ setRecall({ stage: "error", message: String(error) });
534
+ });
535
+ };
536
+ const closePanel = () => setRecall({ stage: "idle" });
537
+ const bubbleChildren = [];
538
+ if (imageBlocks.length && typeof renderMessageImages === "function") {
539
+ bubbleChildren.push(React.createElement(
540
+ React.Fragment,
541
+ { key: "images" },
542
+ renderMessageImages({ images: imageBlocks, align: "end" })
543
+ ));
544
+ }
545
+ if (text !== "") bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" }, text));
546
+ for (let i = 0; i < rest.length; i++) {
547
+ bubbleChildren.push(React.createElement("pre", { className: "dsh-recall-json", key: "rest-" + i }, JSON.stringify(rest[i], null, 2)));
548
+ }
549
+ const actions = [];
550
+ actions.push(React.createElement("span", { className: "dsh-recall-time", key: "time" }, clockText2(data.time)));
551
+ actions.push(React.createElement("button", {
552
+ key: "copy",
553
+ type: "button",
554
+ className: "dsh-recall-action",
555
+ "aria-label": copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
556
+ title: copied ? "\u5DF2\u590D\u5236" : "\u590D\u5236",
557
+ onClick: onCopy
558
+ }, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})));
559
+ if (hasSnapshot) {
560
+ actions.push(React.createElement("button", {
561
+ key: "recall",
562
+ type: "button",
563
+ className: "dsh-recall-action",
564
+ "aria-label": "\u64A4\u56DE",
565
+ title: "\u6574\u6BB5\u56DE\u9000\uFF1A\u6587\u4EF6\u4E0E\u5BF9\u8BDD\u4E00\u5E76\u56DE\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
566
+ onClick: openPreview
567
+ }, React.createElement(UndoIcon, {})));
568
+ }
569
+ return React.createElement(
570
+ "div",
571
+ { className: "dsh-recall-row", "data-time-hover-root": true },
572
+ bubbleChildren.length > 0 ? React.createElement("div", { className: "dsh-recall-stack", key: "stack" }, ...bubbleChildren) : null,
573
+ React.createElement("div", { className: "dsh-recall-actions", key: "actions" }, ...actions),
574
+ recallPanel(recall, closePanel, executeRecall)
575
+ );
576
+ }
577
+ return { UserRecallNode };
578
+ }
579
+
580
+ // src/client/settings-cards.js
581
+ function groupByLineage(ids, lineage) {
582
+ const childOf = /* @__PURE__ */ new Map();
583
+ const childrenOf = /* @__PURE__ */ new Map();
584
+ for (const e of lineage || []) {
585
+ if (e && e.childId && e.parentId) {
586
+ const child = String(e.childId);
587
+ const parent = String(e.parentId);
588
+ childOf.set(child, parent);
589
+ const kids = childrenOf.get(parent) || [];
590
+ kids.push(child);
591
+ childrenOf.set(parent, kids);
592
+ }
593
+ }
594
+ const idSet = new Set((ids || []).map(String));
595
+ const result = /* @__PURE__ */ new Map();
596
+ const assigned = /* @__PURE__ */ new Set();
597
+ for (const id of idSet) {
598
+ if (assigned.has(id)) continue;
599
+ let root = id;
600
+ const seen = /* @__PURE__ */ new Set();
601
+ while (childOf.has(root) && idSet.has(childOf.get(root)) && !seen.has(root)) {
602
+ seen.add(root);
603
+ root = childOf.get(root);
604
+ }
605
+ const chain = [];
606
+ const queue = [root];
607
+ while (queue.length) {
608
+ const cur = queue.shift();
609
+ if (!cur || !idSet.has(cur) || assigned.has(cur)) continue;
610
+ chain.push(cur);
611
+ assigned.add(cur);
612
+ for (const k of childrenOf.get(cur) || []) queue.push(k);
613
+ }
614
+ if (chain.length > 1) {
615
+ chain.forEach((sid, i) => result.set(sid, { family: chain, index: i + 1 }));
616
+ }
617
+ }
618
+ return result;
619
+ }
620
+ function buildSettingsCards(React, util, sessionsSvc) {
621
+ const { api, clockText: clockText2, sizeText: sizeText2, bytesToMb: bytesToMb2, buildTree: buildTree2 } = util;
622
+ const EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
623
+ function ExcludeCard(props) {
624
+ const file = props.file;
625
+ const [draft, setDraft] = React.useState(file.content || "");
626
+ const [baseline, setBaseline] = React.useState(file.content || "");
627
+ const [quick, setQuick] = React.useState("");
628
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
629
+ const dirty = draft !== baseline;
630
+ function appendPattern(pattern) {
631
+ setDraft((d) => (d && !d.endsWith("\n") ? d + "\n" : d) + pattern + "\n");
632
+ }
633
+ function addQuick() {
634
+ const t = quick.trim();
635
+ if (!t) return;
636
+ appendPattern(t);
637
+ setQuick("");
638
+ }
639
+ function save() {
640
+ if (state.busy || !dirty) return;
641
+ setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
642
+ api("exclude-set", { path: file.path, content: draft }).then((res) => {
643
+ if (res && res.ok) {
644
+ setBaseline(draft);
645
+ setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\uFF0C\u4E0B\u4E00\u6B21\u5FEB\u7167 / \u9884\u89C8 / \u56DE\u9000\u65F6\u751F\u6548", error: false });
646
+ } else {
647
+ setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
648
+ }
649
+ }).catch((error) => {
650
+ setState({ busy: false, message: String(error), error: true });
651
+ });
652
+ }
653
+ function discard() {
654
+ if (state.busy) return;
655
+ setDraft(baseline);
656
+ setState({ busy: false, message: "", error: false });
657
+ }
658
+ const draftLines = draft.split("\n").map((l) => l.trim());
659
+ const suggestions = EXCLUDE_SUGGESTIONS.filter((s) => draftLines.indexOf(s) < 0);
660
+ return React.createElement(
661
+ "div",
662
+ { className: "dsh-recall-ex-card" },
663
+ React.createElement("div", { className: "dsh-recall-ex-title" }, "\u5FEB\u7167\u6392\u9664\u9879"),
664
+ React.createElement(
665
+ "div",
666
+ { className: "dsh-recall-ex-note" },
667
+ 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"
668
+ ),
669
+ 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"),
670
+ React.createElement("textarea", {
671
+ className: "dsh-recall-ex-area",
672
+ value: draft,
673
+ spellCheck: false,
674
+ onChange: (e) => setDraft(e.target.value)
675
+ }),
676
+ React.createElement(
677
+ "div",
678
+ { className: "dsh-recall-ex-quick" },
679
+ React.createElement("input", {
680
+ className: "dsh-recall-ex-input",
681
+ value: quick,
682
+ placeholder: "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
683
+ onChange: (e) => setQuick(e.target.value),
684
+ onKeyDown: (e) => {
685
+ if (e.key === "Enter") {
686
+ e.preventDefault();
687
+ addQuick();
688
+ }
689
+ }
690
+ }),
691
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: addQuick }, "\u6DFB\u52A0"),
692
+ ...suggestions.map((s) => React.createElement("button", {
693
+ key: "chip-" + s,
694
+ type: "button",
695
+ className: "dsh-recall-ex-chip",
696
+ title: "\u70B9\u51FB\u8FFD\u52A0 " + s,
697
+ onClick: () => appendPattern(s)
698
+ }, s))
699
+ ),
700
+ React.createElement(
701
+ "div",
702
+ { className: "dsh-recall-panel-actions" },
703
+ state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
704
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
705
+ React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
706
+ )
707
+ );
708
+ }
709
+ function ManageCard(props) {
710
+ const [items, setItems] = React.useState(null);
711
+ const [usage, setUsage] = React.useState(null);
712
+ const [errors, setErrors] = React.useState(null);
713
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
714
+ const [limit, setLimit] = React.useState(200);
715
+ const [total, setTotal] = React.useState(0);
716
+ const [health, setHealth] = React.useState(null);
717
+ const [query, setQuery] = React.useState("");
718
+ const [showAllErrors, setShowAllErrors] = React.useState(false);
719
+ const [titlesPending, setTitlesPending] = React.useState(false);
720
+ const [lineage, setLineage] = React.useState([]);
721
+ function fetchTitles(list) {
722
+ const missing = Array.from(new Set(
723
+ (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
724
+ )).slice(0, 100);
725
+ if (!missing.length) {
726
+ setTitlesPending(false);
727
+ return;
728
+ }
729
+ setTitlesPending(true);
730
+ api("manage", { op: "titles", sessionIds: missing }).then((res) => {
731
+ const map = res && res.ok ? res.titles : null;
732
+ if (map) {
733
+ setItems((prev) => (prev || []).map((it) => it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it));
734
+ }
735
+ setTitlesPending(false);
736
+ }).catch(() => setTitlesPending(false));
737
+ }
738
+ function fetchMessages(list) {
739
+ 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);
740
+ if (!requests.length) return;
741
+ api("manage", { op: "messages", requests }).then((res) => {
742
+ const map = res && res.ok ? res.messageTexts : null;
743
+ if (map) {
744
+ setItems((prev) => (prev || []).map((it) => it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it));
745
+ }
746
+ }).catch(() => {
747
+ });
748
+ }
749
+ function refresh(overLimit) {
750
+ const useLimit = overLimit || limit;
751
+ api("manage", { op: "list", limit: useLimit }).then((res) => {
752
+ if (res && res.ok) {
753
+ setItems(res.items || []);
754
+ setTotal(typeof res.total === "number" ? res.total : (res.items || []).length);
755
+ fetchTitles(res.items || []);
756
+ fetchMessages(res.items || []);
757
+ }
758
+ api("manage", { op: "lineage" }).then((res2) => {
759
+ if (res2 && res2.ok && Array.isArray(res2.lineage)) setLineage(res2.lineage);
760
+ }).catch(() => {
761
+ });
762
+ api("manage", { op: "usage" }).then((res2) => {
763
+ if (res2 && res2.ok) {
764
+ setUsage(res2.bytes || 0);
765
+ setHealth({ gitAvailable: res2.gitAvailable !== false, homeStores: res2.homeStores || 0, fallbackStores: res2.fallbackStores || 0 });
766
+ }
767
+ }).catch(() => {
768
+ });
769
+ api("status", {}).then((res2) => {
770
+ if (res2 && res2.ok) setErrors(res2.errors || []);
771
+ }).catch(() => {
772
+ });
773
+ }).catch(() => {
774
+ api("manage", { op: "usage" }).then((res) => {
775
+ if (res && res.ok) {
776
+ setUsage(res.bytes || 0);
777
+ setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 });
778
+ }
779
+ }).catch(() => {
780
+ });
781
+ api("status", {}).then((res) => {
782
+ if (res && res.ok) setErrors(res.errors || []);
783
+ }).catch(() => {
784
+ });
785
+ });
786
+ }
787
+ React.useEffect(() => {
788
+ refresh();
789
+ }, []);
790
+ function clearErrors() {
791
+ setErrors([]);
792
+ api("status", { op: "clear" }).catch(() => {
793
+ });
794
+ }
795
+ function run(op, extra, doneText) {
796
+ if (state.busy) return;
797
+ setState({ busy: true, message: "\u6267\u884C\u4E2D\u2026", error: false });
798
+ api("manage", Object.assign({ op }, extra || {})).then((res) => {
799
+ if (res && res.ok) {
800
+ setState({ busy: false, message: typeof res.deleted === "number" ? "\u5DF2\u5220\u9664 " + res.deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
801
+ refresh();
802
+ } else {
803
+ setState({ busy: false, message: res && (res.message || res.error) || "\u64CD\u4F5C\u5931\u8D25", error: true });
804
+ }
805
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
806
+ }
807
+ const [expanded, setExpanded] = React.useState(() => /* @__PURE__ */ new Set());
808
+ const [confirming, setConfirming] = React.useState(null);
809
+ function renderDeleteAllConfirm() {
810
+ if (!confirming || confirming.kind !== "all") return null;
811
+ return React.createElement(
812
+ "div",
813
+ { className: "dsh-recall-tree-confirm" },
814
+ "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
815
+ React.createElement("button", {
816
+ type: "button",
817
+ className: "dsh-recall-btn dsh-recall-btn-danger",
818
+ onClick: () => {
819
+ setConfirming(null);
820
+ run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
821
+ }
822
+ }, "\u786E\u8BA4\u5168\u90E8\u5220\u9664"),
823
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
824
+ );
825
+ }
826
+ function toggle(key) {
827
+ setExpanded((prev) => {
828
+ const next = new Set(prev);
829
+ if (next.has(key)) next.delete(key);
830
+ else next.add(key);
831
+ return next;
832
+ });
833
+ }
834
+ const q = query.trim().toLowerCase();
835
+ const filteredItems = q ? (items || []).filter(
836
+ (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
837
+ ) : items;
838
+ const tree = buildTree2(filteredItems);
839
+ const allSessionIds = Array.from(new Set((items || []).map((it) => it.sessionId).filter(Boolean)));
840
+ const versionMap = groupByLineage(allSessionIds, lineage);
841
+ let listById = null;
842
+ try {
843
+ if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.getSnapshot === "function") {
844
+ listById = sessionsSvc.list.getSnapshot().byId || null;
845
+ }
846
+ } catch (e) {
847
+ listById = null;
848
+ }
849
+ function confirmDelete(kind, key, extra, text) {
850
+ setConfirming({ kind, key, extra, text });
851
+ }
852
+ function renderConfirm(kind, key, extra, text) {
853
+ if (!confirming || confirming.kind !== kind || confirming.key !== key) return null;
854
+ return React.createElement(
855
+ "div",
856
+ { className: "dsh-recall-tree-confirm" },
857
+ text,
858
+ React.createElement("button", {
859
+ type: "button",
860
+ className: "dsh-recall-ex-chip",
861
+ onClick: () => {
862
+ const c = confirming;
863
+ setConfirming(null);
864
+ run("delete", c.extra, "\u5DF2\u5220\u9664");
865
+ }
866
+ }, "\u786E\u8BA4"),
867
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
868
+ );
869
+ }
870
+ function renderLeaf(it) {
871
+ const key = "snap-" + it.id;
872
+ const text = it.messageText;
873
+ const title = text || it.id;
874
+ const label = text ? clockText2(it.time) + " " + text : clockText2(it.time) + " " + it.id.slice(0, 12) + "\u2026";
875
+ return React.createElement(
876
+ "div",
877
+ { className: "dsh-recall-tree-node", key },
878
+ React.createElement(
879
+ "div",
880
+ { className: "dsh-recall-tree-row", title },
881
+ React.createElement("span", { className: "dsh-recall-tree-toggle-placeholder" }),
882
+ React.createElement(
883
+ "span",
884
+ { className: "dsh-recall-tree-label" },
885
+ React.createElement("span", { className: "dsh-recall-tree-title" }, label)
886
+ ),
887
+ React.createElement("button", {
888
+ type: "button",
889
+ className: "dsh-recall-ex-chip",
890
+ title: "\u5220\u9664\u8BE5\u5FEB\u7167\uFF08tag \u4E0E\u7D22\u5F15\u6761\u76EE\uFF09",
891
+ 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")
892
+ }, "\u5220\u9664")
893
+ ),
894
+ 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")
895
+ );
896
+ }
897
+ function renderSession(s) {
898
+ const key = "session-" + (s.root || "") + "-" + s.sessionId;
899
+ const open = expanded.has(key);
900
+ const label = s.title || (titlesPending && s.sessionId ? "\u2026" : "\uFF08\u5DF2\u5220\u9664\u4F1A\u8BDD\uFF09");
901
+ const version = s.sessionId ? versionMap.get(String(s.sessionId)) : null;
902
+ const switchable = Boolean(s.sessionId && listById && listById[s.sessionId]);
903
+ return React.createElement(
904
+ "div",
905
+ { className: "dsh-recall-tree-node", key },
906
+ React.createElement(
907
+ "div",
908
+ { className: "dsh-recall-tree-row" },
909
+ React.createElement("span", {
910
+ className: "dsh-recall-tree-toggle",
911
+ onClick: () => toggle(key)
912
+ }, open ? "\u25BE" : "\u25B8"),
913
+ React.createElement(
914
+ "span",
915
+ { className: "dsh-recall-tree-label", title: s.sessionId || "" },
916
+ React.createElement("span", { className: "dsh-recall-tree-title" }, label),
917
+ 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,
918
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, s.items.length + " \u6761")
919
+ ),
920
+ switchable ? React.createElement("button", {
921
+ type: "button",
922
+ className: "dsh-recall-ex-chip",
923
+ title: "\u5207\u6362\u5230\u8BE5\u7248\u672C\u4F1A\u8BDD",
924
+ onClick: () => {
925
+ try {
926
+ sessionsSvc.open(s.sessionId);
927
+ } catch (e) {
928
+ }
929
+ }
930
+ }, "\u5207\u6362") : null,
931
+ s.sessionId ? React.createElement("button", {
932
+ type: "button",
933
+ className: "dsh-recall-ex-chip",
934
+ title: "\u5220\u9664\u8BE5\u4F1A\u8BDD\u5168\u90E8\u5FEB\u7167",
935
+ 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")
936
+ }, "\u5220\u9664") : null
937
+ ),
938
+ open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...s.items.map(renderLeaf)) : null,
939
+ 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
940
+ );
941
+ }
942
+ function renderWorkspace(ws) {
943
+ const key = "ws-" + ws.root;
944
+ const open = expanded.has(key);
945
+ const sessionCount = ws.sessions.length;
946
+ const snapCount = ws.sessions.reduce((n, s) => n + s.items.length, 0);
947
+ return React.createElement(
948
+ "div",
949
+ { className: "dsh-recall-tree-node", key },
950
+ React.createElement(
951
+ "div",
952
+ { className: "dsh-recall-tree-row" },
953
+ React.createElement("span", {
954
+ className: "dsh-recall-tree-toggle",
955
+ onClick: () => toggle(key)
956
+ }, open ? "\u25BE" : "\u25B8"),
957
+ React.createElement(
958
+ "span",
959
+ { className: "dsh-recall-tree-label", title: ws.root || "" },
960
+ React.createElement("span", { className: "dsh-recall-tree-name" }, ws.name),
961
+ React.createElement("span", { className: "dsh-recall-tree-meta" }, sessionCount + " \u4F1A\u8BDD / " + snapCount + " \u5FEB\u7167")
962
+ ),
963
+ ws.root ? React.createElement("button", {
964
+ type: "button",
965
+ className: "dsh-recall-ex-chip",
966
+ title: "\u5220\u9664\u8BE5\u5DE5\u4F5C\u533A\u5168\u90E8\u5FEB\u7167",
967
+ 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")
968
+ }, "\u5220\u9664") : null
969
+ ),
970
+ open ? React.createElement("div", { className: "dsh-recall-tree-children" }, ...ws.sessions.map(renderSession)) : null,
971
+ 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
972
+ );
973
+ }
974
+ const treeNodes = tree.map(renderWorkspace);
975
+ const loaded = items ? items.length : null;
976
+ 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" : "");
977
+ function loadMore() {
978
+ const next = Math.min(Math.max(total, limit), 2e3);
979
+ if (next <= limit) return;
980
+ setLimit(next);
981
+ refresh(next);
982
+ }
983
+ return React.createElement(
984
+ "div",
985
+ { className: "dsh-recall-ex-card" },
986
+ React.createElement("div", { className: "dsh-recall-ex-title" }, "\u5FEB\u7167\u7BA1\u7406"),
987
+ React.createElement(
988
+ "div",
989
+ { className: "dsh-recall-ex-note" },
990
+ usage === null ? countText + "\u3002" : countText + "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 " + sizeText2(usage) + "\u3002"
991
+ ),
992
+ health ? React.createElement(
993
+ "div",
994
+ { className: "dsh-recall-ex-note", key: "health" },
995
+ React.createElement("span", {
996
+ className: health.gitAvailable ? "" : "dsh-recall-ex-status-error"
997
+ }, health.gitAvailable ? "git \u53EF\u7528" : "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09"),
998
+ " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome " + health.homeStores + " \u4E2A\u5DE5\u4F5C\u533A" + (health.fallbackStores ? "\uFF0C\u964D\u7EA7 " + health.fallbackStores + " \u4E2A" : "")
999
+ ) : null,
1000
+ React.createElement("input", {
1001
+ className: "dsh-recall-ex-input",
1002
+ placeholder: "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
1003
+ value: query,
1004
+ spellCheck: false,
1005
+ onChange: (e) => setQuery(e.target.value)
1006
+ }),
1007
+ treeNodes.length > 0 ? React.createElement("div", { className: "dsh-recall-tree" }, ...treeNodes) : null,
1008
+ 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,
1009
+ q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-ex-note", key: "no-match" }, "\u65E0\u5339\u914D\u5FEB\u7167") : null,
1010
+ renderDeleteAllConfirm(),
1011
+ React.createElement(
1012
+ "div",
1013
+ { className: "dsh-recall-panel-actions" },
1014
+ state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1015
+ limit < total ? React.createElement("button", {
1016
+ type: "button",
1017
+ className: "dsh-recall-btn",
1018
+ disabled: state.busy,
1019
+ onClick: loadMore
1020
+ }, "\u52A0\u8F7D\u66F4\u591A") : null,
1021
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: refresh }, "\u5237\u65B0"),
1022
+ React.createElement("button", {
1023
+ type: "button",
1024
+ className: "dsh-recall-btn dsh-recall-btn-danger",
1025
+ disabled: state.busy,
1026
+ 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",
1027
+ onClick: () => setConfirming({ kind: "all" })
1028
+ }, "\u5168\u90E8\u5220\u9664"),
1029
+ React.createElement("button", {
1030
+ type: "button",
1031
+ className: "dsh-recall-btn",
1032
+ disabled: state.busy,
1033
+ 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",
1034
+ onClick: () => run("gc", {}, "gc \u5B8C\u6210")
1035
+ }, "\u7ACB\u5373 gc")
1036
+ ),
1037
+ errors && errors.length > 0 ? React.createElement(
1038
+ "div",
1039
+ { className: "dsh-recall-ex-note", key: "errors" },
1040
+ React.createElement(
1041
+ "div",
1042
+ { className: "dsh-recall-ex-status" },
1043
+ "\u6700\u8FD1\u9519\u8BEF\uFF1A",
1044
+ (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message))
1045
+ ),
1046
+ React.createElement(
1047
+ "div",
1048
+ { className: "dsh-recall-panel-actions" },
1049
+ 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,
1050
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
1051
+ )
1052
+ ) : null
1053
+ );
1054
+ }
1055
+ function ExcludeFilesSection() {
1056
+ const [files, setFiles] = React.useState(null);
1057
+ const [error, setError] = React.useState("");
1058
+ function load() {
1059
+ api("exclude-get", {}).then((res) => {
1060
+ if (res && res.ok) {
1061
+ setFiles(res.files || []);
1062
+ setError("");
1063
+ return;
1064
+ }
1065
+ if (res && res.unsupported) {
1066
+ setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1067
+ return;
1068
+ }
1069
+ setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1070
+ }).catch((e) => setError(String(e)));
1071
+ }
1072
+ React.useEffect(() => {
1073
+ load();
1074
+ }, []);
1075
+ if (error) {
1076
+ return React.createElement(
1077
+ "div",
1078
+ { className: "dsh-recall-ex-card" },
1079
+ React.createElement("div", { className: "dsh-recall-ex-note" }, error),
1080
+ React.createElement(
1081
+ "div",
1082
+ { className: "dsh-recall-panel-actions" },
1083
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
1084
+ )
1085
+ );
1086
+ }
1087
+ if (files === null) {
1088
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
1089
+ }
1090
+ if (!files.length) {
1091
+ 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");
1092
+ }
1093
+ return React.createElement(
1094
+ "div",
1095
+ { className: "dsh-recall-ex-card" },
1096
+ ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1097
+ );
1098
+ }
1099
+ function ConfigForm() {
1100
+ const [baseline, setBaseline] = React.useState(null);
1101
+ const [draft, setDraft] = React.useState(null);
1102
+ const [envLocks, setEnvLocks] = React.useState({});
1103
+ const [overridden, setOverridden] = React.useState({});
1104
+ const [writable, setWritable] = React.useState(true);
1105
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
1106
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
1107
+ function load() {
1108
+ api("config-get", {}).then((res) => {
1109
+ if (res && res.ok) {
1110
+ const v = res.values || {};
1111
+ const next = {
1112
+ gcSnaps: String(v.gcSnaps == null ? "" : v.gcSnaps),
1113
+ gcHours: String(v.gcHours == null ? "" : v.gcHours),
1114
+ maxFileBytes: bytesToMb2(v.maxFileBytes),
1115
+ maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? "" : v.maxSnapshotsPerWorkspace),
1116
+ baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join("\n") : "",
1117
+ refillDraft: v.refillDraft !== false,
1118
+ snapshotEnabled: v.snapshotEnabled !== false,
1119
+ archiveOriginal: v.archiveOriginal !== false,
1120
+ retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
1121
+ };
1122
+ setDraft(next);
1123
+ setBaseline(next);
1124
+ setEnvLocks(res.envLocks || {});
1125
+ setOverridden(res.overridden || {});
1126
+ setWritable(res.writable !== false);
1127
+ } else {
1128
+ setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
1129
+ }
1130
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1131
+ }
1132
+ React.useEffect(() => {
1133
+ load();
1134
+ }, []);
1135
+ function edit(key, value) {
1136
+ setDraft((d) => Object.assign({}, d, { [key]: value }));
1137
+ }
1138
+ function save() {
1139
+ if (state.busy || !draft || !baseline) return;
1140
+ const patch = {};
1141
+ for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
1142
+ if (draft[key] !== baseline[key]) patch[key] = draft[key];
1143
+ }
1144
+ if (!Object.keys(patch).length) {
1145
+ setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
1146
+ return;
1147
+ }
1148
+ const clean = {};
1149
+ if (patch.gcSnaps !== void 0) {
1150
+ const n = parseInt(patch.gcSnaps, 10);
1151
+ if (!Number.isFinite(n) || n < 1) {
1152
+ setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1153
+ return;
1154
+ }
1155
+ clean.gcSnaps = n;
1156
+ }
1157
+ if (patch.gcHours !== void 0) {
1158
+ const n = parseInt(patch.gcHours, 10);
1159
+ if (!Number.isFinite(n) || n < 1) {
1160
+ setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1161
+ return;
1162
+ }
1163
+ clean.gcHours = n;
1164
+ }
1165
+ if (patch.maxFileBytes !== void 0) {
1166
+ const mb = Number(patch.maxFileBytes);
1167
+ if (!Number.isFinite(mb) || mb < 0.01) {
1168
+ setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
1169
+ return;
1170
+ }
1171
+ clean.maxFileBytes = Math.round(mb * 1048576);
1172
+ }
1173
+ if (patch.maxSnapshotsPerWorkspace !== void 0) {
1174
+ const n = parseInt(patch.maxSnapshotsPerWorkspace, 10);
1175
+ if (!Number.isFinite(n) || n < 0) {
1176
+ 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 });
1177
+ return;
1178
+ }
1179
+ clean.maxSnapshotsPerWorkspace = n;
1180
+ }
1181
+ if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
1182
+ if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
1183
+ if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
1184
+ if (patch.retentionDays !== void 0) {
1185
+ const n = parseInt(patch.retentionDays, 10);
1186
+ if (!Number.isFinite(n) || n < 0) {
1187
+ setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
1188
+ return;
1189
+ }
1190
+ clean.retentionDays = n;
1191
+ }
1192
+ if (patch.baseExcludes !== void 0) {
1193
+ clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
1194
+ }
1195
+ setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1196
+ api("config-set", { patch: clean }).then((res) => {
1197
+ if (res && res.ok) {
1198
+ setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
1199
+ load();
1200
+ } else {
1201
+ setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1202
+ }
1203
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1204
+ }
1205
+ function numRow(key, label, hint, opts) {
1206
+ const locked = Boolean(envLocks && envLocks[key]);
1207
+ const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
1208
+ return React.createElement(
1209
+ "div",
1210
+ { className: "dsh-recall-cfg-row", key },
1211
+ React.createElement(
1212
+ "div",
1213
+ { className: "dsh-recall-cfg-line" },
1214
+ React.createElement("label", { className: "dsh-recall-cfg-label" }, label),
1215
+ React.createElement("input", {
1216
+ className: "dsh-recall-cfg-input",
1217
+ type: "number",
1218
+ value: draft ? draft[key] : "",
1219
+ disabled: locked || !writable,
1220
+ min: opts && opts.min,
1221
+ step: opts && opts.step,
1222
+ onChange: (e) => edit(key, e.target.value)
1223
+ }),
1224
+ opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
1225
+ changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1226
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
1227
+ locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
1228
+ ),
1229
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
1230
+ );
1231
+ }
1232
+ function resetDefaults() {
1233
+ if (state.busy || !writable) return;
1234
+ setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
1235
+ api("config-reset", {}).then((res) => {
1236
+ if (res && res.ok) {
1237
+ load();
1238
+ setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
1239
+ } else {
1240
+ setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
1241
+ }
1242
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1243
+ }
1244
+ if (!draft) {
1245
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
1246
+ }
1247
+ return React.createElement(
1248
+ "div",
1249
+ { className: "dsh-recall-ex-card" },
1250
+ React.createElement(
1251
+ "div",
1252
+ { className: "dsh-recall-cfg-row", key: "snapshotEnabled" },
1253
+ React.createElement(
1254
+ "div",
1255
+ { className: "dsh-recall-cfg-line" },
1256
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-snapshot" }, "\u542F\u7528\u5FEB\u7167"),
1257
+ React.createElement("input", {
1258
+ id: "dsh-recall-cfg-snapshot",
1259
+ type: "checkbox",
1260
+ checked: Boolean(draft.snapshotEnabled),
1261
+ disabled: !writable,
1262
+ onChange: (e) => edit("snapshotEnabled", e.target.checked)
1263
+ }),
1264
+ draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1265
+ overridden && overridden.snapshotEnabled !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1266
+ ),
1267
+ 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")
1268
+ ),
1269
+ 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 }),
1270
+ 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 }),
1271
+ 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 }),
1272
+ 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 }),
1273
+ 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 }),
1274
+ React.createElement(
1275
+ "div",
1276
+ { className: "dsh-recall-cfg-row", key: "refillDraft" },
1277
+ React.createElement(
1278
+ "div",
1279
+ { className: "dsh-recall-cfg-line" },
1280
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-refill" }, "\u64A4\u56DE\u540E\u56DE\u586B\u8F93\u5165\u6846"),
1281
+ React.createElement("input", {
1282
+ id: "dsh-recall-cfg-refill",
1283
+ type: "checkbox",
1284
+ checked: Boolean(draft.refillDraft),
1285
+ disabled: !writable,
1286
+ onChange: (e) => edit("refillDraft", e.target.checked)
1287
+ }),
1288
+ draft.refillDraft !== baseline.refillDraft ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1289
+ overridden && overridden.refillDraft !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1290
+ ),
1291
+ 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")
1292
+ ),
1293
+ React.createElement(
1294
+ "div",
1295
+ { className: "dsh-recall-cfg-row", key: "archiveOriginal" },
1296
+ React.createElement(
1297
+ "div",
1298
+ { className: "dsh-recall-cfg-line" },
1299
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-archive" }, "\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD"),
1300
+ React.createElement("input", {
1301
+ id: "dsh-recall-cfg-archive",
1302
+ type: "checkbox",
1303
+ checked: Boolean(draft.archiveOriginal),
1304
+ disabled: !writable,
1305
+ onChange: (e) => edit("archiveOriginal", e.target.checked)
1306
+ }),
1307
+ draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1308
+ overridden && overridden.archiveOriginal !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1309
+ ),
1310
+ 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")
1311
+ ),
1312
+ React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
1313
+ showAdvanced ? React.createElement(
1314
+ "div",
1315
+ { className: "dsh-recall-cfg-row", key: "baseExcludes" },
1316
+ React.createElement(
1317
+ "div",
1318
+ { className: "dsh-recall-cfg-line" },
1319
+ React.createElement("label", { className: "dsh-recall-cfg-label" }, "\u57FA\u7840\u6392\u9664\u8868"),
1320
+ draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1321
+ overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1322
+ ),
1323
+ React.createElement("textarea", {
1324
+ className: "dsh-recall-cfg-area",
1325
+ rows: 4,
1326
+ value: draft.baseExcludes,
1327
+ disabled: !writable,
1328
+ onChange: (e) => edit("baseExcludes", e.target.value)
1329
+ }),
1330
+ 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")
1331
+ ) : null,
1332
+ React.createElement(
1333
+ "div",
1334
+ { className: "dsh-recall-panel-actions" },
1335
+ state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1336
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, "\u653E\u5F03\u4FEE\u6539"),
1337
+ React.createElement("button", {
1338
+ type: "button",
1339
+ className: "dsh-recall-btn",
1340
+ disabled: state.busy || !writable,
1341
+ title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
1342
+ onClick: resetDefaults
1343
+ }, "\u6062\u590D\u9ED8\u8BA4"),
1344
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
1345
+ !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
1346
+ )
1347
+ );
1348
+ }
1349
+ function SectionToggle(props) {
1350
+ return React.createElement(
1351
+ "button",
1352
+ {
1353
+ type: "button",
1354
+ className: "dsh-recall-cardbtn",
1355
+ "aria-expanded": props.open,
1356
+ onClick: props.onToggle
1357
+ },
1358
+ React.createElement("span", { className: "dsh-recall-tree-toggle" }, props.open ? "\u25BE" : "\u25B8"),
1359
+ React.createElement("span", { style: { fontWeight: 600, fontSize: "14px", lineHeight: "22px" } }, props.title),
1360
+ props.meta ? React.createElement("span", { className: "dsh-recall-tree-meta" }, props.meta) : null
1361
+ );
1362
+ }
1363
+ function RecallSettingsCard() {
1364
+ const [open, setOpen] = React.useState(false);
1365
+ const [sections, setSections] = React.useState({ exclude: false, manage: false });
1366
+ function toggle(key) {
1367
+ setSections((prev) => Object.assign({}, prev, { [key]: !prev[key] }));
1368
+ }
1369
+ return React.createElement(
1370
+ "li",
1371
+ { className: "dsh-recall-card" + (open ? " dsh-recall-card-open" : "") },
1372
+ React.createElement(
1373
+ "button",
1374
+ {
1375
+ type: "button",
1376
+ className: "dsh-recall-cardbtn",
1377
+ "aria-expanded": open,
1378
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + ": \u64A4\u56DE\u63D2\u4EF6",
1379
+ onClick: () => setOpen((v) => !v)
1380
+ },
1381
+ React.createElement(
1382
+ "span",
1383
+ { className: "dsh-recall-card-head" },
1384
+ React.createElement("span", { className: "dsh-recall-card-name" }, "\u64A4\u56DE\u63D2\u4EF6"),
1385
+ 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")
1386
+ ),
1387
+ React.createElement("svg", {
1388
+ width: 14,
1389
+ height: 14,
1390
+ viewBox: "0 0 16 16",
1391
+ style: { color: "var(--dsw-alias-label-tertiary)", flex: "none", transition: "transform .16s", transform: open ? "rotate(180deg)" : "none" }
1392
+ }, React.createElement("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round" }))
1393
+ ),
1394
+ open ? React.createElement(
1395
+ "div",
1396
+ { className: "dsh-recall-card-body" },
1397
+ React.createElement(ConfigForm),
1398
+ React.createElement(SectionToggle, { title: "\u6392\u9664\u914D\u7F6E\uFF08exclude.txt\uFF09", open: sections.exclude, onToggle: () => toggle("exclude") }),
1399
+ sections.exclude ? React.createElement(ExcludeFilesSection) : null,
1400
+ React.createElement(SectionToggle, { title: "\u5FEB\u7167\u7BA1\u7406", open: sections.manage, onToggle: () => toggle("manage") }),
1401
+ sections.manage ? React.createElement(ManageCard) : null
1402
+ ) : null
1403
+ );
1404
+ }
1405
+ return { RecallSettingsCard };
1406
+ }
1407
+
1408
+ // src/client/app.js
1409
+ function createApp(React) {
1410
+ return function apply(ctx) {
1411
+ const slots = ctx.get("slots");
1412
+ if (!slots) return;
1413
+ const sessionsSvc = ctx.get("sessions");
1414
+ const workspacesSvc = ctx.get("workspaces");
1415
+ const stylesSvc = ctx.get("styles");
1416
+ if (stylesSvc && typeof stylesSvc.insert === "function") {
1417
+ stylesSvc.insert(CSS);
1418
+ } else if (typeof document !== "undefined") {
1419
+ const tag = document.createElement("style");
1420
+ tag.setAttribute("data-plugin", "dsh-recall-plugin");
1421
+ tag.textContent = CSS;
1422
+ document.head.appendChild(tag);
1423
+ }
1424
+ const util = buildUtil();
1425
+ const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc);
1426
+ const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc);
1427
+ for (const slotKey of ["user", "steering"]) {
1428
+ let mounted = false;
1429
+ for (let priority = -1; priority >= -3 && !mounted; priority--) {
1430
+ try {
1431
+ slots.inject("conversation.chat.node", () => slots.register(
1432
+ { name: "conversation.chat.node", key: slotKey, priority },
1433
+ UserRecallNode
1434
+ ));
1435
+ mounted = true;
1436
+ } catch (error) {
1437
+ if (priority === -3) console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
1438
+ }
1439
+ }
1440
+ }
1441
+ try {
1442
+ slots.inject("settings.plugin.item", () => slots.register(
1443
+ { name: "settings.plugin.item", key: "dsh-recall" },
1444
+ RecallSettingsCard
1445
+ ));
1446
+ } catch (error) {
1447
+ console.error("[dsh-recall-plugin] settings card register failed:", error);
1448
+ }
1449
+ };
1450
+ }
1451
+
1452
+ // src/client/entry.js
1453
+ window.__ModuleLoader__.load({
1454
+ id: "dsh-recall-plugin",
1455
+ factory: (require2) => {
1456
+ const React = require2("react");
1457
+ return { name: "dsh-recall-plugin", apply: createApp(React) };
1458
+ }
1459
+ });