dsh-recall-plugin 2.3.0 → 2.3.2

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,56 +1,91 @@
1
- // src/client/css.js
1
+ "use strict";
2
+
3
+ // src/client/css.ts
2
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}",
3
9
  ".dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}",
4
10
  ".dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}",
5
- ".dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}",
6
- ".dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}",
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)}",
7
13
  ".dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}",
8
- ".dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}",
14
+ ".dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:1.5}",
9
15
  ".dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}",
10
16
  ".dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}",
11
17
  "@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}",
12
18
  ".dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}",
13
- ".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
14
- ".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
19
+ ".dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.5}",
20
+ ".dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;word-break:break-word}",
15
21
  ".dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}",
16
- ".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}",
17
- ".dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}",
18
- ".dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}",
22
+ ".dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.5}",
23
+ ".dsh-recall-badge{flex:none;font-size:12px;line-height:1.5;padding:0 6px;border-radius:6px}",
24
+ ".dsh-recall-badge-modified{color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}",
19
25
  ".dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
20
26
  ".dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}",
21
27
  ".dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}",
22
- ".dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}",
23
- ".dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
28
+ // grid-column 对非 grid 祖先(exclude/快照卡片的 flex 布局)自动无效,无害;
29
+ // 在 cfg-grid 内则保证操作区/占满行不被 auto-placement 塞进第一列撑爆列宽。
30
+ ".dsh-recall-panel-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;margin-top:2px;grid-column:1/-1}",
31
+ ".dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
24
32
  ".dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}",
25
- ".dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}",
26
- ".dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}",
27
- ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
33
+ ".dsh-recall-btn:disabled,.dsh-recall-ex-chip:disabled{opacity:.5;cursor:default}",
34
+ ".dsh-recall-btn:disabled:hover,.dsh-recall-ex-chip:disabled:hover{color:var(--dsw-alias-label-secondary)}",
35
+ ".dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-bg-layer-3)}",
36
+ ".dsh-recall-btn-danger:hover{color:var(--dsw-alias-bg-layer-3);filter:var(--dsh-recall-btn-danger-hover)}",
37
+ // 焦点环(V2-5):focus-visible 令牌不存在(核验见计划文档),按官方按钮
38
+ // 惯例用 border-l3 2px 环 + outline:none——border-l2 加深一档即 border-l3,
39
+ // 与官方 settings 卡片按钮 focus 写法逐字一致。
40
+ ".dsh-recall-btn:focus-visible,.dsh-recall-ex-chip:focus-visible,.dsh-recall-tree-toggle:focus-visible,.dsh-recall-cardbtn:focus-visible,.dsh-recall-ex-input:focus-visible,.dsh-recall-cfg-input:focus-visible,.dsh-recall-ex-area:focus-visible,.dsh-recall-cfg-area:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3);outline:none}",
41
+ ".dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:1.5;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}",
28
42
  ".dsh-recall-toast.dsh-recall-toast-in{opacity:1}",
29
43
  ".dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}",
30
44
  ".dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}",
31
- ".dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}",
32
- ".dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}",
33
- ".dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:20px;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}",
45
+ ".dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:1.5}",
46
+ ".dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;word-break:break-word}",
47
+ ".dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:1.5;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}",
34
48
  ".dsh-recall-ex-quick{display:flex;flex-wrap:wrap;gap:8px;align-items:center}",
35
- ".dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:20px}",
36
- ".dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:18px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
49
+ ".dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:1.5}",
50
+ ".dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:1.5;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}",
37
51
  ".dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}",
38
- ".dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
52
+ // V9 确认按钮 danger chip:红色文字 + 官方失败状态行底,与 .btn-danger 同义
53
+ // 但保持 chip 尺寸层级(确认条内部不出现大按钮)
54
+ ".dsh-recall-ex-chip-danger{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger)}",
55
+ ".dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}",
39
56
  ".dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}",
40
57
  ".dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}",
58
+ // V6 健康徽章(git 可用性):pill 配色直接复用官方状态行配对——成功用
59
+ // success-tertiary 底、失败用 interactive-bg-hover-danger 底(error 无
60
+ // tertiary 令牌,官方失败状态行即用此搭配,主题感知)。
61
+ ".dsh-recall-health-pill{display:inline-flex;align-items:center;padding:1px 10px;border-radius:999px;font-size:12px;line-height:1.5}",
62
+ ".dsh-recall-health-pill-ok{background:var(--dsw-alias-state-success-tertiary);color:var(--dsw-alias-state-success-primary)}",
63
+ ".dsh-recall-health-pill-bad{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}",
64
+ // V6 错误区标题:error 色 + 条数,错误不再是灰色小字(fail-loud 可见性)
65
+ ".dsh-recall-errors-title{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:1.5;font-weight:600}",
41
66
  ".dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
42
67
  ".dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}",
43
68
  ".dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}",
44
69
  ".dsh-recall-tree-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
45
- ".dsh-recall-tree-toggle{flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:11px;line-height:18px;user-select:none}",
70
+ // V2 树折叠钮 span→button 的 UA 默认样式重置:button 自带 appearance/背景/边框/
71
+ // 内边距,与 span 形态差异在此抹平,保证纯键盘可达不引入视觉回归。
72
+ ".dsh-recall-tree-toggle{appearance:none;background:0 0;border:0;padding:0;font:inherit;flex:none;width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:4px;font-size:12px;line-height:1.5;user-select:none}",
46
73
  ".dsh-recall-tree-toggle:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}",
47
74
  ".dsh-recall-tree-toggle-placeholder{flex:none;width:18px;height:18px}",
48
- ".dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px;overflow:hidden}",
75
+ ".dsh-recall-tree-label{flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:1.5;overflow:hidden}",
49
76
  ".dsh-recall-tree-name{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}",
50
77
  ".dsh-recall-tree-title{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
51
- ".dsh-recall-tree-meta{flex:none;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
52
- ".dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:16px;border-left:1px solid var(--dsw-alias-border-l1);padding-left:8px}",
53
- ".dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}",
78
+ ".dsh-recall-tree-meta{flex:none;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary);white-space:nowrap}",
79
+ // V4 树缩进契约化:总缩进 = --dsh-recall-tree-indent(折叠钮 18px + gap 6px),
80
+ // children 按 2/3(16px margin)与 1/3(8px padding)拆分,恰好让子行文字
81
+ // 与父行折叠钮右缘对齐——把「16+8 恰等于 18+6」的巧合变成单一事实源。
82
+ ".dsh-recall-tree-children{display:flex;flex-direction:column;gap:1px;margin-left:calc(var(--dsh-recall-tree-indent)*2/3);border-left:1px solid var(--dsw-alias-border-l1);padding-left:calc(var(--dsh-recall-tree-indent)/3);animation:dsh-recall-unfold .16s ease-out}",
83
+ ".dsh-recall-tree-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);animation:dsh-recall-unfold .16s ease-out}",
84
+ // V9 展开动效:opacity + 2px 上移入场(确认条/树展开共用一个 keyframes)。
85
+ // 弃用 max-height 过渡:树展开高度可变(长列表数百 px),固定上限会裁切;
86
+ // opacity+位移给出同等显隐反馈且零裁切风险。
87
+ "@keyframes dsh-recall-unfold{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}",
88
+ "@media (prefers-reduced-motion:reduce){.dsh-recall-tree-children,.dsh-recall-tree-confirm{animation:none}}",
54
89
  ".dsh-recall-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s;display:flex;flex-direction:column;text-align:left}",
55
90
  ".dsh-recall-card.dsh-recall-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
56
91
  ".dsh-recall-card-head{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}",
@@ -58,17 +93,46 @@ var CSS = [
58
93
  ".dsh-recall-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
59
94
  ".dsh-recall-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:flex;flex-direction:column;gap:12px}",
60
95
  ".dsh-recall-cardbtn{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}",
61
- ".dsh-recall-cfg-row{display:flex;flex-direction:column;gap:4px}",
62
- ".dsh-recall-cfg-line{display:flex;align-items:center;gap:8px}",
63
- ".dsh-recall-cfg-label{flex:none;width:130px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}",
96
+ // 折叠头(SectionToggle)复用卡片头 cardbtn:14/16px 内边距在卡片 body 内表现为
97
+ // 16px 左缩进,与分组标题/表单内容错位(用户实测反馈)——body 内覆写为贴左,
98
+ // 卡片头(.dsh-recall-card 直接子级)不受影响。
99
+ ".dsh-recall-card-body .dsh-recall-cardbtn{padding:6px 0}",
100
+ // cfg-grid:ConfigForm 全部行共享的单一 grid 容器。此前每行 cfg-row 是独立
101
+ // grid,「第一列 max-content」各行各算,跨行对齐从未成立(checkbox/输入框
102
+ // 列参差);cfg-row 改 display:contents 透明化后,label/控件行/hint 直接成为
103
+ // 本容器的 item,第一列列宽由全表单最长 label 决定——跨行对齐自此成立。
104
+ ".dsh-recall-cfg-grid{display:grid;grid-template-columns:max-content minmax(0,1fr);column-gap:8px;row-gap:4px;align-items:start}",
105
+ ".dsh-recall-cfg-row{display:contents}",
106
+ ".dsh-recall-cfg-grid > .dsh-recall-cardbtn{grid-column:1/-1}",
107
+ // V5 表单分组小标题:语义分组分隔符,字号与 hint 同级、加粗 + label-secondary
108
+ // 作层级区分,不引入第三层字阶;占满整行,margin-top 补组间分隔(首组清零)。
109
+ ".dsh-recall-cfg-group{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5;font-weight:600;grid-column:1/-1;margin-top:4px}",
110
+ ".dsh-recall-cfg-group:first-child{margin-top:0}",
111
+ // V4 后 cfg-line 只装控件(label 已是 cfg-row 直接子元素,占 grid 第一列 max-content)
112
+ ".dsh-recall-cfg-line{display:flex;align-items:center;gap:8px;min-width:0;grid-column:2}",
113
+ ".dsh-recall-cfg-label{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.5}",
64
114
  ".dsh-recall-cfg-input{flex:1;min-width:0;box-sizing:border-box;font:inherit;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px 8px}",
65
115
  ".dsh-recall-cfg-input:disabled{opacity:.5}",
66
- ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:18px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;min-height:64px}",
67
- ".dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;padding-left:138px}",
68
- ".dsh-recall-cfg-tag{flex:none;font-size:11px;line-height:16px;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}"
116
+ ".dsh-recall-cfg-area{font-family:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;min-height:64px;box-sizing:border-box;width:100%;grid-column:2}",
117
+ ".dsh-recall-cfg-hint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;grid-column:2;padding-bottom:4px}",
118
+ // V4/V8 共用 480px 断点:cfg 表单单列堆叠(V4);exclude quick 行输入框独占
119
+ // 一行(flex:1 1 100%,basis 100% 强制换行,添加按钮与芯片建议另起一行)、
120
+ // panel-actions 长状态文案不再挤压按钮(wrap 已在基础规则,此处无需重复)。
121
+ "@media (max-width:480px){.dsh-recall-cfg-grid{grid-template-columns:minmax(0,1fr);row-gap:2px}.dsh-recall-cfg-line,.dsh-recall-cfg-hint,.dsh-recall-cfg-area{grid-column:auto}.dsh-recall-ex-quick .dsh-recall-ex-input{flex:1 1 100%}}",
122
+ ".dsh-recall-cfg-tag{flex:none;font-size:12px;line-height:1.4;padding:1px 6px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}",
123
+ // 已修改(用户改了配置)用 warn 底色提示「有未保存变更」——修改不是错误,
124
+ // warn 家族来自 V1 核验的官方配对(tertiary 底 + label 文)。
125
+ ".dsh-recall-cfg-tag-modified{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}",
126
+ // 环境变量锁定:系统锁住不可写,区别于「已覆盖」(值被更高优先源覆盖)——
127
+ // 两者同属中性,用左边框做结构级区分,不引入第三色。
128
+ ".dsh-recall-cfg-tag-locked{border-left:2px solid var(--dsw-alias-border-l2)}",
129
+ // V3 快照树加载骨架:items===null 时的 5 条占位行,pulse 明暗呼吸模拟加载。
130
+ ".dsh-recall-tree-skeleton{display:flex;flex-direction:column;gap:2px;padding:4px 0}",
131
+ ".dsh-recall-tree-skeleton-row{height:20px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);animation:dsh-recall-pulse 1.2s ease-in-out infinite}",
132
+ "@keyframes dsh-recall-pulse{0%,100%{opacity:1}50%{opacity:.45}}"
69
133
  ].join("");
70
134
 
71
- // src/client/util.js
135
+ // src/client/util.ts
72
136
  function clockText(ms) {
73
137
  try {
74
138
  if (!ms || isNaN(new Date(ms).getTime())) return "";
@@ -83,10 +147,11 @@ function clockText(ms) {
83
147
  }
84
148
  }
85
149
  function sizeText(bytes) {
86
- if (!bytes || bytes <= 0) return "0 MB";
87
- if (bytes < 1048576) return (bytes / 1024).toFixed(0) + " KB";
88
- if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + " MB";
89
- return (bytes / 1073741824).toFixed(2) + " GB";
150
+ const n = Number(bytes);
151
+ if (!bytes || n <= 0) return "0 MB";
152
+ if (n < 1048576) return (n / 1024).toFixed(0) + " KB";
153
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
154
+ return (n / 1073741824).toFixed(2) + " GB";
90
155
  }
91
156
  function bytesToMb(bytes) {
92
157
  const n = Number(bytes);
@@ -103,15 +168,23 @@ function buildTree(list) {
103
168
  if (!ws.sessions.has(sidKey)) ws.sessions.set(sidKey, { root: ws.root, sessionId: it.sessionId || null, title: it.sessionTitle || null, items: [] });
104
169
  ws.sessions.get(sidKey).items.push(it);
105
170
  }
106
- const wsList = Array.from(workspaces.values());
171
+ const wsList = Array.from(workspaces.values()).map((ws) => ({ root: ws.root, name: ws.name, sessions: Array.from(ws.sessions.values()) }));
107
172
  wsList.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
108
173
  for (const ws of wsList) {
109
- ws.sessions = Array.from(ws.sessions.values());
110
174
  ws.sessions.sort((a, b) => (a.title || "").localeCompare(b.title || ""));
111
175
  for (const s of ws.sessions) s.items.sort((a, b) => (b.time || 0) - (a.time || 0));
112
176
  }
113
177
  return wsList;
114
178
  }
179
+ function useAutoDismissMessage(React, state, setState) {
180
+ React.useEffect(() => {
181
+ if (!state.message || state.error || state.busy) return;
182
+ const timer = setTimeout(() => {
183
+ setState((prev) => prev.message === state.message && !prev.error && !prev.busy ? Object.assign({}, prev, { message: "" }) : prev);
184
+ }, 4e3);
185
+ return () => clearTimeout(timer);
186
+ }, [state.message, state.error, state.busy]);
187
+ }
115
188
  function buildUtil() {
116
189
  function api(name, args) {
117
190
  return fetch("/api/recall/" + name, {
@@ -129,21 +202,24 @@ function buildUtil() {
129
202
  };
130
203
  function messageFor(res, fallback) {
131
204
  if (!res) return fallback;
132
- const code = res && res.code;
133
- if (code && CODE_TEXT[code]) return CODE_TEXT[code];
134
- return res && (res.message || res.error) || fallback;
205
+ const code = res.code;
206
+ if (code && CODE_TEXT[String(code)]) return CODE_TEXT[String(code)];
207
+ const m = res.message;
208
+ const e = res.error;
209
+ return String(m || e || fallback);
135
210
  }
136
211
  const noticeShown = /* @__PURE__ */ new Set();
137
212
  function mountToast(text) {
138
213
  if (typeof document === "undefined") return;
139
214
  try {
140
- let dismiss = function() {
215
+ let dismiss2 = function() {
141
216
  if (dismissed) return;
142
217
  dismissed = true;
143
218
  clearTimeout(timer);
144
219
  el.classList.remove("dsh-recall-toast-in");
145
220
  setTimeout(() => el.remove(), 300);
146
221
  };
222
+ var dismiss = dismiss2;
147
223
  const el = document.createElement("div");
148
224
  el.className = "dsh-recall-toast";
149
225
  const tag = document.createElement("span");
@@ -153,10 +229,10 @@ function buildUtil() {
153
229
  body.textContent = text;
154
230
  el.appendChild(tag);
155
231
  el.appendChild(body);
156
- el.addEventListener("click", () => dismiss(), { once: true });
232
+ el.addEventListener("click", () => dismiss2(), { once: true });
157
233
  document.body.appendChild(el);
158
234
  requestAnimationFrame(() => el.classList.add("dsh-recall-toast-in"));
159
- const timer = setTimeout(dismiss, 7e3);
235
+ const timer = setTimeout(dismiss2, 7e3);
160
236
  let dismissed = false;
161
237
  } catch (e) {
162
238
  }
@@ -183,8 +259,9 @@ function buildUtil() {
183
259
  if (cached) return cached;
184
260
  const done = api("init", { sessionId }).then((res) => {
185
261
  if (res && res.config && typeof res.config === "object") {
186
- if (typeof res.config.refillDraft === "boolean") pluginConfig.refillDraft = res.config.refillDraft;
187
- if (typeof res.config.archiveOriginal === "boolean") pluginConfig.archiveOriginal = res.config.archiveOriginal;
262
+ const cfg = res.config;
263
+ if (typeof cfg.refillDraft === "boolean") pluginConfig.refillDraft = cfg.refillDraft;
264
+ if (typeof cfg.archiveOriginal === "boolean") pluginConfig.archiveOriginal = cfg.archiveOriginal;
188
265
  }
189
266
  const notice = res && res.notice;
190
267
  if (notice && notice.unsupported) {
@@ -231,7 +308,7 @@ function buildUtil() {
231
308
  return { api, messageFor, showNotice, showThrottledToast, ensureInit, clockText, writeClipboard, sizeText, bytesToMb, buildTree, pluginConfig };
232
309
  }
233
310
 
234
- // src/client/recall-node.js
311
+ // src/client/recall-node.ts
235
312
  var KIND_INFO = {
236
313
  modified: { label: "\u4FEE\u6539", cls: "modified" },
237
314
  restored: { label: "\u6062\u590D", cls: "restored" },
@@ -538,10 +615,11 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
538
615
  const closePanel = () => setRecall({ stage: "idle" });
539
616
  const bubbleChildren = [];
540
617
  if (imageBlocks.length && typeof renderMessageImages === "function") {
618
+ const render = renderMessageImages;
541
619
  bubbleChildren.push(React.createElement(
542
620
  React.Fragment,
543
621
  { key: "images" },
544
- renderMessageImages({ images: imageBlocks, align: "end" })
622
+ render({ images: imageBlocks, align: "end" })
545
623
  ));
546
624
  }
547
625
  if (text !== "") bubbleChildren.push(React.createElement("div", { className: "dsh-recall-bubble", key: "text" }, text));
@@ -579,55 +657,298 @@ function buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc) {
579
657
  return { UserRecallNode };
580
658
  }
581
659
 
582
- // src/client/settings-cards.js
583
- function groupByLineage(ids, lineage) {
584
- const childOf = /* @__PURE__ */ new Map();
585
- const childrenOf = /* @__PURE__ */ new Map();
586
- for (const e of lineage || []) {
587
- if (e && e.childId && e.parentId) {
588
- const child = String(e.childId);
589
- const parent = String(e.parentId);
590
- childOf.set(child, parent);
591
- const kids = childrenOf.get(parent) || [];
592
- kids.push(child);
593
- childrenOf.set(parent, kids);
660
+ // src/client/config-card.ts
661
+ function buildConfigForm(React, util, SectionToggle) {
662
+ const { api, bytesToMb: bytesToMb2 } = util;
663
+ function ConfigForm() {
664
+ const [baseline, setBaseline] = React.useState(null);
665
+ const [draft, setDraft] = React.useState(null);
666
+ const [envLocks, setEnvLocks] = React.useState({});
667
+ const [overridden, setOverridden] = React.useState({});
668
+ const [writable, setWritable] = React.useState(true);
669
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
670
+ useAutoDismissMessage(React, state, setState);
671
+ const [showAdvanced, setShowAdvanced] = React.useState(false);
672
+ function load() {
673
+ api("config-get", {}).then((res) => {
674
+ if (res && res.ok) {
675
+ const v = res.values;
676
+ const next = {
677
+ gcSnaps: String(v.gcSnaps == null ? "" : v.gcSnaps),
678
+ gcHours: String(v.gcHours == null ? "" : v.gcHours),
679
+ maxFileBytes: bytesToMb2(v.maxFileBytes),
680
+ maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? "" : v.maxSnapshotsPerWorkspace),
681
+ baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join("\n") : "",
682
+ refillDraft: v.refillDraft !== false,
683
+ snapshotEnabled: v.snapshotEnabled !== false,
684
+ archiveOriginal: v.archiveOriginal !== false,
685
+ retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
686
+ };
687
+ setDraft(next);
688
+ setBaseline(next);
689
+ setEnvLocks(res.envLocks || {});
690
+ setOverridden(res.overridden || {});
691
+ setWritable(res.writable !== false);
692
+ } else {
693
+ setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
694
+ }
695
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
594
696
  }
595
- }
596
- const idSet = new Set((ids || []).map(String));
597
- const result = /* @__PURE__ */ new Map();
598
- const assigned = /* @__PURE__ */ new Set();
599
- for (const id of idSet) {
600
- if (assigned.has(id)) continue;
601
- let root = id;
602
- const seen = /* @__PURE__ */ new Set();
603
- while (childOf.has(root) && idSet.has(childOf.get(root)) && !seen.has(root)) {
604
- seen.add(root);
605
- root = childOf.get(root);
697
+ React.useEffect(() => {
698
+ load();
699
+ }, []);
700
+ function edit(key, value) {
701
+ setDraft((d) => Object.assign({}, d, { [key]: value }));
606
702
  }
607
- const chain = [];
608
- const queue = [root];
609
- while (queue.length) {
610
- const cur = queue.shift();
611
- if (!cur || !idSet.has(cur) || assigned.has(cur)) continue;
612
- chain.push(cur);
613
- assigned.add(cur);
614
- for (const k of childrenOf.get(cur) || []) queue.push(k);
703
+ function save() {
704
+ if (state.busy || !draft || !baseline) return;
705
+ const patch = {};
706
+ for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
707
+ if (draft[key] !== baseline[key]) patch[key] = draft[key];
708
+ }
709
+ if (!Object.keys(patch).length) {
710
+ setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
711
+ return;
712
+ }
713
+ const clean = {};
714
+ if (patch.gcSnaps !== void 0) {
715
+ const n = parseInt(String(patch.gcSnaps), 10);
716
+ if (!Number.isFinite(n) || n < 1) {
717
+ setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
718
+ return;
719
+ }
720
+ clean.gcSnaps = n;
721
+ }
722
+ if (patch.gcHours !== void 0) {
723
+ const n = parseInt(String(patch.gcHours), 10);
724
+ if (!Number.isFinite(n) || n < 1) {
725
+ setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
726
+ return;
727
+ }
728
+ clean.gcHours = n;
729
+ }
730
+ if (patch.maxFileBytes !== void 0) {
731
+ const mb = Number(patch.maxFileBytes);
732
+ if (!Number.isFinite(mb) || mb < 0.01) {
733
+ setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
734
+ return;
735
+ }
736
+ clean.maxFileBytes = Math.round(mb * 1048576);
737
+ }
738
+ if (patch.maxSnapshotsPerWorkspace !== void 0) {
739
+ const n = parseInt(String(patch.maxSnapshotsPerWorkspace), 10);
740
+ if (!Number.isFinite(n) || n < 0) {
741
+ setState({ busy: false, message: "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09", error: true });
742
+ return;
743
+ }
744
+ clean.maxSnapshotsPerWorkspace = n;
745
+ }
746
+ if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
747
+ if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
748
+ if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
749
+ if (patch.retentionDays !== void 0) {
750
+ const n = parseInt(String(patch.retentionDays), 10);
751
+ if (!Number.isFinite(n) || n < 0) {
752
+ setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
753
+ return;
754
+ }
755
+ clean.retentionDays = n;
756
+ }
757
+ if (patch.baseExcludes !== void 0) {
758
+ clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
759
+ }
760
+ setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
761
+ api("config-set", { patch: clean }).then((res) => {
762
+ if (res && res.ok) {
763
+ setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
764
+ load();
765
+ } else {
766
+ setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
767
+ }
768
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
615
769
  }
616
- if (chain.length > 1) {
617
- chain.forEach((sid, i) => result.set(sid, { family: chain, index: i + 1 }));
770
+ function numRow(key, label, hint, opts) {
771
+ const locked = Boolean(envLocks && envLocks[key]);
772
+ const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
773
+ return React.createElement(
774
+ "div",
775
+ { className: "dsh-recall-cfg-row", key },
776
+ // V4:label 上提为 cfg-row 直接子元素——grid 第一列(max-content)跨行
777
+ // 对齐最长标签,消灭 130px 定宽魔法数与 hint 138px 缩进耦合;
778
+ // htmlFor-id 关联不变(V2)
779
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-" + key }, label),
780
+ React.createElement(
781
+ "div",
782
+ { className: "dsh-recall-cfg-line" },
783
+ React.createElement("input", {
784
+ id: "dsh-recall-cfg-" + key,
785
+ className: "dsh-recall-cfg-input",
786
+ type: "number",
787
+ value: draft ? draft[key] : "",
788
+ disabled: locked || !writable,
789
+ min: opts && opts.min,
790
+ step: opts && opts.step,
791
+ onChange: (e) => edit(key, e.target.value)
792
+ }),
793
+ opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
794
+ changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
795
+ overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
796
+ locked ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-locked" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
797
+ ),
798
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
799
+ );
800
+ }
801
+ function resetDefaults() {
802
+ if (state.busy || !writable) return;
803
+ setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
804
+ api("config-reset", {}).then((res) => {
805
+ if (res && res.ok) {
806
+ load();
807
+ setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
808
+ } else {
809
+ setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
810
+ }
811
+ }).catch((e) => setState({ busy: false, message: String(e), error: true }));
812
+ }
813
+ if (!draft || !baseline) {
814
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
618
815
  }
816
+ return React.createElement(
817
+ "div",
818
+ { className: "dsh-recall-ex-card" },
819
+ // 全部行包进单一 cfg-grid:cfg-row 是 display:contents 透明层(css.ts),
820
+ // label/控件行/hint 直接成为同一 grid 的 item,第一列 max-content 由全表单
821
+ // 最长 label 决定——checkbox 与数字输入框的控件列自此跨行对齐(此前每行
822
+ // 是独立 grid 容器,max-content 各算各的,跨行对齐从未成立,实测参差)。
823
+ React.createElement(
824
+ "div",
825
+ { className: "dsh-recall-cfg-grid" },
826
+ // V5 表单分组:9 字段平铺 → 「快照行为 / 自动治理」两组语义分组小标题,
827
+ // 降低认知负担;「高级:基础排除表」沿用 SectionToggle 折叠,不重复加标题。
828
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u5FEB\u7167\u884C\u4E3A"),
829
+ React.createElement(
830
+ "div",
831
+ { className: "dsh-recall-cfg-row", key: "snapshotEnabled" },
832
+ // V4:label 上提(与 numRow 同法)
833
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-snapshot" }, "\u542F\u7528\u5FEB\u7167"),
834
+ React.createElement(
835
+ "div",
836
+ { className: "dsh-recall-cfg-line" },
837
+ React.createElement("input", {
838
+ id: "dsh-recall-cfg-snapshot",
839
+ type: "checkbox",
840
+ checked: Boolean(draft.snapshotEnabled),
841
+ disabled: !writable,
842
+ onChange: (e) => edit("snapshotEnabled", e.target.checked)
843
+ }),
844
+ draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
845
+ overridden && overridden.snapshotEnabled !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
846
+ ),
847
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF08\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09\uFF0C\u9002\u5408\u4E34\u65F6\u7981\u7528\u5FEB\u7167\u7684\u573A\u5408")
848
+ ),
849
+ React.createElement(
850
+ "div",
851
+ { className: "dsh-recall-cfg-row", key: "refillDraft" },
852
+ // V4:label 上提(与 numRow 同法)
853
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-refill" }, "\u64A4\u56DE\u540E\u56DE\u586B\u8F93\u5165\u6846"),
854
+ React.createElement(
855
+ "div",
856
+ { className: "dsh-recall-cfg-line" },
857
+ React.createElement("input", {
858
+ id: "dsh-recall-cfg-refill",
859
+ type: "checkbox",
860
+ checked: Boolean(draft.refillDraft),
861
+ disabled: !writable,
862
+ onChange: (e) => edit("refillDraft", e.target.checked)
863
+ }),
864
+ draft.refillDraft !== baseline.refillDraft ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
865
+ overridden && overridden.refillDraft !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
866
+ ),
867
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u6210\u529F\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846\uFF0C\u65B9\u4FBF\u4FEE\u6539\u540E\u91CD\u65B0\u53D1\u9001")
868
+ ),
869
+ React.createElement(
870
+ "div",
871
+ { className: "dsh-recall-cfg-row", key: "archiveOriginal" },
872
+ // V4:label 上提(与 numRow 同法)
873
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-archive" }, "\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD"),
874
+ React.createElement(
875
+ "div",
876
+ { className: "dsh-recall-cfg-line" },
877
+ React.createElement("input", {
878
+ id: "dsh-recall-cfg-archive",
879
+ type: "checkbox",
880
+ checked: Boolean(draft.archiveOriginal),
881
+ disabled: !writable,
882
+ onChange: (e) => edit("archiveOriginal", e.target.checked)
883
+ }),
884
+ draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
885
+ overridden && overridden.archiveOriginal !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
886
+ ),
887
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u540E\u539F\u4F1A\u8BDD\u4ECE\u5217\u8868\u5F52\u6863\u9690\u85CF\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\uFF1B\u5173\u95ED\u5219\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF0C\u65B9\u4FBF\u5BF9\u7167\u56DE\u9000\u524D\u540E\u7684\u4E0A\u4E0B\u6587")
888
+ ),
889
+ React.createElement("div", { className: "dsh-recall-cfg-group" }, "\u81EA\u52A8\u6CBB\u7406"),
890
+ numRow("gcSnaps", "gc \u89E6\u53D1\u6761\u6570", "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 git gc", { min: 1, step: 1 }),
891
+ numRow("gcHours", "gc \u89E6\u53D1\u5C0F\u65F6", "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF08\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1\uFF09", { min: 1, step: 1 }),
892
+ numRow("maxFileBytes", "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650", "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\uFF08\u5355\u4F4D MB\uFF0C\u652F\u6301\u5C0F\u6570\uFF09", { suffix: "MB", min: 0.01, step: 0.5 }),
893
+ numRow("maxSnapshotsPerWorkspace", "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650", "\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u9650\u5236", { min: 0, step: 1 }),
894
+ numRow("retentionDays", "\u5FEB\u7167\u4FDD\u7559\u5929\u6570", "\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u542F\u7528\uFF08\u4E0E\u5FEB\u7167\u603B\u6570\u4E0A\u9650\u5404\u81EA\u751F\u6548\uFF09", { min: 0, step: 1 }),
895
+ // 操作区在「高级」折叠头之前:按钮服务整个表单(含折叠区之外的字段),
896
+ // 排在折叠头之后会被误读为折叠区内容、折叠时像漏收起(用户实测反馈);
897
+ // 因此也不把按钮藏进折叠分支——否则折叠高级区后将无法保存。
898
+ React.createElement(
899
+ "div",
900
+ { className: "dsh-recall-panel-actions" },
901
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
902
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(baseline ? Object.assign({}, baseline) : null) }, "\u653E\u5F03\u4FEE\u6539"),
903
+ React.createElement("button", {
904
+ type: "button",
905
+ className: "dsh-recall-btn",
906
+ disabled: state.busy || !writable,
907
+ title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
908
+ onClick: resetDefaults
909
+ }, "\u6062\u590D\u9ED8\u8BA4"),
910
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
911
+ !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
912
+ ),
913
+ React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
914
+ showAdvanced ? React.createElement(
915
+ "div",
916
+ { className: "dsh-recall-cfg-row", key: "baseExcludes" },
917
+ // V4:label 上提(与 numRow 同法);cfg-line 只剩 tags,textarea/hint 各自归第二列
918
+ React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-baseExcludes" }, "\u57FA\u7840\u6392\u9664\u8868"),
919
+ React.createElement(
920
+ "div",
921
+ { className: "dsh-recall-cfg-line" },
922
+ draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag dsh-recall-cfg-tag-modified" }, "\u5DF2\u4FEE\u6539") : null,
923
+ overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
924
+ ),
925
+ React.createElement("textarea", {
926
+ id: "dsh-recall-cfg-baseExcludes",
927
+ className: "dsh-recall-cfg-area",
928
+ rows: 4,
929
+ value: draft.baseExcludes,
930
+ disabled: !writable,
931
+ onChange: (e) => edit("baseExcludes", e.target.value)
932
+ }),
933
+ React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5185\u7F6E\u89C4\u5219\uFF0C\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u5171\u4EAB\uFF0C\u5EFA\u8BAE\u4FDD\u6301\u9ED8\u8BA4\uFF1Bgitignore \u8BED\u6CD5\u6BCF\u884C\u4E00\u6761\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u91CC\u7684 exclude.txt\uFF08S3-2 \u6298\u53E0\uFF09")
934
+ ) : null
935
+ )
936
+ );
619
937
  }
620
- return result;
938
+ return { ConfigForm };
621
939
  }
622
- function buildSettingsCards(React, util, sessionsSvc) {
623
- const { api, clockText: clockText2, sizeText: sizeText2, bytesToMb: bytesToMb2, buildTree: buildTree2 } = util;
624
- const EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
940
+
941
+ // src/client/exclude-card.ts
942
+ var EXCLUDE_SUGGESTIONS = ["dist/", "build/", "out/", "coverage/", "*.log", ".env"];
943
+ function buildExcludeCards(React, util) {
944
+ const { api } = util;
625
945
  function ExcludeCard(props) {
626
946
  const file = props.file;
627
947
  const [draft, setDraft] = React.useState(file.content || "");
628
948
  const [baseline, setBaseline] = React.useState(file.content || "");
629
949
  const [quick, setQuick] = React.useState("");
630
950
  const [state, setState] = React.useState({ busy: false, message: "", error: false });
951
+ useAutoDismissMessage(React, state, setState);
631
952
  const dirty = draft !== baseline;
632
953
  function appendPattern(pattern) {
633
954
  setDraft((d) => (d && !d.endsWith("\n") ? d + "\n" : d) + pattern + "\n");
@@ -671,6 +992,7 @@ function buildSettingsCards(React, util, sessionsSvc) {
671
992
  React.createElement("div", { className: "dsh-recall-ex-note" }, "gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF0C\u652F\u6301 # \u6CE8\u91CA\uFF1B\u547D\u4E2D\u6392\u9664\u7684\u6587\u4EF6\u4E0E\u76EE\u5F55\u4E0D\u8FDB\u5165\u5FEB\u7167\uFF0C\u4E5F\u4E0D\u4F1A\u88AB\u56DE\u9000\u89E6\u78B0\u3002"),
672
993
  React.createElement("textarea", {
673
994
  className: "dsh-recall-ex-area",
995
+ "aria-label": "\u5FEB\u7167\u6392\u9664\u6A21\u5F0F\u5217\u8868\uFF08gitignore \u8BED\u6CD5\uFF0C\u4E00\u884C\u4E00\u6761\uFF09",
674
996
  value: draft,
675
997
  spellCheck: false,
676
998
  onChange: (e) => setDraft(e.target.value)
@@ -682,6 +1004,7 @@ function buildSettingsCards(React, util, sessionsSvc) {
682
1004
  className: "dsh-recall-ex-input",
683
1005
  value: quick,
684
1006
  placeholder: "\u8F93\u5165\u8DEF\u5F84\u6216\u6A21\u5F0F\uFF0C\u56DE\u8F66\u5FEB\u901F\u6DFB\u52A0",
1007
+ "aria-label": "\u5FEB\u901F\u6DFB\u52A0\u6392\u9664\u6A21\u5F0F",
685
1008
  onChange: (e) => setQuick(e.target.value),
686
1009
  onKeyDown: (e) => {
687
1010
  if (e.key === "Enter") {
@@ -702,51 +1025,141 @@ function buildSettingsCards(React, util, sessionsSvc) {
702
1025
  React.createElement(
703
1026
  "div",
704
1027
  { className: "dsh-recall-panel-actions" },
705
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1028
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
706
1029
  React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: discard }, "\u653E\u5F03\u4FEE\u6539"),
707
- React.createElement("button", { type: "button", className: "dsh-recall-btn dsh-recall-btn-danger", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
1030
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: !dirty || state.busy, onClick: save }, "\u4FDD\u5B58")
708
1031
  )
709
1032
  );
710
1033
  }
711
- function ManageCard(props) {
712
- const [items, setItems] = React.useState(null);
713
- const [usage, setUsage] = React.useState(null);
714
- const [errors, setErrors] = React.useState(null);
715
- const [state, setState] = React.useState({ busy: false, message: "", error: false });
716
- const [limit, setLimit] = React.useState(200);
717
- const [total, setTotal] = React.useState(0);
718
- const [health, setHealth] = React.useState(null);
719
- const [query, setQuery] = React.useState("");
720
- const [showAllErrors, setShowAllErrors] = React.useState(false);
721
- const [titlesPending, setTitlesPending] = React.useState(false);
722
- const [lineage, setLineage] = React.useState([]);
723
- function fetchTitles(list) {
724
- const missing = Array.from(new Set(
725
- (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
726
- )).slice(0, 100);
727
- if (!missing.length) {
728
- setTitlesPending(false);
729
- return;
730
- }
731
- setTitlesPending(true);
732
- api("manage", { op: "titles", sessionIds: missing }).then((res) => {
733
- const map = res && res.ok ? res.titles : null;
734
- if (map) {
735
- setItems((prev) => (prev || []).map((it) => it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it));
1034
+ function ExcludeFilesSection() {
1035
+ const [files, setFiles] = React.useState(null);
1036
+ const [error, setError] = React.useState("");
1037
+ function load() {
1038
+ api("exclude-get", {}).then((res) => {
1039
+ if (res && res.ok) {
1040
+ setFiles(res.files || []);
1041
+ setError("");
1042
+ return;
736
1043
  }
737
- setTitlesPending(false);
738
- }).catch(() => setTitlesPending(false));
739
- }
740
- function fetchMessages(list) {
741
- const requests = (list || []).filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, "messageText")).map((it) => ({ sessionId: it.sessionId, messageId: it.id })).slice(0, 200);
742
- if (!requests.length) return;
743
- api("manage", { op: "messages", requests }).then((res) => {
744
- const map = res && res.ok ? res.messageTexts : null;
745
- if (map) {
746
- setItems((prev) => (prev || []).map((it) => it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it));
1044
+ if (res && res.unsupported) {
1045
+ setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1046
+ return;
747
1047
  }
748
- }).catch(() => {
749
- });
1048
+ setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1049
+ }).catch((e) => setError(String(e)));
1050
+ }
1051
+ React.useEffect(() => {
1052
+ load();
1053
+ }, []);
1054
+ if (error) {
1055
+ return React.createElement(
1056
+ "div",
1057
+ { className: "dsh-recall-ex-card" },
1058
+ React.createElement("div", { className: "dsh-recall-ex-note" }, error),
1059
+ React.createElement(
1060
+ "div",
1061
+ { className: "dsh-recall-panel-actions" },
1062
+ React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
1063
+ )
1064
+ );
1065
+ }
1066
+ if (files === null) {
1067
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
1068
+ }
1069
+ if (!files.length) {
1070
+ return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002");
1071
+ }
1072
+ return React.createElement(
1073
+ "div",
1074
+ { className: "dsh-recall-ex-card" },
1075
+ ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1076
+ );
1077
+ }
1078
+ return { ExcludeFilesSection };
1079
+ }
1080
+
1081
+ // src/client/snapshot-manager.ts
1082
+ function groupByLineage(ids, lineage) {
1083
+ const childOf = /* @__PURE__ */ new Map();
1084
+ const childrenOf = /* @__PURE__ */ new Map();
1085
+ for (const e of lineage || []) {
1086
+ if (e && e.childId && e.parentId) {
1087
+ const child = String(e.childId);
1088
+ const parent = String(e.parentId);
1089
+ childOf.set(child, parent);
1090
+ const kids = childrenOf.get(parent) || [];
1091
+ kids.push(child);
1092
+ childrenOf.set(parent, kids);
1093
+ }
1094
+ }
1095
+ const idSet = new Set((ids || []).map((v) => String(v)));
1096
+ const result = /* @__PURE__ */ new Map();
1097
+ const assigned = /* @__PURE__ */ new Set();
1098
+ for (const id of idSet) {
1099
+ if (assigned.has(id)) continue;
1100
+ let root = id;
1101
+ const seen = /* @__PURE__ */ new Set();
1102
+ while (childOf.has(root) && idSet.has(childOf.get(root)) && !seen.has(root)) {
1103
+ seen.add(root);
1104
+ root = childOf.get(root);
1105
+ }
1106
+ const chain = [];
1107
+ const queue = [root];
1108
+ while (queue.length) {
1109
+ const cur = queue.shift();
1110
+ if (!cur || !idSet.has(cur) || assigned.has(cur)) continue;
1111
+ chain.push(cur);
1112
+ assigned.add(cur);
1113
+ for (const k of childrenOf.get(cur) || []) queue.push(k);
1114
+ }
1115
+ if (chain.length > 1) {
1116
+ chain.forEach((sid, i) => result.set(sid, { family: chain, index: i + 1 }));
1117
+ }
1118
+ }
1119
+ return result;
1120
+ }
1121
+ function buildSnapshotManager(React, util, sessionsSvc) {
1122
+ const { api, clockText: clockText2, sizeText: sizeText2, buildTree: buildTree2 } = util;
1123
+ function ManageCard() {
1124
+ const [items, setItems] = React.useState(null);
1125
+ const [usage, setUsage] = React.useState(null);
1126
+ const [errors, setErrors] = React.useState(null);
1127
+ const [state, setState] = React.useState({ busy: false, message: "", error: false });
1128
+ useAutoDismissMessage(React, state, setState);
1129
+ const [limit, setLimit] = React.useState(200);
1130
+ const [total, setTotal] = React.useState(0);
1131
+ const [health, setHealth] = React.useState(null);
1132
+ const [query, setQuery] = React.useState("");
1133
+ const [showAllErrors, setShowAllErrors] = React.useState(false);
1134
+ const [titlesPending, setTitlesPending] = React.useState(false);
1135
+ const [lineage, setLineage] = React.useState([]);
1136
+ function fetchTitles(list) {
1137
+ const missing = Array.from(new Set(
1138
+ (list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
1139
+ )).slice(0, 100);
1140
+ if (!missing.length) {
1141
+ setTitlesPending(false);
1142
+ return;
1143
+ }
1144
+ setTitlesPending(true);
1145
+ api("manage", { op: "titles", sessionIds: missing }).then((res) => {
1146
+ const map = res && res.ok ? res.titles : null;
1147
+ if (map) {
1148
+ setItems((prev) => (prev || []).map((it) => it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it));
1149
+ }
1150
+ setTitlesPending(false);
1151
+ }).catch(() => setTitlesPending(false));
1152
+ }
1153
+ function fetchMessages(list) {
1154
+ const requests = (list || []).filter((it) => it.sessionId && it.id && !Object.prototype.hasOwnProperty.call(it, "messageText")).map((it) => ({ sessionId: it.sessionId, messageId: it.id })).slice(0, 200);
1155
+ if (!requests.length) return;
1156
+ api("manage", { op: "messages", requests }).then((res) => {
1157
+ const map = res && res.ok ? res.messageTexts : null;
1158
+ if (map) {
1159
+ setItems((prev) => (prev || []).map((it) => it.id && Object.prototype.hasOwnProperty.call(map, it.id) ? Object.assign({}, it, { messageText: map[it.id] }) : it));
1160
+ }
1161
+ }).catch(() => {
1162
+ });
750
1163
  }
751
1164
  function refresh(overLimit) {
752
1165
  const useLimit = overLimit || limit;
@@ -810,7 +1223,8 @@ function buildSettingsCards(React, util, sessionsSvc) {
810
1223
  setState({ busy: true, message: "\u6267\u884C\u4E2D\u2026", error: false });
811
1224
  api("manage", Object.assign({ op }, extra || {})).then((res) => {
812
1225
  if (res && res.ok) {
813
- setState({ busy: false, message: typeof res.deleted === "number" ? "\u5DF2\u5220\u9664 " + res.deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
1226
+ const deleted = res.deleted;
1227
+ setState({ busy: false, message: typeof deleted === "number" ? "\u5DF2\u5220\u9664 " + deleted + " \u6761\u5FEB\u7167" : doneText, error: false });
814
1228
  refresh();
815
1229
  } else {
816
1230
  setState({ busy: false, message: res && (res.message || res.error) || "\u64CD\u4F5C\u5931\u8D25", error: true });
@@ -819,23 +1233,26 @@ function buildSettingsCards(React, util, sessionsSvc) {
819
1233
  }
820
1234
  const [expanded, setExpanded] = React.useState(() => /* @__PURE__ */ new Set());
821
1235
  const [confirming, setConfirming] = React.useState(null);
822
- function renderDeleteAllConfirm() {
823
- if (!confirming || confirming.kind !== "all") return null;
1236
+ function ConfirmRow(props) {
824
1237
  return React.createElement(
825
1238
  "div",
826
1239
  { className: "dsh-recall-tree-confirm" },
827
- "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
828
- React.createElement("button", {
829
- type: "button",
830
- className: "dsh-recall-btn dsh-recall-btn-danger",
831
- onClick: () => {
832
- setConfirming(null);
833
- run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
834
- }
835
- }, "\u786E\u8BA4\u5168\u90E8\u5220\u9664"),
836
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
1240
+ props.text,
1241
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip dsh-recall-ex-chip-danger", onClick: props.onConfirm }, "\u786E\u8BA4"),
1242
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: props.onCancel }, "\u53D6\u6D88")
837
1243
  );
838
1244
  }
1245
+ function renderDeleteAllConfirm() {
1246
+ if (!confirming || confirming.kind !== "all") return null;
1247
+ return React.createElement(ConfirmRow, {
1248
+ text: "\u786E\u8BA4\u5220\u9664\u6240\u6709\u5DE5\u4F5C\u533A\u7684\u5168\u90E8\u5FEB\u7167\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002",
1249
+ onConfirm: () => {
1250
+ setConfirming(null);
1251
+ run("deleteAll", {}, "\u5DF2\u6E05\u7A7A\u5168\u90E8\u5FEB\u7167");
1252
+ },
1253
+ onCancel: () => setConfirming(null)
1254
+ });
1255
+ }
839
1256
  function toggle(key) {
840
1257
  setExpanded((prev) => {
841
1258
  const next = new Set(prev);
@@ -854,7 +1271,8 @@ function buildSettingsCards(React, util, sessionsSvc) {
854
1271
  let listById = null;
855
1272
  try {
856
1273
  if (sessionsSvc && sessionsSvc.list && typeof sessionsSvc.list.getSnapshot === "function") {
857
- listById = sessionsSvc.list.getSnapshot().byId || null;
1274
+ const snapshot = sessionsSvc.list.getSnapshot();
1275
+ listById = snapshot && snapshot.byId || null;
858
1276
  }
859
1277
  } catch (e) {
860
1278
  listById = null;
@@ -864,21 +1282,15 @@ function buildSettingsCards(React, util, sessionsSvc) {
864
1282
  }
865
1283
  function renderConfirm(kind, key, extra, text) {
866
1284
  if (!confirming || confirming.kind !== kind || confirming.key !== key) return null;
867
- return React.createElement(
868
- "div",
869
- { className: "dsh-recall-tree-confirm" },
1285
+ return React.createElement(ConfirmRow, {
870
1286
  text,
871
- React.createElement("button", {
872
- type: "button",
873
- className: "dsh-recall-ex-chip",
874
- onClick: () => {
875
- const c = confirming;
876
- setConfirming(null);
877
- run("delete", c.extra, "\u5DF2\u5220\u9664");
878
- }
879
- }, "\u786E\u8BA4"),
880
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setConfirming(null) }, "\u53D6\u6D88")
881
- );
1287
+ onConfirm: () => {
1288
+ const c = confirming;
1289
+ setConfirming(null);
1290
+ run("delete", c.extra, "\u5DF2\u5220\u9664");
1291
+ },
1292
+ onCancel: () => setConfirming(null)
1293
+ });
882
1294
  }
883
1295
  function renderLeaf(it) {
884
1296
  const key = "snap-" + it.id;
@@ -919,8 +1331,13 @@ function buildSettingsCards(React, util, sessionsSvc) {
919
1331
  React.createElement(
920
1332
  "div",
921
1333
  { className: "dsh-recall-tree-row" },
922
- React.createElement("span", {
1334
+ // V2:折叠钮 span→button——Tab/Enter/Space 可达,读屏经 aria-expanded
1335
+ // 与 aria-label 播报展开语义与节点名;CSS 已做 button 重置防视觉回归。
1336
+ React.createElement("button", {
1337
+ type: "button",
923
1338
  className: "dsh-recall-tree-toggle",
1339
+ "aria-expanded": open,
1340
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + label,
924
1341
  onClick: () => toggle(key)
925
1342
  }, open ? "\u25BE" : "\u25B8"),
926
1343
  React.createElement(
@@ -963,8 +1380,12 @@ function buildSettingsCards(React, util, sessionsSvc) {
963
1380
  React.createElement(
964
1381
  "div",
965
1382
  { className: "dsh-recall-tree-row" },
966
- React.createElement("span", {
1383
+ // V2:工作区折叠钮同 renderSession——span→button 键盘化,aria 语义并列播报
1384
+ React.createElement("button", {
1385
+ type: "button",
967
1386
  className: "dsh-recall-tree-toggle",
1387
+ "aria-expanded": open,
1388
+ "aria-label": (open ? "\u6536\u8D77" : "\u5C55\u5F00") + "\uFF1A" + ws.name,
968
1389
  onClick: () => toggle(key)
969
1390
  }, open ? "\u25BE" : "\u25B8"),
970
1391
  React.createElement(
@@ -1002,29 +1423,55 @@ function buildSettingsCards(React, util, sessionsSvc) {
1002
1423
  { className: "dsh-recall-ex-note" },
1003
1424
  usage === null ? countText + "\u3002" : countText + "\uFF0C\u5168\u90E8\u5DE5\u4F5C\u533A\u5FEB\u7167\u5B58\u50A8\u5360\u7528 " + sizeText2(usage) + "\u3002"
1004
1425
  ),
1426
+ // V6 健康行徽章化:git 状态用彩色 pill(成功/失败,官方状态行配对),
1427
+ // 从普通 note 提升为卡片顶部横幅(渲染在搜索框与树之前);存储计数
1428
+ // 维持文字,避免 pill 堆叠丢失信息。
1005
1429
  health ? React.createElement(
1006
1430
  "div",
1007
1431
  { className: "dsh-recall-ex-note", key: "health" },
1008
1432
  React.createElement("span", {
1009
- className: health.gitAvailable ? "" : "dsh-recall-ex-status-error"
1433
+ className: "dsh-recall-health-pill " + (health.gitAvailable ? "dsh-recall-health-pill-ok" : "dsh-recall-health-pill-bad"),
1434
+ title: "\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git"
1010
1435
  }, health.gitAvailable ? "git \u53EF\u7528" : "git \u4E0D\u53EF\u7528\uFF08\u5FEB\u7167\u5F15\u64CE\u4F9D\u8D56 git\uFF09"),
1011
1436
  " \xB7 \u5FEB\u7167\u5B58\u50A8\uFF1Ahome " + health.homeStores + " \u4E2A\u5DE5\u4F5C\u533A" + (health.fallbackStores ? "\uFF0C\u964D\u7EA7 " + health.fallbackStores + " \u4E2A" : "")
1012
1437
  ) : null,
1013
1438
  React.createElement("input", {
1014
1439
  className: "dsh-recall-ex-input",
1015
1440
  placeholder: "\u641C\u7D22\u5DE5\u4F5C\u533A / \u4F1A\u8BDD\u6807\u9898 / \u6D88\u606F\u5185\u5BB9 / ID",
1441
+ "aria-label": "\u641C\u7D22\u5FEB\u7167",
1016
1442
  value: query,
1017
1443
  spellCheck: false,
1018
1444
  onChange: (e) => setQuery(e.target.value)
1019
1445
  }),
1446
+ // V3 加载骨架:items===null 表示首查未回——用 5 条 pulse 灰条占位替代
1447
+ // 打开快照管理时的一段空白;aria-hidden 纯装饰不打扰读屏
1448
+ items === null ? React.createElement(
1449
+ "div",
1450
+ { className: "dsh-recall-tree-skeleton", "aria-hidden": true },
1451
+ ...[1, 2, 3, 4, 5].map((n) => React.createElement("div", { key: "sk-" + n, className: "dsh-recall-tree-skeleton-row" }))
1452
+ ) : null,
1020
1453
  treeNodes.length > 0 ? React.createElement("div", { className: "dsh-recall-tree" }, ...treeNodes) : null,
1021
1454
  items && items.length === 0 && !q ? React.createElement("div", { className: "dsh-recall-ex-note", key: "empty" }, "\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u5FEB\u7167\u3002") : null,
1022
1455
  q && filteredItems && filteredItems.length === 0 ? React.createElement("div", { className: "dsh-recall-ex-note", key: "no-match" }, "\u65E0\u5339\u914D\u5FEB\u7167") : null,
1023
1456
  renderDeleteAllConfirm(),
1457
+ // V6:错误区从卡片最底上移到操作区上方(fail-loud 可见性);标题
1458
+ // error 色带条数徽章,时间戳格式维持原样
1459
+ errors && errors.length > 0 ? React.createElement(
1460
+ "div",
1461
+ { key: "errors" },
1462
+ React.createElement("div", { className: "dsh-recall-errors-title" }, "\u6700\u8FD1\u9519\u8BEF (" + errors.length + ")"),
1463
+ (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message)),
1464
+ React.createElement(
1465
+ "div",
1466
+ { className: "dsh-recall-panel-actions" },
1467
+ errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u90E8 (" + errors.length + ")") : null,
1468
+ React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
1469
+ )
1470
+ ) : null,
1024
1471
  React.createElement(
1025
1472
  "div",
1026
1473
  { className: "dsh-recall-panel-actions" },
1027
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1474
+ state.message ? React.createElement("span", { role: "status", "aria-live": "polite", className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, (state.error ? "\u9519\u8BEF\uFF1A" : "") + state.message) : null,
1028
1475
  limit < total ? React.createElement("button", {
1029
1476
  type: "button",
1030
1477
  className: "dsh-recall-btn",
@@ -1032,333 +1479,31 @@ function buildSettingsCards(React, util, sessionsSvc) {
1032
1479
  onClick: loadMore
1033
1480
  }, "\u52A0\u8F7D\u66F4\u591A") : null,
1034
1481
  React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy, onClick: refresh }, "\u5237\u65B0"),
1035
- React.createElement("button", {
1036
- type: "button",
1037
- className: "dsh-recall-btn dsh-recall-btn-danger",
1038
- disabled: state.busy,
1039
- title: "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
1040
- onClick: () => setConfirming({ kind: "all" })
1041
- }, "\u5168\u90E8\u5220\u9664"),
1042
1482
  React.createElement("button", {
1043
1483
  type: "button",
1044
1484
  className: "dsh-recall-btn",
1045
1485
  disabled: state.busy,
1046
1486
  title: "\u7ACB\u5373\u5BF9\u5168\u90E8\u5DE5\u4F5C\u533A\u6267\u884C\u4E00\u6B21 git gc\uFF08\u538B\u7F29\u5BF9\u8C61\u5E93\u91CA\u653E\u7A7A\u95F4\uFF09",
1047
1487
  onClick: () => run("gc", {}, "gc \u5B8C\u6210")
1048
- }, "\u7ACB\u5373 gc")
1049
- ),
1050
- errors && errors.length > 0 ? React.createElement(
1051
- "div",
1052
- { className: "dsh-recall-ex-note", key: "errors" },
1053
- React.createElement(
1054
- "div",
1055
- { className: "dsh-recall-ex-status" },
1056
- "\u6700\u8FD1\u9519\u8BEF\uFF1A",
1057
- (showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement("div", { key: i, className: "dsh-recall-ex-note" }, clockText2(e.time) + " " + e.message))
1058
- ),
1059
- React.createElement(
1060
- "div",
1061
- { className: "dsh-recall-panel-actions" },
1062
- errors.length > 5 ? React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? "\u6536\u8D77" : "\u5C55\u5F00\u5168\u90E8 (" + errors.length + ")") : null,
1063
- React.createElement("button", { type: "button", className: "dsh-recall-ex-chip", onClick: clearErrors }, "\u6E05\u7A7A")
1064
- )
1065
- ) : null
1066
- );
1067
- }
1068
- function ExcludeFilesSection() {
1069
- const [files, setFiles] = React.useState(null);
1070
- const [error, setError] = React.useState("");
1071
- function load() {
1072
- api("exclude-get", {}).then((res) => {
1073
- if (res && res.ok) {
1074
- setFiles(res.files || []);
1075
- setError("");
1076
- return;
1077
- }
1078
- if (res && res.unsupported) {
1079
- setError("\u5F53\u524D\u5E73\u53F0\u4E0D\u652F\u6301\u5FEB\u7167\u529F\u80FD\uFF0C\u6392\u9664\u914D\u7F6E\u4E0D\u53EF\u7528\u3002");
1080
- return;
1081
- }
1082
- setError(res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u6392\u9664\u914D\u7F6E");
1083
- }).catch((e) => setError(String(e)));
1084
- }
1085
- React.useEffect(() => {
1086
- load();
1087
- }, []);
1088
- if (error) {
1089
- return React.createElement(
1090
- "div",
1091
- { className: "dsh-recall-ex-card" },
1092
- React.createElement("div", { className: "dsh-recall-ex-note" }, error),
1093
- React.createElement(
1094
- "div",
1095
- { className: "dsh-recall-panel-actions" },
1096
- React.createElement("button", { type: "button", className: "dsh-recall-btn", onClick: load }, "\u91CD\u8BD5")
1097
- )
1098
- );
1099
- }
1100
- if (files === null) {
1101
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u6B63\u5728\u52A0\u8F7D\u6392\u9664\u914D\u7F6E\u2026");
1102
- }
1103
- if (!files.length) {
1104
- return React.createElement("div", { className: "dsh-recall-ex-note" }, "\u5C1A\u672A\u521B\u5EFA\u4EFB\u4F55\u5FEB\u7167\u5B58\u50A8\uFF1A\u5728\u4EFB\u610F\u5DE5\u4F5C\u533A\u53D1\u9001\u4E00\u6761\u6D88\u606F\u540E\uFF0C\u8FD9\u91CC\u4F1A\u51FA\u73B0\u53EF\u7F16\u8F91\u7684\u6392\u9664\u914D\u7F6E\u3002");
1105
- }
1106
- return React.createElement(
1107
- "div",
1108
- { className: "dsh-recall-ex-card" },
1109
- ...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
1110
- );
1111
- }
1112
- function ConfigForm() {
1113
- const [baseline, setBaseline] = React.useState(null);
1114
- const [draft, setDraft] = React.useState(null);
1115
- const [envLocks, setEnvLocks] = React.useState({});
1116
- const [overridden, setOverridden] = React.useState({});
1117
- const [writable, setWritable] = React.useState(true);
1118
- const [state, setState] = React.useState({ busy: false, message: "", error: false });
1119
- const [showAdvanced, setShowAdvanced] = React.useState(false);
1120
- function load() {
1121
- api("config-get", {}).then((res) => {
1122
- if (res && res.ok) {
1123
- const v = res.values || {};
1124
- const next = {
1125
- gcSnaps: String(v.gcSnaps == null ? "" : v.gcSnaps),
1126
- gcHours: String(v.gcHours == null ? "" : v.gcHours),
1127
- maxFileBytes: bytesToMb2(v.maxFileBytes),
1128
- maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? "" : v.maxSnapshotsPerWorkspace),
1129
- baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join("\n") : "",
1130
- refillDraft: v.refillDraft !== false,
1131
- snapshotEnabled: v.snapshotEnabled !== false,
1132
- archiveOriginal: v.archiveOriginal !== false,
1133
- retentionDays: String(v.retentionDays == null ? "" : v.retentionDays)
1134
- };
1135
- setDraft(next);
1136
- setBaseline(next);
1137
- setEnvLocks(res.envLocks || {});
1138
- setOverridden(res.overridden || {});
1139
- setWritable(res.writable !== false);
1140
- } else {
1141
- setState({ busy: false, message: res && (res.message || res.error) || "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E", error: true });
1142
- }
1143
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1144
- }
1145
- React.useEffect(() => {
1146
- load();
1147
- }, []);
1148
- function edit(key, value) {
1149
- setDraft((d) => Object.assign({}, d, { [key]: value }));
1150
- }
1151
- function save() {
1152
- if (state.busy || !draft || !baseline) return;
1153
- const patch = {};
1154
- for (const key of ["gcSnaps", "gcHours", "maxFileBytes", "maxSnapshotsPerWorkspace", "baseExcludes", "refillDraft", "snapshotEnabled", "archiveOriginal", "retentionDays"]) {
1155
- if (draft[key] !== baseline[key]) patch[key] = draft[key];
1156
- }
1157
- if (!Object.keys(patch).length) {
1158
- setState({ busy: false, message: "\u6CA1\u6709\u4FEE\u6539", error: false });
1159
- return;
1160
- }
1161
- const clean = {};
1162
- if (patch.gcSnaps !== void 0) {
1163
- const n = parseInt(patch.gcSnaps, 10);
1164
- if (!Number.isFinite(n) || n < 1) {
1165
- setState({ busy: false, message: "\u5FEB\u7167\u6761\u6570\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1166
- return;
1167
- }
1168
- clean.gcSnaps = n;
1169
- }
1170
- if (patch.gcHours !== void 0) {
1171
- const n = parseInt(patch.gcHours, 10);
1172
- if (!Number.isFinite(n) || n < 1) {
1173
- setState({ busy: false, message: "gc \u5C0F\u65F6\u9608\u503C\u5FC5\u987B\u662F >= 1 \u7684\u6574\u6570", error: true });
1174
- return;
1175
- }
1176
- clean.gcHours = n;
1177
- }
1178
- if (patch.maxFileBytes !== void 0) {
1179
- const mb = Number(patch.maxFileBytes);
1180
- if (!Number.isFinite(mb) || mb < 0.01) {
1181
- setState({ busy: false, message: "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650\u81F3\u5C11 0.01 MB", error: true });
1182
- return;
1183
- }
1184
- clean.maxFileBytes = Math.round(mb * 1048576);
1185
- }
1186
- if (patch.maxSnapshotsPerWorkspace !== void 0) {
1187
- const n = parseInt(patch.maxSnapshotsPerWorkspace, 10);
1188
- if (!Number.isFinite(n) || n < 0) {
1189
- setState({ busy: false, message: "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u9650\u5236\uFF09", error: true });
1190
- return;
1191
- }
1192
- clean.maxSnapshotsPerWorkspace = n;
1193
- }
1194
- if (patch.refillDraft !== void 0) clean.refillDraft = Boolean(patch.refillDraft);
1195
- if (patch.snapshotEnabled !== void 0) clean.snapshotEnabled = Boolean(patch.snapshotEnabled);
1196
- if (patch.archiveOriginal !== void 0) clean.archiveOriginal = Boolean(patch.archiveOriginal);
1197
- if (patch.retentionDays !== void 0) {
1198
- const n = parseInt(patch.retentionDays, 10);
1199
- if (!Number.isFinite(n) || n < 0) {
1200
- setState({ busy: false, message: "\u4FDD\u7559\u5929\u6570\u5FC5\u987B\u662F >= 0 \u7684\u6574\u6570\uFF080 \u8868\u793A\u4E0D\u542F\u7528\uFF09", error: true });
1201
- return;
1202
- }
1203
- clean.retentionDays = n;
1204
- }
1205
- if (patch.baseExcludes !== void 0) {
1206
- clean.baseExcludes = String(patch.baseExcludes).split("\n").map((l) => l.trim()).filter(Boolean);
1207
- }
1208
- setState({ busy: true, message: "\u4FDD\u5B58\u4E2D\u2026", error: false });
1209
- api("config-set", { patch: clean }).then((res) => {
1210
- if (res && res.ok) {
1211
- setState({ busy: false, message: "\u5DF2\u4FDD\u5B58\u5E76\u5373\u65F6\u751F\u6548", error: false });
1212
- load();
1213
- } else {
1214
- setState({ busy: false, message: res && (res.message || res.error) || "\u4FDD\u5B58\u5931\u8D25", error: true });
1215
- }
1216
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1217
- }
1218
- function numRow(key, label, hint, opts) {
1219
- const locked = Boolean(envLocks && envLocks[key]);
1220
- const changed = Boolean(draft && baseline && draft[key] !== baseline[key]);
1221
- return React.createElement(
1222
- "div",
1223
- { className: "dsh-recall-cfg-row", key },
1224
- React.createElement(
1225
- "div",
1226
- { className: "dsh-recall-cfg-line" },
1227
- React.createElement("label", { className: "dsh-recall-cfg-label" }, label),
1228
- React.createElement("input", {
1229
- className: "dsh-recall-cfg-input",
1230
- type: "number",
1231
- value: draft ? draft[key] : "",
1232
- disabled: locked || !writable,
1233
- min: opts && opts.min,
1234
- step: opts && opts.step,
1235
- onChange: (e) => edit(key, e.target.value)
1236
- }),
1237
- opts && opts.suffix ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, opts.suffix) : null,
1238
- changed && !locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1239
- overridden && overridden[key] !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null,
1240
- locked ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u73AF\u5883\u53D8\u91CF\u9501\u5B9A") : null
1241
- ),
1242
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, hint)
1243
- );
1244
- }
1245
- function resetDefaults() {
1246
- if (state.busy || !writable) return;
1247
- setState({ busy: true, message: "\u6062\u590D\u9ED8\u8BA4\u4E2D\u2026", error: false });
1248
- api("config-reset", {}).then((res) => {
1249
- if (res && res.ok) {
1250
- load();
1251
- setState({ busy: false, message: "\u5DF2\u6062\u590D\u9ED8\u8BA4\u503C", error: false });
1252
- } else {
1253
- setState({ busy: false, message: res && (res.message || res.error) || "\u6062\u590D\u9ED8\u8BA4\u5931\u8D25", error: true });
1254
- }
1255
- }).catch((e) => setState({ busy: false, message: String(e), error: true }));
1256
- }
1257
- if (!draft) {
1258
- return React.createElement("div", { className: "dsh-recall-ex-note" }, state.message || "\u6B63\u5728\u8BFB\u53D6\u914D\u7F6E\u2026");
1259
- }
1260
- return React.createElement(
1261
- "div",
1262
- { className: "dsh-recall-ex-card" },
1263
- React.createElement(
1264
- "div",
1265
- { className: "dsh-recall-cfg-row", key: "snapshotEnabled" },
1266
- React.createElement(
1267
- "div",
1268
- { className: "dsh-recall-cfg-line" },
1269
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-snapshot" }, "\u542F\u7528\u5FEB\u7167"),
1270
- React.createElement("input", {
1271
- id: "dsh-recall-cfg-snapshot",
1272
- type: "checkbox",
1273
- checked: Boolean(draft.snapshotEnabled),
1274
- disabled: !writable,
1275
- onChange: (e) => edit("snapshotEnabled", e.target.checked)
1276
- }),
1277
- draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1278
- overridden && overridden.snapshotEnabled !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1279
- ),
1280
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5173\u95ED\u540E\u4E0D\u518D\u65B0\u5EFA\u5FEB\u7167\uFF08\u5DF2\u6709\u5FEB\u7167\u4ECD\u53EF\u64A4\u56DE\uFF09\uFF0C\u9002\u5408\u4E34\u65F6\u7981\u7528\u5FEB\u7167\u7684\u573A\u5408")
1281
- ),
1282
- numRow("gcSnaps", "gc \u89E6\u53D1\u6761\u6570", "\u6BCF\u79EF\u7D2F\u591A\u5C11\u6761\u5FEB\u7167\u89E6\u53D1\u4E00\u6B21 git gc", { min: 1, step: 1 }),
1283
- numRow("gcHours", "gc \u89E6\u53D1\u5C0F\u65F6", "\u8DDD\u4E0A\u6B21 gc \u8D85\u8FC7\u591A\u5C11\u5C0F\u65F6\u89E6\u53D1\uFF08\u4E0E\u6761\u6570\u5148\u5230\u5148\u89E6\u53D1\uFF09", { min: 1, step: 1 }),
1284
- numRow("maxFileBytes", "\u6587\u4EF6\u5927\u5C0F\u4E0A\u9650", "\u8D85\u8FC7\u8BE5\u5927\u5C0F\u7684\u6587\u4EF6\u4E0D\u8FDB\u5FEB\u7167\u3001\u4E0D\u88AB\u56DE\u9000\u89E6\u78B0\uFF08\u5355\u4F4D MB\uFF0C\u652F\u6301\u5C0F\u6570\uFF09", { suffix: "MB", min: 0.01, step: 0.5 }),
1285
- numRow("maxSnapshotsPerWorkspace", "\u5FEB\u7167\u603B\u91CF\u4E0A\u9650", "\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u4FDD\u7559\u7684\u6700\u5927\u5FEB\u7167\u6570\uFF0C\u8D85\u9650\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u9650\u5236", { min: 0, step: 1 }),
1286
- numRow("retentionDays", "\u5FEB\u7167\u4FDD\u7559\u5929\u6570", "\u6309\u5929\u6570\u4FDD\u7559\u5FEB\u7167\uFF0C\u8D85\u671F\u81EA\u52A8\u5220\u9664\u6700\u65E7\u7684\uFF1B\u586B 0 \u8868\u793A\u4E0D\u542F\u7528\uFF08\u4E0E\u5FEB\u7167\u603B\u6570\u4E0A\u9650\u5404\u81EA\u751F\u6548\uFF09", { min: 0, step: 1 }),
1287
- React.createElement(
1288
- "div",
1289
- { className: "dsh-recall-cfg-row", key: "refillDraft" },
1290
- React.createElement(
1291
- "div",
1292
- { className: "dsh-recall-cfg-line" },
1293
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-refill" }, "\u64A4\u56DE\u540E\u56DE\u586B\u8F93\u5165\u6846"),
1294
- React.createElement("input", {
1295
- id: "dsh-recall-cfg-refill",
1296
- type: "checkbox",
1297
- checked: Boolean(draft.refillDraft),
1298
- disabled: !writable,
1299
- onChange: (e) => edit("refillDraft", e.target.checked)
1300
- }),
1301
- draft.refillDraft !== baseline.refillDraft ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1302
- overridden && overridden.refillDraft !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1303
- ),
1304
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u6210\u529F\u540E\u628A\u88AB\u64A4\u56DE\u7684\u6D88\u606F\u6587\u672C\u56DE\u586B\u5230\u8F93\u5165\u6846\uFF0C\u65B9\u4FBF\u4FEE\u6539\u540E\u91CD\u65B0\u53D1\u9001")
1305
- ),
1306
- React.createElement(
1307
- "div",
1308
- { className: "dsh-recall-cfg-row", key: "archiveOriginal" },
1309
- React.createElement(
1310
- "div",
1311
- { className: "dsh-recall-cfg-line" },
1312
- React.createElement("label", { className: "dsh-recall-cfg-label", htmlFor: "dsh-recall-cfg-archive" }, "\u64A4\u56DE\u540E\u5F52\u6863\u539F\u4F1A\u8BDD"),
1313
- React.createElement("input", {
1314
- id: "dsh-recall-cfg-archive",
1315
- type: "checkbox",
1316
- checked: Boolean(draft.archiveOriginal),
1317
- disabled: !writable,
1318
- onChange: (e) => edit("archiveOriginal", e.target.checked)
1319
- }),
1320
- draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1321
- overridden && overridden.archiveOriginal !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1322
- ),
1323
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u64A4\u56DE\u540E\u539F\u4F1A\u8BDD\u4ECE\u5217\u8868\u5F52\u6863\u9690\u85CF\uFF08\u53EF\u4ECE\u5F52\u6863\u627E\u56DE\uFF09\uFF1B\u5173\u95ED\u5219\u4FDD\u7559\u5728\u5217\u8868\u4E2D\uFF0C\u65B9\u4FBF\u5BF9\u7167\u56DE\u9000\u524D\u540E\u7684\u4E0A\u4E0B\u6587")
1324
- ),
1325
- React.createElement(SectionToggle, { title: "\u9AD8\u7EA7\uFF1A\u57FA\u7840\u6392\u9664\u8868", open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
1326
- showAdvanced ? React.createElement(
1327
- "div",
1328
- { className: "dsh-recall-cfg-row", key: "baseExcludes" },
1329
- React.createElement(
1330
- "div",
1331
- { className: "dsh-recall-cfg-line" },
1332
- React.createElement("label", { className: "dsh-recall-cfg-label" }, "\u57FA\u7840\u6392\u9664\u8868"),
1333
- draft.baseExcludes !== baseline.baseExcludes ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u4FEE\u6539") : null,
1334
- overridden && overridden.baseExcludes !== void 0 ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u5DF2\u8986\u76D6") : null
1335
- ),
1336
- React.createElement("textarea", {
1337
- className: "dsh-recall-cfg-area",
1338
- rows: 4,
1339
- value: draft.baseExcludes,
1340
- disabled: !writable,
1341
- onChange: (e) => edit("baseExcludes", e.target.value)
1342
- }),
1343
- React.createElement("div", { className: "dsh-recall-cfg-hint" }, "\u5185\u7F6E\u89C4\u5219\uFF0C\u6BCF\u4E2A\u5DE5\u4F5C\u533A\u5171\u4EAB\uFF0C\u5EFA\u8BAE\u4FDD\u6301\u9ED8\u8BA4\uFF1Bgitignore \u8BED\u6CD5\u6BCF\u884C\u4E00\u6761\uFF0C\u4F18\u5148\u7EA7\u4F4E\u4E8E\u300C\u6392\u9664\u914D\u7F6E\u300D\u91CC\u7684 exclude.txt\uFF08S3-2 \u6298\u53E0\uFF09")
1344
- ) : null,
1345
- React.createElement(
1346
- "div",
1347
- { className: "dsh-recall-panel-actions" },
1348
- state.message ? React.createElement("span", { className: "dsh-recall-ex-status" + (state.error ? " dsh-recall-ex-status-error" : " dsh-recall-ex-status-success") }, state.message) : null,
1349
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, "\u653E\u5F03\u4FEE\u6539"),
1488
+ }, "\u7ACB\u5373 gc"),
1489
+ // V5:全部删除固定为操作区最后一个按钮(排在立即 gc 之后)——即使
1490
+ // 「加载更多」出现/消失也不漂移;danger 与普通按钮间由 panel-actions
1491
+ // 统一 gap:8px 形成视觉分组
1350
1492
  React.createElement("button", {
1351
1493
  type: "button",
1352
- className: "dsh-recall-btn",
1353
- disabled: state.busy || !writable,
1354
- title: "\u628A\u6240\u6709\u5B57\u6BB5\u6062\u590D\u5230\u63D2\u4EF6\u51FA\u5382\u9ED8\u8BA4\u503C",
1355
- onClick: resetDefaults
1356
- }, "\u6062\u590D\u9ED8\u8BA4"),
1357
- React.createElement("button", { type: "button", className: "dsh-recall-btn", disabled: state.busy || !writable, onClick: save }, "\u4FDD\u5B58"),
1358
- !writable ? React.createElement("span", { className: "dsh-recall-cfg-tag" }, "\u53EA\u8BFB\u8BBE\u7F6E\u6E90") : null
1494
+ className: "dsh-recall-btn dsh-recall-btn-danger",
1495
+ disabled: state.busy,
1496
+ title: "\u5220\u9664\u5168\u90E8\u5DE5\u4F5C\u533A\u7684\u6240\u6709\u5FEB\u7167\uFF1B\u4F1A\u76F4\u63A5\u6838\u5BF9\u5E76\u5220\u9664 git tag\uFF08\u5373\u4F7F\u5217\u8868\u4E3A\u7A7A\u4E5F\u53EF\u6E05\u7406\u6B8B\u7559\uFF09",
1497
+ onClick: () => setConfirming({ kind: "all" })
1498
+ }, "\u5168\u90E8\u5220\u9664")
1359
1499
  )
1360
1500
  );
1361
1501
  }
1502
+ return { ManageCard };
1503
+ }
1504
+
1505
+ // src/client/settings-cards.ts
1506
+ function buildSettingsCards(React, util, sessionsSvc) {
1362
1507
  function SectionToggle(props) {
1363
1508
  return React.createElement(
1364
1509
  "button",
@@ -1369,10 +1514,13 @@ function buildSettingsCards(React, util, sessionsSvc) {
1369
1514
  onClick: props.onToggle
1370
1515
  },
1371
1516
  React.createElement("span", { className: "dsh-recall-tree-toggle" }, props.open ? "\u25BE" : "\u25B8"),
1372
- React.createElement("span", { style: { fontWeight: 600, fontSize: "14px", lineHeight: "22px" } }, props.title),
1517
+ React.createElement("span", { className: "dsh-recall-ex-title" }, props.title),
1373
1518
  props.meta ? React.createElement("span", { className: "dsh-recall-tree-meta" }, props.meta) : null
1374
1519
  );
1375
1520
  }
1521
+ const { ConfigForm } = buildConfigForm(React, util, SectionToggle);
1522
+ const { ExcludeFilesSection } = buildExcludeCards(React, util);
1523
+ const { ManageCard } = buildSnapshotManager(React, util, sessionsSvc);
1376
1524
  function RecallSettingsCard() {
1377
1525
  const [open, setOpen] = React.useState(false);
1378
1526
  const [sections, setSections] = React.useState({ exclude: false, manage: false });
@@ -1418,7 +1566,17 @@ function buildSettingsCards(React, util, sessionsSvc) {
1418
1566
  return { RecallSettingsCard };
1419
1567
  }
1420
1568
 
1421
- // src/client/app.js
1569
+ // src/client/app.ts
1570
+ function nextShadowPriority(entries, key) {
1571
+ let priority = -1;
1572
+ for (const entry of Array.isArray(entries) ? entries : []) {
1573
+ if (!entry || !entry.options || entry.options.key !== key) continue;
1574
+ const p = entry.options.priority;
1575
+ const occupied = typeof p === "number" && Number.isFinite(p) ? p : 0;
1576
+ if (occupied <= priority) priority = occupied - 1;
1577
+ }
1578
+ return priority;
1579
+ }
1422
1580
  function createApp(React) {
1423
1581
  return function apply(ctx) {
1424
1582
  const slots = ctx.slots;
@@ -1438,17 +1596,16 @@ function createApp(React) {
1438
1596
  const { UserRecallNode } = buildRecallNode(React, util, ctx, sessionsSvc, workspacesSvc);
1439
1597
  const { RecallSettingsCard } = buildSettingsCards(React, util, sessionsSvc);
1440
1598
  for (const slotKey of ["user", "steering"]) {
1441
- let mounted = false;
1442
- for (let priority = -1; priority >= -3 && !mounted; priority--) {
1443
- try {
1444
- slots.inject("conversation.chat.node", () => slots.register(
1599
+ try {
1600
+ slots.inject("conversation.chat.node", () => {
1601
+ const priority = nextShadowPriority(slots.entries("conversation.chat.node"), slotKey);
1602
+ return slots.register(
1445
1603
  { name: "conversation.chat.node", key: slotKey, priority },
1446
1604
  UserRecallNode
1447
- ));
1448
- mounted = true;
1449
- } catch (error) {
1450
- if (priority === -3) console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
1451
- }
1605
+ );
1606
+ });
1607
+ } catch (error) {
1608
+ console.error("[dsh-recall-plugin] slot register failed (" + slotKey + "):", error);
1452
1609
  }
1453
1610
  }
1454
1611
  try {
@@ -1462,7 +1619,7 @@ function createApp(React) {
1462
1619
  };
1463
1620
  }
1464
1621
 
1465
- // src/client/entry.js
1622
+ // src/client/entry.ts
1466
1623
  window.__ModuleLoader__.load({
1467
1624
  id: "dsh-recall-plugin",
1468
1625
  factory: (require2) => {