dsh-recall-plugin 2.3.19 → 2.3.21

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