dsh-plugin-tool-management 0.20.3 → 0.21.1

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +9 -5
  3. package/README_EN.md +9 -5
  4. package/docs/images/icon.png +0 -0
  5. package/docs/images/icon.svg +1 -0
  6. package/docs/update.md +2 -0
  7. package/lib/client.js +829 -153
  8. package/lib/compat/capability-specs.js +60 -1
  9. package/lib/compat/host-contracts.js +72 -0
  10. package/lib/compat/host-probe.js +59 -0
  11. package/lib/compat/host-roots.js +1 -1
  12. package/lib/compat/probe.js +84 -13
  13. package/lib/context-inject-frame.js +168 -29
  14. package/lib/context-inject.js +13 -10
  15. package/lib/http-api.js +3 -2
  16. package/lib/http-fence.js +26 -1
  17. package/lib/index.js +34 -7
  18. package/lib/mcp/manager.js +6 -1
  19. package/lib/mcp/state-section.js +75 -34
  20. package/lib/memories/constants.js +39 -7
  21. package/lib/memories/projection.js +18 -18
  22. package/lib/memories/service.js +8 -7
  23. package/lib/memories/snapshot.js +39 -21
  24. package/lib/op-registry.js +3 -0
  25. package/lib/ops/compat.js +64 -2
  26. package/lib/ops/memory.js +2 -2
  27. package/lib/ops/prompts.js +30 -2
  28. package/lib/ops/trash.js +12 -1
  29. package/lib/request-gate.js +5 -4
  30. package/lib/settings-sidecars.js +37 -0
  31. package/lib/sidebar-settings.js +13 -0
  32. package/lib/skills/catalog.js +2 -2
  33. package/lib/subagents/catalog.js +4 -4
  34. package/lib/subagents/child-preset.js +120 -0
  35. package/lib/subagents/persona-parse.js +9 -9
  36. package/lib/subagents/run-provider.js +218 -62
  37. package/lib/subagents/tool-filter.js +12 -12
  38. package/lib/subagents/tools.js +29 -22
  39. package/lib/tools/deps.js +4 -4
  40. package/lib/tools/mcp.js +34 -34
  41. package/lib/tools/memory.js +34 -34
  42. package/lib/tools/prompt.js +5 -5
  43. package/lib/tools/remove.js +17 -17
  44. package/lib/tools/scene.js +66 -66
  45. package/lib/tools/skills.js +21 -21
  46. package/lib/tools/subagent.js +13 -13
  47. package/lib/tools/table.js +2 -2
  48. package/locale/en.json +6 -0
  49. package/locale/zh.json +6 -0
  50. package/package.json +28 -24
package/lib/client.js CHANGED
@@ -110,7 +110,11 @@ window.__ModuleLoader__.load({
110
110
  .dsm-btn.dsm-btn-icon{width:24px;min-width:24px;min-height:24px;padding:0}
111
111
  /* 目录卡片右侧动作组:间距收窄一档,配合图标化后不再显挤。 */
112
112
  /* 卡片头里的标签抬到 11px:左侧「N 个技能」是 12px,10px 的标签在这里显得忽大忽小。 */
113
- .dsm-source-head .dsm-tag{font-size:12px;min-height:20px;padding:0 8px}
113
+ .dsm-source-head .dsm-tag{font-size:12px;line-height:18px;min-height:20px;padding:0 8px}
114
+ /* 卡头里的按钮与标题同用 18px 行高:字号不同(11/12/13px)时只靠居中,文字基线会各差
115
+ 一两像素,一行里看着"没对齐"(用户 2026-10-18)。 */
116
+ .dsm-source-head .dsm-btn{line-height:18px}
117
+ .dsm-path,.dsm-count,.dsm-source-title{line-height:18px}
114
118
  /* 「N 个附件」标签:和 tag 同形,但它是**能点的**(点了去编辑弹窗的附件区),
115
119
  所以要把 button 的默认外观抹掉,并给一个可点的悬停反馈 —— 光靠颜色区分不出"能不能点"。 */
116
120
  button.dsm-tag{background:transparent;font:inherit;font-size:12px;cursor:pointer}
@@ -133,6 +137,10 @@ button.dsm-qty:disabled{cursor:default}
133
137
  淡到读不出的话,用户只会以为这两颗按钮没了。危险项只有红字、没有红框 —— 边框是"常驻控件"
134
138
  的语言,收在菜单里的动作不配再占一份视觉重量。 */
135
139
  .dsm-rowmenu{display:flex;flex:none;align-items:center}
140
+ /* 场景卡里那颗 ⋯ 与开关同高(用户 2026-10-18:「场景的 switch 按键和展开三个点按钮要
141
+ 上下居中」):⋯ 自身是 26×24、开关 36×20,两个盒子的中线本来就都在卡片的居中线上,
142
+ 差的是 ⋯ 那 24px 的高度把行撑得比开关高一点点 —— 给它固定 20px 高、图标靠行盒居中。 */
143
+ .dsm-scene-tile .dsm-rowmenu-btn{height:20px}
136
144
  .dsm-rowmenu-btn{display:inline-flex;width:26px;height:24px;align-items:center;justify-content:center;padding:0;border:0;border-radius:var(--dsm-radius-s);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}
137
145
  .dsm-rowmenu-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
138
146
  .dsm-rowmenu-btn:disabled{opacity:.45;cursor:default}
@@ -143,7 +151,7 @@ button.dsm-qty:disabled{cursor:default}
143
151
  .dsm-select-menu .dsm-option-danger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}
144
152
  /* 未启用场景卡里的开关跟着灰(.dsm-rule-off 已把名称与描述压成弱色,原先漏了开关):
145
153
  整卡置灰却留一颗亮绿,读着像"开着却没用",两条口径打架。 */
146
- .dsm-rule-off .dsm-switch-on{background:var(--dsw-alias-border-l3)}
154
+ .dsm-rule-off .dsm-switch[aria-checked=true]{background:var(--dsw-alias-border-l3)}
147
155
  /* 按钮的"档位"里包含尺寸,别把它当纯修饰色(REDESIGN §5,2026-10-11 切片五定稿):
148
156
  .dsm-btn 实心 30px(本屏主操作)/ .dsm-btn-secondary 描边 30px(常规动作)/
149
157
  .dsm-btn-quiet 描边 24px(次要动作;叠 -danger 即 quiet-danger)。
@@ -175,7 +183,10 @@ button.dsm-qty:disabled{cursor:default}
175
183
  省下的 60px 全部归搜索框(它 flex:1)。 */
176
184
  .dsm-filters{display:flex;gap:8px}.dsm-search{flex:1}.dsm-source-filter{width:150px;flex:none}
177
185
  .dsm-control,.dsm-select-trigger{box-sizing:border-box;width:100%;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dsm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dsm-control{min-height:160px;padding-top:8px;resize:vertical;line-height:20px}.dsm-select{position:relative}.dsm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dsm-select-menu{z-index:40;display:flex;max-height:260px;padding:4px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dsm-option{padding:8px;border:0;border-radius:var(--dsm-radius-s);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dsm-option:hover,.dsm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
178
- .dsm-sources{display:flex;flex-direction:column;gap:8px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 12px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:8px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;font-variant-numeric:tabular-nums;flex:none}.dsm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dsm-source-actions{display:flex;align-items:center;gap:4px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}
186
+ .dsm-sources{display:flex;flex-direction:column;gap:8px}.dsm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2)}.dsm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 12px}.dsm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;flex-wrap:wrap;align-items:center;align-content:center;gap:3px 8px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dsm-source-head:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-source-title{font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsm-count{color:var(--dsw-alias-label-tertiary);font-size:12px;font-variant-numeric:tabular-nums;flex:none}.dsm-path{min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}/* 目录挪到标题下面一行、与标题左端对齐(用户 2026-10-18:「应该开头对齐……放在标题下面
187
+ 是不是更好」):行内右对齐时它的位置随右侧按钮多少而变,几行摞起来参差;单独占一行后
188
+ 每条路径都从同一个 x 起头,也读得下更长的目录。width:100% 负责强制换行。 */
189
+ .dsm-source-head-main>.dsm-path{flex:none;width:100%;margin-left:0;order:99}.dsm-source-actions{display:flex;flex:none;align-items:center;gap:4px;margin-left:8px}.dsm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}
179
190
  /* 计数已经改成「启用/总数」这种自带括号感的写法,卡头 8px 的间距把它推太远,读起来像
180
191
  另一栏;贴回名称后面(用户 2026-10-16)。 */
181
192
  .dsm-count-tight{margin-left:-4px}
@@ -198,7 +209,19 @@ button.dsm-qty:disabled{cursor:default}
198
209
  .dsm-source-body .dsm-source{border:0;border-radius:0;border-bottom:1px solid var(--dsw-alias-border-l1)}
199
210
  .dsm-source-body .dsm-source:last-child{border-bottom:0}
200
211
  .dsm-note{overflow:hidden;margin-top:4px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-tags{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.dsm-tag{display:inline-flex;min-height:20px;align-items:center;padding:0 8px;border:1px solid var(--dsw-alias-border-l3);border-radius:var(--dsm-radius-s);color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dsm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dsm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dsm-tag-warn{border-color:var(--dsm-warn);color:var(--dsm-warn)}
201
- .dsm-switch{position:relative;width:32px;height:18px;flex:none;padding:0;border:0;border-radius:99px;background:var(--dsw-alias-border-l3);cursor:pointer}.dsm-switch:after{position:absolute;top:3px;left:3px;width:12px;height:12px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dsm-switch-on{background:var(--dsw-alias-state-success-primary)}.dsm-switch-on:after{transform:translateX(14px)}.dsm-switch:disabled{opacity:.45;cursor:default}.dsm-trash-count{margin-left:auto;padding:4px 8px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px;font-variant-numeric:tabular-nums}.dsm-empty{padding:24px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-undo{display:flex;align-items:center;gap:8px}.dsm-undo-text{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsm-undo-close{padding:0;border:0;background:transparent;font:inherit;font-size:13px;cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary)}.dsm-undo-close:focus-visible{outline-style:solid;outline-color:var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-width:var(--dsw-focus-ring-width,2px);outline-offset:2px}.dsm-undo.dsm-warning{background:var(--dsm-warn-bg)}.dsm-undo.dsm-warning .dsm-undo-text{font-weight:400}.dsm-undo.dsm-warning .dsm-undo-close{color:var(--dsm-warn)}.dsm-warning{border-color:var(--dsm-warn);color:var(--dsm-warn)}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
212
+ /* 开关与官方同款(用户 2026-10-18:「switch 按键统一和官方一样的模板」→「改成完全一样」):
213
+ 36×20 外壳 + 16px 拇指、内边距 2px、滑块位移 16px、开态用 brand-primary,
214
+ 关态拇指用 switch-thumb —— 尺寸与配色照抄宿主设置面板那颗。
215
+ ⚠️ corner-shape:round 不能省:宿主主题在 :root 上全局设了
216
+ corner-shape:superellipse(1.5)(dsh-client-ui-theme 的 corner_shape_css),
217
+ 官方开关自己用 corner-shape:round 把它按回正圆角 —— 我们漏了这一句,于是同一个
218
+ border-radius 画出来是"方肩"的超级椭圆,看着就和官方那颗不是一个东西。
219
+ aria-checked 是官方判据,本组件的 role=switch 本来就带着它。 */
220
+ .dsm-switch{box-sizing:border-box;position:relative;width:36px;height:20px;flex:none;padding:2px;border:0;border-radius:999px;corner-shape:round;background:var(--dsw-alias-border-l3);cursor:pointer}
221
+ .dsm-switch:after{display:block;width:16px;height:16px;border-radius:50%;corner-shape:round;background:var(--dsw-alias-switch-thumb);content:"";transition:transform .12s ease}
222
+ .dsm-switch[aria-checked=true]{background:var(--dsw-alias-brand-primary)}
223
+ .dsm-switch[aria-checked=true]:after{background:var(--dsw-alias-label-primary-foreground);transform:translate(16px)}
224
+ .dsm-switch:disabled{opacity:.5;cursor:default}.dsm-trash-count{margin-left:auto;padding:4px 8px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px;font-variant-numeric:tabular-nums}.dsm-empty{padding:24px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dsm-feedback{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-undo{display:flex;align-items:center;gap:8px}.dsm-undo-text{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dsm-undo-close{padding:0;border:0;background:transparent;font:inherit;font-size:13px;cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary)}.dsm-undo-close:focus-visible{outline-style:solid;outline-color:var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-width:var(--dsw-focus-ring-width,2px);outline-offset:2px}.dsm-undo.dsm-warning{background:var(--dsm-warn-bg)}.dsm-undo.dsm-warning .dsm-undo-text{font-weight:400}.dsm-undo.dsm-warning .dsm-undo-close{color:var(--dsm-warn)}.dsm-warning{border-color:var(--dsm-warn);color:var(--dsm-warn)}.dsm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
202
225
  .dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:20px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-l);background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-sm{width:min(480px,100%)!important}.dsm-modal-lg,.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-list{width:min(560px,100%)!important;height:min(640px,calc(100vh - 48px))!important;max-height:none!important;overflow:hidden}.dsm-modal-list.dsm-modal-editor{width:min(720px,100%)!important}.dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:8px}.dsm-modal-list .dsm-modal-body{flex:1;overflow:auto;overscroll-behavior:contain}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}
203
226
  /* 头部这一层包住「标题行 + 令牌提示」:提示挂在头部里面(而不是 head 与 body 之间)是为了
204
227
  让 body 的子节点位置稳定 —— 位置一变 React 会把 body 整棵重挂(输入框失焦、下拉归零)。 */
@@ -209,7 +232,14 @@ button.dsm-qty:disabled{cursor:default}
209
232
  /* 条目清单必须自己声明字号:裸 ul/li 会从宿主那里继承 16px 近白正文(兼容页「工具能用 ≠
210
233
  模型知道」三条),比同页的说明文字大两号。口径照 .dsm-help-list。 */
211
234
  .dsm-upload-requirements ul{display:flex;margin:4px 0 0;padding-left:16px;flex-direction:column;gap:4px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}
212
- .dsm-upload-req{margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}.dsm-detail-section{gap:8px}.dsm-detail-path,.dsm-code{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px}.dsm-detail-path{padding:8px 8px;border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}
235
+ .dsm-upload-req{margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px}.dsm-detail-section{gap:8px}
236
+ /* 详情页只读的备注正文(用户 2026-10-18:「详细页只能看」):与配置表同形的一格,
237
+ 但它是文字块不是输入框 —— 换行原样保留,长段落读得开。
238
+ 字色用正文色(用户 2026-10-18:「备注后的字应该是黑色的备注」):它是内容本身,
239
+ 不是"辅助说明",弱色会让备注看着像灰掉的占位。没备注时那一格照样有边框,
240
+ 只是字换成最弱的一档 —— 空状态也说得出口。 */
241
+ .dsm-detail-note{margin:0;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-word}
242
+ .dsm-detail-note-empty{color:var(--dsw-alias-label-tertiary)}.dsm-detail-path,.dsm-code{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px}.dsm-detail-path{padding:8px 8px;border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}
213
243
  /* 技能详情的「路径 + 编辑器查看」同一行(用户 2026-10-12):路径条 flex:1 吃掉剩余宽度,
214
244
  按钮 flex:none 固定不动。按钮那颗 .dsm-btn 自带 white-space:nowrap,长路径自己折行。 */
215
245
  .dsm-detail-path-row{display:flex;min-width:0;align-items:center;gap:8px}
@@ -246,9 +276,10 @@ button.dsm-qty:disabled{cursor:default}
246
276
  「没有滚动条 + 内容缺一截」。列表项的尺寸一律由内容决定,滚动只交给 body 这一层。 */
247
277
  .dsm-modal-list .dsm-modal-body>*{flex:none}
248
278
  .dsm-fm{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1)}.dsm-fm-row{display:grid;grid-template-columns:minmax(96px,150px) minmax(0,1fr);gap:8px;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:18px}.dsm-fm-row:last-child{border-bottom:0}.dsm-fm-key{color:var(--dsw-alias-label-secondary);word-break:break-word}.dsm-fm-raw{margin-left:4px;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px}.dsm-fm-val{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-word;white-space:pre-wrap}
249
- @container(max-width:780px){
250
- .dsm-path{display:none}
251
- }@container(max-width:520px){.dsm-head{flex-direction:column}.dsm-actions{width:100%;margin-left:0}.dsm-actions .dsm-btn{flex:1}.dsm-filters{flex-direction:column}.dsm-source-filter{width:100%}}
279
+ /* 窄面板里**不再隐藏目录**(用户 2026-10-18:「设置页的工具管理不显示路径是怎么回事」):
280
+ 那条 display:none 是目录还挂在行尾、与标题抢宽度时写的;现在它单独占第二行,
281
+ 窄到 700px 也只是自己截断,没有理由整段不显示。 */
282
+ @container(max-width:520px){.dsm-head{flex-direction:column}.dsm-actions{width:100%;margin-left:0}.dsm-actions .dsm-btn{flex:1}.dsm-filters{flex-direction:column}.dsm-source-filter{width:100%}}
252
283
  .dsm-hist-group-head{gap:8px}.dsm-hist-group-extra{display:flex;align-items:center;gap:8px;flex:none;margin-left:4px}.dsm-hist-group-extra .dsm-tag{flex:none}.dsm-hist-dir-missing{flex:none;padding:1px 6px;border:1px solid var(--dsw-alias-state-error-primary);border-radius:var(--dsm-radius-s);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:16px;white-space:nowrap;cursor:default;user-select:none}.dsm-hist-register-path{margin-top:-4px;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code,monospace);font-size:11px;word-break:break-all}/* (.dsm-hist-batch / .dsm-hist-batch-actions 已随批量动作移进列表工具条删除;
253
284
  .dsm-hist-batch-count 仍用于导出弹窗里的「已选 N 项」计数。) */
254
285
  .dsm-hist-batch-count{color:var(--dsw-alias-label-secondary);font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}.dsm-dir-row{display:flex;gap:8px}.dsm-dir-row .dsm-control{flex:1}.dsm-dir-list{display:flex;max-height:220px;flex-direction:column;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1)}.dsm-dir-item{display:flex;width:100%;align-items:center;padding:8px 12px;border:0;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}.dsm-dir-item:last-child{border-bottom:0}.dsm-dir-item:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsm-dir-item::before{content:"📁";margin-right:8px;font-size:12px}
@@ -262,6 +293,9 @@ button.dsm-qty:disabled{cursor:default}
262
293
  .dsm-attach-actions{display:inline-flex;align-items:center;gap:6px}
263
294
  .dsm-attach-cap{max-height:126px;overflow:auto;padding-right:2px}
264
295
  .dsm-attach-cap .dsm-file{gap:6px;padding:5px 8px}
296
+ /* 回收站里技能条目的正文:与技能详情同款的一段代码区,吃掉弹窗剩余高度(用户 2026-10-18:
297
+ 「技能的回收站详细还是挤在一起,不能套用技能详细弹窗吗」)。 */
298
+ .dsm-code-fill{flex:1;min-height:200px;max-height:none}
265
299
  /* 附件小窗里的拖放区:导入弹窗那一块是给整页用的(170px),这一档 140 —— 再矮就不像能拖东西了。 */
266
300
  .dsm-dropzone-sm{min-height:140px;padding:18px}
267
301
  .dsm-toast{position:fixed;right:22px;bottom:22px;z-index:1250;box-sizing:border-box;max-width:min(420px,calc(100vw - 44px));padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;animation:dsm-toast-in 160ms ease-out}@keyframes dsm-toast-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}/* 动效降级:系统开了「减少动态效果」就一律关掉。新增 transition / animation 时记得同步加到这里,
@@ -302,22 +336,48 @@ button.dsm-qty:disabled{cursor:default}
302
336
  /* 当前模式条:只在进入模式后出现(没有条 = 自由模式,不需要常驻的解释文案)。 */
303
337
  .dsm-mode-bar{display:flex;min-width:0;align-items:center;gap:8px;padding:12px 12px;border:1px solid var(--dsw-alias-state-success-primary);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2)}
304
338
  .dsm-mode-bar .dsm-btn{margin-left:auto;flex:none}
339
+ /* 右侧两颗动作(锁定 + 退出模式)成一组、靠右(用户 2026-10-18:「锁定……出现在退出模式
340
+ 旁边」):margin-left:auto 只给这一组,两颗之间按 8px 排。
341
+ 锁定键在这一组里**与「退出模式」同高同宽口径**(用户 2026-10-18:「锁定按钮的大小对齐
342
+ 旁边按键」):quiet 默认 24px 高、8px 内边距,旁边那颗 secondary 是 30px / 12px —— 并排
343
+ 看着像两档按钮。这里只在本组内把它抬到同一档。 */
344
+ .dsm-mode-acts{display:flex;flex:none;align-items:center;gap:8px;margin-left:auto}
345
+ .dsm-mode-acts .dsm-btn{margin-left:0}
346
+ /* 锁定键:与旁边「退出模式」同高同宽口径,字重加粗(用户 2026-10-18:「锁定要加粗」)。
347
+ 底色保持透明 —— 用户 2026-10-18:「锁定键白色部分不用变黄了,鼠标放上去微微变黄就行」:
348
+ 常态是"琥珀字 + 琥珀描边"的描边键,只有悬停才铺一层极淡的琥珀。 */
349
+ .dsm-mode-acts .dsm-btn-quiet{min-height:30px;padding:0 12px;font-size:13px}
350
+ .dsm-mode-acts .dsm-btn-lock{font-weight:600;background:transparent}
351
+ .dsm-mode-acts .dsm-btn-lock:hover:not(:disabled){background:var(--dsm-warn-bg);color:var(--dsm-warn)}
352
+ /* 退出模式是**破坏性/收尾**动作(用户 2026-10-18:「退出模式改成红色字体红色边框」):
353
+ 红字红框的描边键(不是实底 —— 全站实底红只留给确认弹窗里的主操作)。悬停只铺一层
354
+ 极淡的红底,与上面那颗锁定的悬停同一档强度。 */
355
+ .dsm-mode-acts .dsm-btn-secondary{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}
356
+ .dsm-mode-acts .dsm-btn-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);opacity:1}
305
357
  .dsm-mode-dot{width:8px;height:8px;flex:none;border-radius:50%;background:var(--dsw-alias-state-success-primary)}
306
358
  .dsm-mode-main{min-width:0;flex:1}
307
359
  .dsm-mode-title{overflow:hidden;font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}
308
360
  .dsm-mode-sub{overflow:hidden;margin-top:4px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}
309
- .dsm-scenes{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:8px}
310
- .dsm-scene-tile{display:flex;min-width:0;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2)}
361
+ /* 场景**一条一行**(用户 2026-10-18 裁定):原先按 minmax(300px,1fr) 铺网格,名字与标签多的
362
+ 场景被挤成两三行;改成单列之后开关 / 锁定 / ⋯ 一列对齐,扫起来是一张表。 */
363
+ .dsm-scenes{display:flex;flex-direction:column;gap:6px}
364
+ .dsm-scene-tile{display:flex;min-width:0;align-items:center;gap:10px;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-2);padding:8px 10px}
311
365
  .dsm-scene-tile-mode{border-color:var(--dsw-alias-state-success-primary)}
312
- .dsm-scene-tile-head{display:flex;min-width:0;align-items:center;gap:8px;padding:12px 12px 0}
313
- .dsm-scene-tile-name{overflow:hidden;font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}
366
+ /* 左列(名字 + 描述两行)与右侧控件(开关 / ⋯)是**同一条横排**的两个格子(用户 2026-10-18:
367
+ 「场景的 switch 按键和三个点按键没有上下居中」)。描述挪到第二行之后,卡头只剩第一行高,
368
+ 开关若还挂在卡头里,它的中线就贴在上半截 —— 卡片越高偏得越多。改成整张卡横排、右侧控件
369
+ 按卡高中线对齐,和列表行(.dsm-listrow 的 side / actions)是同一种排法。 */
370
+ .dsm-scene-tile-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}
371
+ .dsm-scene-tile-head{display:flex;min-width:0;align-items:center;gap:8px}
372
+ .dsm-scene-tile-name{overflow:hidden;flex:none;max-width:40%;font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}
314
373
  .dsm-scene-tile-key{flex:none;color:var(--dsw-alias-label-tertiary);font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px}
315
374
  .dsm-scene-tile-head .dsm-tag{flex:none}
316
- .dsm-scene-tile-switch{margin-left:auto;flex:none}
317
- /* 描述行是卡片的最后一行(2026-10-16 删掉底部那条 编辑/删除 band,动作进卡头 ⋯),
318
- 所以卡片下内距由它承担。 */
319
- .dsm-scene-tile-desc{overflow:hidden;margin:4px 0 0;padding:0 12px 12px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
320
- @container(max-width:640px){.dsm-scenes{grid-template-columns:1fr}}
375
+ /* 描述固定占**第二行**(用户 2026-10-18:「应该显示在第二行」):此前它挤在名字同一行、
376
+ 窄屏下还被容器查询整段隐藏 —— 而它恰恰是这个场景"是干什么的"唯一说明。 */
377
+ .dsm-scene-tile-desc{overflow:hidden;min-width:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap}
378
+ /* 右侧控件是一列横排的两格(开关 + ⋯),二者与卡片中线对齐(见 .dsm-scene-tile)。 */
379
+ .dsm-scene-tile-side{display:flex;flex:none;align-items:center;gap:8px}
380
+ .dsm-scene-tile-switch{flex:none}
321
381
  .dsm-tools-grid{display:flex;max-height:180px;padding:4px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsm-radius-m);background:var(--dsw-alias-bg-layer-1)}
322
382
  .dsm-tools-grid>*{flex:none}
323
383
  /* ── 高级选项折叠区(人设表单;默认收起,展开后才拉候选数据)────────────────── */
@@ -721,6 +781,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
721
781
  /* 锁定键:文字是 warn 色,边框必须同色 —— 否则「琥珀字 + 灰框」看着像两颗按钮拼的
722
782
  (用户 2026-10-12「锁定的边框应该是字体一样」)。 */
723
783
  .dsm-btn-quiet.dsm-btn-lock{border-color:var(--dsm-warn);background:transparent;color:var(--dsm-warn)}
784
+ /* 悬停时**字色仍是琥珀**(用户 2026-10-18:「锁定键鼠标放上去也应该是黄色字」):
785
+ .dsm-btn-quiet:hover 把字色统一改成正文色,那条通用规则排在后面、权重又只低一级,
786
+ 于是锁键一悬停就变黑 —— 这里把它按同色写回来。
787
+ 2026-10-18 第二轮:底色不再常驻琥珀(「白色部分不用变黄了」),只在悬停铺一层极淡的底,
788
+ 这一条只管"悬停时字别变黑",底由模式横幅那一组按上下文给。 */
789
+ .dsm-btn-quiet.dsm-btn-lock:hover:not(:disabled){background:var(--dsm-warn-bg);color:var(--dsm-warn)}
724
790
  .dsm-bulk-ghost{border: 0; background: none; padding: 0 2px; font: inherit; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); cursor: pointer;}
725
791
  .dsm-file-remove:hover{background: var(--dsw-alias-interactive-bg-hover-danger);}
726
792
  .dsm-select-trigger > span{min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
@@ -1096,8 +1162,29 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1096
1162
  .dsm-listrow-meta-info{flex:none;color:var(--dsw-alias-label-tertiary)}
1097
1163
  /* 技术值(路径 / id)排印(§2.3):与普通文案在字形上区分,不靠颜色。 */
1098
1164
  .dsm-listrow-meta .dsm-mono{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace)}
1165
+ /* 目录与「归档日期」分开读(用户 2026-10-18:「目录栏应该和归档对话区分」):两者原先都是
1166
+ 12px 弱色、只隔一个中间点,长路径读起来跟日期连成一句。目录改用等宽字体 + 更弱的色,
1167
+ 日期保持正文色 —— 一段是"在哪",一段是"什么时候",字形不同就不用再看标点分辨。
1168
+ 字号整体压到 10/11px(用户 2026-10-18:「字体都再缩小」):这一页一屏要扫十几行,
1169
+ 两行字都按正文大小排,行与行之间没有主次、只剩一片字。 */
1170
+ .dsm-listrow-meta-path{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:10px;line-height:15px;color:var(--dsw-alias-label-tertiary)}
1171
+ .dsm-hist-row .dsm-listrow-meta-info{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:15px}
1099
1172
  .dsm-listrow-side{display:flex;flex:none;align-items:center;gap:8px}
1100
1173
  .dsm-listrow-actions{display:flex;flex:none;align-items:center;justify-content:flex-end;gap:4px}
1174
+ /* 归档会话行放宽一档(用户 2026-10-18:「归档会话太拥挤了」):这一页每行是「标题 + 目录 +
1175
+ 日期」两段文字,右端还常驻两颗按钮比别处密。48px 的通用行高在这里把三段挤成一坨 ——
1176
+ 行高抬到 52px、两行之间 4px,逐行读开。 */
1177
+ .dsm-hist-row{min-height:52px;padding:6px 12px}
1178
+ .dsm-hist-row .dsm-listrow-main{gap:4px}
1179
+ /* 层级靠字号、不靠加粗(用户 2026-10-18:「归档会话的标题不用加粗,放大字体就行」→
1180
+ 「字体都再缩小」):标题去掉 600,字号与正文齐平,靠上面那行目录的小字拉开主次。 */
1181
+ .dsm-hist-row .dsm-listrow-name{font-size:13px;font-weight:400}
1182
+ /* 分组卡头(工作区目录)与其他页的收缩栏同款(用户 2026-10-18:「会话目录栏感觉太粗太高了,
1183
+ 改成和其他收缩栏一样」):48px、标题 13px/600 —— 上一版为它单独加高的 64px / 15px/700
1184
+ 已撤掉,技能 / MCP / 记忆三页的组头就是这套尺寸。 */
1185
+ /* 行内按钮**保留描边**(用户 2026-10-18:「归档会话的恢复和永久删除边框怎么不见了」):
1186
+ 上一版为了"降噪"把它们去掉了边框,结果两颗键成了裸文字,跟旁边的目录文字混成一片 ——
1187
+ 在行尾它们本来就是按钮,边框是"这是能点的"最直接的信号。降噪改由行高与字号承担。 */
1101
1188
  /* 行2 之下的追加内容(体检结果 / ⚠ 提示 / 被覆盖说明)原本按表格网格留白,
1102
1189
  两行制里由主列 gap 接管,各自的 margin-top 归零。 */
1103
1190
  .dsm-listrow .dsm-row-hint,.dsm-listrow .dsm-rule-hint,.dsm-listrow .dsm-rule-shadow-hint{margin-top:0}
@@ -1143,6 +1230,10 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1143
1230
  .dsm-mode-caret,.dsm-pick-caret{width:12px;height:12px;flex:none;color:var(--dsw-alias-label-tertiary)}
1144
1231
  /* 快照导入预览:行选择纵向堆叠(原为横向勾选 chips,切片五换 RowSelect)。 */
1145
1232
  .dsm-snap-picks{display:flex;flex-direction:column;gap:2px}
1233
+ /* 侧栏入口打开的那一页(main 面板):宿主只给一个空的中栏,滚动与留白都得自己声明。
1234
+ 顶端让出标题栏净空(macOS / Windows 桌面端由宿主在这两个属性上给高度),与 dshmarket
1235
+ 的主面板同一套算法;内容仍是 .dsm-section —— 宽度上限与居中它自己管。 */
1236
+ .dsm-panel{box-sizing:border-box;width:100%;height:100%;min-height:0;overflow:auto;padding:calc(20px + var(--dsh-frame-top-clearance,0px)) clamp(16px,4vw,40px) 32px}
1146
1237
  `
1147
1238
 
1148
1239
  /** 统一的异常文案:Error 取 message,其余(字符串/对象/null)原样 String。此前 63 处各写一遍。 */
@@ -1258,7 +1349,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1258
1349
  function MoreIcon(props) {
1259
1350
  var size = (props && props.size) || 14
1260
1351
  return h('svg', { viewBox: '0 0 16 16', width: size, height: size, 'aria-hidden': 'true', focusable: 'false', className: props && props.className },
1261
- h('path', { fill: 'currentColor', d: 'M3 6.6h2v2H3zM7 6.6h2v2H7zM11 6.6h2v2h-2z' }))
1352
+ h('path', { fill: 'currentColor', d: 'M3 7h2v2H3zM7 7h2v2H7zM11 7h2v2h-2z' }))
1262
1353
  }
1263
1354
 
1264
1355
  // `react` / `h` 别名(2026-10-11 切片五从 42 片上移):工厂作用域声明对 apply 内外
@@ -1324,7 +1415,9 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1324
1415
  * 2026-10-11 切片五从 42 片上移:兼容页的原生勾选框(斜杠入口 / 注入域 / 快照)
1325
1416
  * 要换成开关,而兼容页在 apply 之外看不到 42 片的声明。
1326
1417
  */
1327
- function Switch(props) { return h('button', { type: 'button', className: 'dsm-switch' + (props.on ? ' dsm-switch-on' : ''), role: 'switch', 'aria-checked': props.on, 'aria-label': props.label, title: props.title, disabled: props.disabled, onClick: props.onClick }); }
1418
+ // 开关的视觉完全交给 `[aria-checked]`(用户 2026-10-18:「switch 按键统一和官方一样的模板」):
1419
+ // 官方那颗也是只靠这个属性切换外观,多挂一个 dsm-switch-on 类等于留两条互相打架的判据。
1420
+ function Switch(props) { return h('button', { type: 'button', className: 'dsm-switch', role: 'switch', 'aria-checked': props.on === true, 'aria-label': props.label, title: props.title, disabled: props.disabled, onClick: props.onClick }); }
1328
1421
 
1329
1422
  /**
1330
1423
  * 下拉菜单的高度上限,**必须与 CSS 里 .dsm-select-menu 的 max-height 一致** ——
@@ -1658,7 +1751,12 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1658
1751
  // without a CORS preflight that this route never answers.
1659
1752
  const headers = { 'content-type': 'application/json', 'x-dsh-plugin': 'dsh-plugin-tool-management' }
1660
1753
  if (TOKEN) headers['x-dsh-token'] = TOKEN
1661
- return fetch('/dsh-plugin-tool-management/api', {
1754
+ // **按 document.baseURI 解析**,不写死 '/':宿主可以被挂在反代 / 静态服务器的
1755
+ // 子路径下(base 存在于 index.html 的 <base> 里),写死 '/' 时浏览器会向宿主
1756
+ // 站点的根本身发请求,面板整页 404 —— 而那时宿主与插件都"装对了"。
1757
+ // baseURI 总是绝对 URL(文档没有 base 时等于 location.href 去掉 query/hash),
1758
+ // 所以这里不需要再判断它是不是绝对路径。
1759
+ return fetch(new URL('dsh-plugin-tool-management/api', document.baseURI).toString(), {
1662
1760
  method: 'POST',
1663
1761
  headers,
1664
1762
  body: JSON.stringify({ op, args: args || {} }),
@@ -1859,6 +1957,58 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1859
1957
  else slashMountStates = { tools: state, quickPrompts: state }
1860
1958
  }
1861
1959
 
1960
+ // ── 侧栏入口的开关(兼容页那颗按钮写,apply 侧的注册读)──────────────────────
1961
+ //
1962
+ // 为什么放在 factory 作用域:写它的是兼容页(22 片,apply 之外),读它的是 apply 侧的
1963
+ // 两处注册(49 片)—— 只有这个共享作用域两边都够得着,谁也不跨 apply 边界引对方的声明。
1964
+ //
1965
+ // 默认开:侧车那边的默认值也是开,读失败一律按开处理(见 sidebar-settings.ts)——
1966
+ // 判不出来时不该把一个入口藏掉。
1967
+ let sidebarEntryState = { enabled: true, busy: false }
1968
+ const sidebarEntrySubs = new Set()
1969
+ function sidebarEntrySnapshot() { return sidebarEntryState }
1970
+ function setSidebarEntryState(patch) {
1971
+ const next = Object.assign({}, sidebarEntryState, patch)
1972
+ if (next.enabled === sidebarEntryState.enabled && next.busy === sidebarEntryState.busy) return
1973
+ sidebarEntryState = next
1974
+ for (const fn of Array.from(sidebarEntrySubs)) { try { fn() } catch (e) { /* 单个订阅者出错不影响其它 */ } }
1975
+ }
1976
+ /** 订阅这份状态(兼容页与 apply 侧的注册共用,保证两边永远看到同一份)。 */
1977
+ function onSidebarEntryChange(fn) {
1978
+ sidebarEntrySubs.add(fn)
1979
+ return function () { sidebarEntrySubs.delete(fn) }
1980
+ }
1981
+ function useSidebarEntrySnapshot() {
1982
+ const st = React.useState(sidebarEntrySnapshot)
1983
+ React.useEffect(function () {
1984
+ const fn = function () { st[1](sidebarEntrySnapshot()) }
1985
+ sidebarEntrySubs.add(fn)
1986
+ fn()
1987
+ return function () { sidebarEntrySubs.delete(fn) }
1988
+ }, [])
1989
+ return st[0]
1990
+ }
1991
+ /**
1992
+ * 写开关:先乐观更新(点一下就该有反馈),服务端回包为准,失败回滚。
1993
+ * @returns `{ ok: boolean, error?: string }` —— 文案由调用方按当前语言给(这里没有 t)。
1994
+ */
1995
+ function saveSidebarEntry(enabled) {
1996
+ const before = sidebarEntryState.enabled
1997
+ const next = enabled === true
1998
+ setSidebarEntryState({ enabled: next, busy: true })
1999
+ return apiCall('sidebar-settings', { set: true, enabled: next }).then(function (r) {
2000
+ if (r && r.ok && r.settings) {
2001
+ setSidebarEntryState({ enabled: r.settings.enabled !== false, busy: false })
2002
+ return { ok: true }
2003
+ }
2004
+ setSidebarEntryState({ enabled: before, busy: false })
2005
+ return { ok: false, error: (r && r.error) || '' }
2006
+ }).catch(function (e) {
2007
+ setSidebarEntryState({ enabled: before, busy: false })
2008
+ return { ok: false, error: errMsg(e) }
2009
+ })
2010
+ }
2011
+
1862
2012
  /**
1863
2013
  * 一条全局轻提示(body 级,与页面/弹窗无关)。
1864
2014
  *
@@ -1914,6 +2064,20 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
1914
2064
  list: 'compat.op.list', archive: 'compat.op.archive', unarchive: 'compat.op.unarchive',
1915
2065
  batch: 'compat.op.batch', delete: 'compat.op.delete',
1916
2066
  }
2067
+ /**
2068
+ * 0.2.1 起**新增**的宿主契约能力 id(capability-specs.ts 的那四条)。
2069
+ *
2070
+ * 与上面那排 action 的区别:action 是"这个操作走哪条路",这些是"某个契约还在不在"。
2071
+ * 它们缺了都不抛错、只是功能少一半,所以单独一块而不是混进 action 表 —— 混进去会让
2072
+ * "删除不可用"和"工具限制失效"看起来一样严重,实际后者不影响你删任何东西。
2073
+ *
2074
+ * 键集必须与服务端 `CONTRACT_CAPABILITY_IDS` 对齐:那里按 feature-overview 的行 key 映射
2075
+ * 到能力 id,这里按能力 id 过滤 findings。两边任一处改名,另一处会在界面上静默失效
2076
+ * (契约行不再出现),所以加了就两处一起加。
2077
+ */
2078
+ var CONTRACT_CAPABILITY_IDS = [
2079
+ 'agentPreset.register', 'tools.restrict-method', 'agent.created-event', 'connection.admit',
2080
+ ]
1917
2081
  /**
1918
2082
  * 「最近改动」流水里 op 的人类名(0.15.0 B2)。
1919
2083
  *
@@ -2007,6 +2171,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
2007
2171
  'tool-table': 'compat.audit.op.toolTable',
2008
2172
  'inject-settings': 'compat.audit.op.injectSettings',
2009
2173
  'scene-settings': 'compat.audit.op.sceneSettings',
2174
+ 'sidebar-settings': 'compat.audit.op.sidebarSettings',
2010
2175
  'backups-clean': 'compat.audit.op.backupsClean',
2011
2176
  'history-archive': 'compat.audit.op.sessionArchive',
2012
2177
  'history-unarchive': 'compat.audit.op.sessionUnarchive',
@@ -2644,6 +2809,10 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
2644
2809
  case 'sessions.cold-announce': return t('compat.cap.sessionsColdAnnounce')
2645
2810
  case 'projection.write': return t('compat.cap.projectionWrite')
2646
2811
  case 'projection.table-delete': return t('compat.cap.projectionTableDelete')
2812
+ case 'agentPreset.register': return t('compat.cap.agentPresetRegister')
2813
+ case 'tools.restrict-method': return t('compat.cap.toolsRestrictMethod')
2814
+ case 'agent.created-event': return t('compat.cap.agentCreatedEvent')
2815
+ case 'connection.admit': return t('compat.cap.connectionAdmit')
2647
2816
  case 'archive': return t('compat.cap.opArchive')
2648
2817
  case 'unarchive': return t('compat.cap.opUnarchive')
2649
2818
  case 'batch': return t('compat.cap.opBatch')
@@ -2679,6 +2848,10 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
2679
2848
  case 'prompts': return t('compat.ov.prompts')
2680
2849
  case 'subagents': return t('compat.ov.subagents')
2681
2850
  case 'scenes': return t('compat.ov.scenes')
2851
+ case 'subagent-persona': return t('compat.ov.subagentPersona')
2852
+ case 'subagent-tool-restrict': return t('compat.ov.subagentToolRestrict')
2853
+ case 'subagent-created-event': return t('compat.ov.subagentCreatedEvent')
2854
+ case 'web-admission': return t('compat.ov.webAdmission')
2682
2855
  default: return null
2683
2856
  }
2684
2857
  }
@@ -2763,8 +2936,17 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
2763
2936
  if (row.asarHost) return t('compat.ov.hostIdentity.asar', { version: row.version, name: row.asarName || 'app.asar' })
2764
2937
  return t('compat.ov.hostIdentity.ok', { version: row.version })
2765
2938
  }
2939
+ if (row.key === 'subagent-persona' || row.key === 'subagent-tool-restrict' || row.key === 'subagent-created-event' || row.key === 'web-admission') {
2940
+ // 宿主契约行:可用时说的是"运行时看到的那份实现"(它的 detail 由探针写成"成员齐备"),
2941
+ // 降级时说的是"这条契约没了,对应功能少一半"——两个方向都得按语言说。
2942
+ return row.state === 'unknown' ? t('compat.ov.route.unknown')
2943
+ : row.state === 'ok' ? t('compat.ov.contract.ok')
2944
+ : t('compat.ov.contract.degraded', { detail: row.detail })
2945
+ }
2766
2946
  if (row.state === 'ok' && (row.key === 'memory' || row.key === 'prompts' || row.key === 'subagents' || row.key === 'scenes')) {
2767
- return t('compat.ov.plain.ok')
2947
+ // 有宿主模块可核的域(提示词 / 子智能体)按实况说;确实没有可核对象的才说"无降级上报"。
2948
+ if (row.key === 'memory' || row.key === 'scenes') return t('compat.ov.plain.ok')
2949
+ return t('compat.ov.domain.contract', { detail: row.detail })
2768
2950
  }
2769
2951
  return row.detail
2770
2952
  }
@@ -3439,7 +3621,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
3439
3621
  card(t('compat.caps'), String(usableCount) + '/' + String(findings.length), capsNote, degraded.length > 0)))
3440
3622
 
3441
3623
  // 版本栅栏(B3):实际宿主与本插件验证过的版本不同时说出来 —— 否则"照旧版本的印象
3442
- // 判断"会让人把探测结果当成 bug。只提示、不拦(peer 保持无上界是既定决策)。
3624
+ // 判断"会让人把探测结果当成 bug。只提示、不拦(peer 的上界只管**安装期**收不收,
3625
+ // 这里管的是"已经跑起来了,版本对不上要说一声":同代的 rc 差异提示一次即可)。
3443
3626
  const hostVersion = (data.host && data.host.version) || ''
3444
3627
  if (hostVersion && data.verifiedVersion && hostVersion !== data.verifiedVersion) {
3445
3628
  push(React.createElement('p', { className: 'dsm-help' },
@@ -3481,6 +3664,24 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
3481
3664
  blockers.map(function (item, i) { return React.createElement('li', { key: i }, item) })))))
3482
3665
  }
3483
3666
 
3667
+ // 宿主契约(0.2.1 新增的三处适配面 + Web 连接准入):这几条在能力表里是**必填项**,
3668
+ // 不是"宿主没有、插件自己顶上"的可选槽。缺了不抛错,只是功能少一半 —— 所以单独一块
3669
+ // 列出来,让"升级后某处悄悄不工作"有一个显眼的位置。
3670
+ const contractFindings = findings.filter(function (item) {
3671
+ return CONTRACT_CAPABILITY_IDS.indexOf(item.id) !== -1 && item.state !== 'ok'
3672
+ })
3673
+ push(section(t('compat.contracts'), t('compat.contracts.hint'),
3674
+ contractFindings.length === 0
3675
+ ? React.createElement('p', { className: 'dsm-help' }, t('compat.contracts.allOk'))
3676
+ : React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
3677
+ contractFindings.map(function (item) {
3678
+ return React.createElement('div', { className: 'dsm-compat-mod-row', key: item.id },
3679
+ React.createElement('span', { className: 'dsm-compat-name' }, capLabel(item.id, item.label)),
3680
+ React.createElement('span', { className: 'dsm-compat-pill dsm-compat-pill-warn' }, t('compat.contracts.missing')),
3681
+ React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
3682
+ React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')), item.detail))
3683
+ }))))
3684
+
3484
3685
  // 动作可用性:一行一个动作 + 路径标记(不可用的标红)。
3485
3686
  push(section(t('compat.ops'), t('compat.ops.hint'),
3486
3687
  React.createElement('div', { className: 'dsm-compat-mod-list' },
@@ -3520,6 +3721,10 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
3520
3721
  // 其它非 ok 项:`optional` 表示"宿主没有这个入口,插件自己顶上"——功能完好,
3521
3722
  // 既不算降级,也不能说按钮被禁用(那句只对真降级成立;早先把两者混在一起
3522
3723
  // 会让用户以为删除功能坏了,实际它照常可用)。
3724
+ //
3725
+ // 与上面「宿主契约」那块的区别:这里的条目缺了,插件**自有等价实现**(适配路完整可用);
3726
+ // 那块的条目缺了,没有替代实现,功能就是少一半。两块都是"非 ok",但后果不同,
3727
+ // 所以分开显示、各自的胶囊文案也不同。
3523
3728
  const optionalAbsent = findings.filter(function (item) { return item.state !== 'ok' && item.optional === true })
3524
3729
  if (optionalAbsent.length) {
3525
3730
  push(section(t('compat.others'), t('compat.others.hint'),
@@ -3566,6 +3771,31 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
3566
3771
  React.createElement('span', { className: 'dsm-compat-pill ' + cls }, text))
3567
3772
  }))))
3568
3773
 
3774
+ // 裸模块解析(与上面那一块的分工:上面比的是**同一份物理模块**,这里看的是**包还在不在**)。
3775
+ //
3776
+ // 为什么分开:上面那 6 个包是插件的 peerDependency,「两份不同拷贝」那种故障只可能发生在
3777
+ // 它们身上;而下面这 4 个包是插件按**裸模块名**在运行时 require 的官方包,宿主不要求插件
3778
+ // 依赖它们。官方撤掉其中一个,对应功能静默降级,而身份比对那一栏根本看不见它们 —— 所以
3779
+ // 必须在这里列一行。canary 缺失不是「包坏了」,是「该人工复核这条契约」,显示成提示而非红色。
3780
+ const contractPackages = (data && data.contracts && data.contracts.packages) || []
3781
+ if (contractPackages.length) {
3782
+ push(section(t('compat.bareModules'), t('compat.bareModules.hint'),
3783
+ React.createElement('div', { className: 'dsm-compat-mod-list dsm-compat-cards' },
3784
+ contractPackages.map(function (item) {
3785
+ const missing = item.resolved === null
3786
+ const drifted = !missing && (item.missingMarkers || []).length > 0
3787
+ return React.createElement('div', { className: 'dsm-compat-mod-row', key: item.package },
3788
+ React.createElement('span', { className: 'dsm-compat-name dsm-compat-mod' }, item.package),
3789
+ React.createElement('span', { className: 'dsm-compat-pill ' + (missing ? 'dsm-compat-pill-bad' : drifted ? 'dsm-compat-pill-warn' : 'dsm-compat-pill-ok') },
3790
+ missing ? t('compat.bareModules.absent') : drifted ? t('compat.bareModules.drift') : t('compat.bareModules.present')),
3791
+ React.createElement('span', { className: 'dsm-compat-mod dsm-compat-mod-self' },
3792
+ React.createElement('span', { className: 'dsm-compat-label' }, t('compat.reason')),
3793
+ missing ? t('compat.bareModules.absentWhy', { feature: item.feature })
3794
+ : drifted ? t('compat.bareModules.driftWhy', { feature: item.feature, markers: (item.missingMarkers || []).join('、') })
3795
+ : t('compat.bareModules.presentWhy', { feature: item.feature })))
3796
+ }))))
3797
+ }
3798
+
3569
3799
  // 「无法比对」必须给原因:否则一排未知标记看起来只是噪音,用户会当成正常现象。
3570
3800
  // 归档宿主的说明排最前 —— 它解释的正是这一排「无法比较」的成因,而且必须讲明白
3571
3801
  // **不要去跑 host-deps --fix**:那条命令会把插件的依赖指向 npx 缓存里另一个版本。
@@ -4132,6 +4362,19 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4132
4362
  React.createElement('div', { className: 'dsm-modal-body' }, presetRows)))
4133
4363
  }
4134
4364
 
4365
+ /**
4366
+ * 侧栏入口的开关(2026-10-18):按钮文字说的是**点下去会发生什么**(开着 → 「关闭侧栏」),
4367
+ * 与「全选 ↔ 取消全选」「刷新 ↔ 刷新中…」同一套写法。默认开(侧车默认值同)。
4368
+ * 失败只回一行提示,不改按钮 —— 回滚由 saveSidebarEntry 做(乐观更新那一步)。
4369
+ */
4370
+ const sidebarEntry = useSidebarEntrySnapshot()
4371
+ const toggleSidebarEntry = function () {
4372
+ if (sidebarEntry.busy === true) return
4373
+ saveSidebarEntry(sidebarEntry.enabled === false).then(function (r) {
4374
+ if (!r || r.ok) return
4375
+ showPluginToast(r.error || t('compat.sidebar.failed'), 'err')
4376
+ })
4377
+ }
4135
4378
  // 三块内容各自只在**打开时**挂进弹窗;说明句留在框内第一行(原来长在区块小标题右侧,
4136
4379
  // 进了弹窗就没有"标题右边"这个位置了)。
4137
4380
  // 按钮**永远点得开**:数据没读到时框里给一句话,而不是摆一枚按了没反应的按钮
@@ -4180,6 +4423,16 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4180
4423
  : t('compat.backups.btnHint'),
4181
4424
  onClick: openBackupClean,
4182
4425
  }, backups && backups.total ? t('compat.backups.btnCount', { count: backups.total }) : t('compat.backups.btn')),
4426
+ // 侧栏入口的开关:放在诊断工具后面,它改的是本插件自己的入口,与上面三块"看数据"
4427
+ // 不是一类(用户 2026-10-18 要求摆在兼容页这一排)。
4428
+ React.createElement('button', {
4429
+ key: 'sidebar-entry',
4430
+ type: 'button',
4431
+ className: 'dsm-btn dsm-btn-quiet',
4432
+ disabled: sidebarEntry.busy === true,
4433
+ title: t(sidebarEntry.enabled === false ? 'compat.sidebar.showHint' : 'compat.sidebar.hideHint'),
4434
+ onClick: toggleSidebarEntry,
4435
+ }, t(sidebarEntry.enabled === false ? 'compat.sidebar.show' : 'compat.sidebar.hide')),
4183
4436
  ],
4184
4437
  }),
4185
4438
  body,
@@ -4249,6 +4502,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4249
4502
  // 是服务端动态中文尾巴,EN 界面显示英文框架 + 中文细节(比整句中文可读)。
4250
4503
  "error.rules.sceneNotFound": "场景不存在:{group}(请先在「场景」页创建该场景)",
4251
4504
  "error.rules.reservedScene": "「{name}」是保留场景,不可{action}{reason}",
4505
+ "error.rules.sceneNotActive": "场景未启动:先启动再锁定",
4252
4506
  "error.rules.singleSceneOnly": "除「全局」外同时只能启用一个场景{detail}",
4253
4507
  "error.agentsMd.sceneRebindFailed": "无法把场景「{scene}」的提示词绑定改成「{target}」:{reason}",
4254
4508
  "error.agentsMd.referenced": "「{id}」不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "error.agentsMd.refProbeFailed": "无法确认「{id}」是否被引用,已拒绝删除:引用探测失败(场景绑定 / AGENTS.md 当前内容 / 进场景前的基线,这三类里至少有一类没读到)。请稍后重试。", "error.agentsMd.noUnapplyBackup": "没有可恢复的「应用之前的状态」:AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),所以没有起点可回。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身。", "error.agentsMd.unapplyWhileScene": "场景「{scene}」正在驱动全局基线,不能在这里取消应用:先退出场景,或到场景页改掉它的提示词绑定。",
@@ -4289,6 +4543,23 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4289
4543
  "trash.preview.readonly": "只读预览:内容不能修改,这里也没有保存。",
4290
4544
  "trash.preview.files": "文件 {count} 个",
4291
4545
  "trash.preview.meta": "来源记录",
4546
+ "trash.preview.entry": "条目信息",
4547
+ "trash.preview.field.deletedAt": "删除时间",
4548
+ "trash.preview.field.scene": "所属场景",
4549
+ "trash.preview.field.form": "形态",
4550
+ "trash.preview.field.label": "显示名",
4551
+ "trash.preview.field.desc": "描述",
4552
+ "trash.preview.field.locked": "锁定",
4553
+ "trash.preview.field.mcp": "MCP",
4554
+ "trash.preview.sourceGone": "已移除的来源",
4555
+ "trash.preview.field.skills": "技能",
4556
+ "trash.preview.field.subagents": "子智能体",
4557
+ "trash.preview.field.quick": "快捷提示词",
4558
+ "trash.preview.field.toolTable": "工具表方案",
4559
+ "trash.preview.field.prompt": "提示词预设",
4560
+ "trash.preview.field.memories": "随场景删除的记忆",
4561
+ "trash.preview.none": "(空)",
4562
+ "trash.preview.yes": "是",
4292
4563
  "trash.preview.field.scope": "范围",
4293
4564
  "trash.preview.field.origin": "来源类型",
4294
4565
  "trash.preview.field.path": "目录",
@@ -4349,7 +4620,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4349
4620
  "mcp.msg.ok": "操作成功", "mcp.msg.failed": "操作失败", "mcp.msg.loadFailed": "加载失败", "mcp.msg.warn": "操作完成,但加载器有提示:{warning}", "mcp.msg.toolsRefreshed": "已取到 {count} 个工具", "mcp.msg.restartWarn": "已重启,但有告警:{warning}", "mcp.msg.restarted": "已重启:{name}",
4350
4621
  "mcp.form.addTitle": "新增 MCP", "mcp.form.editTitle": "编辑 MCP 服务:",
4351
4622
  "mcp.form.err.serverName": "服务名需为 1-32 位字母、数字、下划线或短横线(不能有空格与中文)", "mcp.form.err.url": "地址需以 http:// 或 https:// 开头", "mcp.form.err.command": "命令不能为空",
4352
- "mcp.form.maskedHint": "密钥框里的打码值(如 ••••••)不是原文:直接保存不会改动它们,插件会保留补丁文件里的原值。要看原文请点「显示密钥」(本插件配了访问令牌的话,先填对令牌)。",
4623
+ "mcp.form.maskedHint": "该 MCP 服务的密钥已打码,可以直接更改;看原文去详细页点「显示密钥」。",
4353
4624
  "mcp.field.serverName": "服务名称", "mcp.field.serverName.hint": "1-32 位 [A-Za-z0-9_-],补丁里按此名注册",
4354
4625
  "mcp.field.note": "备注(可选)", "mcp.field.note.placeholder": "给模型看的决策提示,比如「这台要 sudo 免密」;留空不写",
4355
4626
  "mcp.field.transport": "传输方式", "mcp.field.level": "级别",
@@ -4364,7 +4635,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4364
4635
  "mcp.compact.confirm": "确认整理", "mcp.compact.done": "补丁文件已整理:清掉 {count} 条失效的旧开关记录,服务的启用/停用状态未变", "mcp.compact.clean": "补丁文件已经很干净,没有可清理的旧记录",
4365
4636
  "mcp.detail.entryId": "条目 ID", "mcp.detail.status": "运行状态", "mcp.detail.loader": "Loader 登记", "mcp.detail.loader.yes": "已登记", "mcp.detail.loader.no": "未登记",
4366
4637
  "mcp.tools.stale": "上次运行时", "mcp.tools.expand": "展开", "mcp.tools.collapse": "收起",
4367
- "mcp.detail.note": "备注(随上下文注入发给模型)", "mcp.detail.note.placeholder": "例如:A 不可用时改用 B 兜底", "mcp.detail.note.save": "保存备注",
4638
+ "mcp.detail.note": "备注(随上下文注入发给模型)", "mcp.detail.note.empty": "当前没有备注",
4639
+ "mcp.msg.noteFailed": "服务已保存,但备注没写进去。",
4368
4640
  "mcp.detail.tools": "工具({count})", "mcp.detail.toolsRefresh": "刷新工具",
4369
4641
  "mcp.field.headersShort": "请求头", "mcp.field.envShort": "环境变量",
4370
4642
  "mcp.tools.loading": "正在获取工具列表…", "mcp.tools.loadFailed": "加载工具失败:", "mcp.tools.none": "该服务暂无已注册工具。",
@@ -4409,6 +4681,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4409
4681
  "tabs.scenes": "场景", "tabs.skills": "技能", "tabs.subagents": "子智能体", "tabs.prompts": "提示词", "tabs.memory": "记忆", "tabs.sessions": "会话", "tabs.compat": "兼容",
4410
4682
  "compat.title": "宿主兼容", "compat.desc": "本插件与 DSH 的对接体检(只读)。",
4411
4683
  "compat.backups.btn": "清理旧备份", "compat.backups.btnCount": "清理旧备份 · {count} 份", "compat.backups.btnHint": "删掉多余的配置备份副本(它们含明文密钥)", "compat.backups.btnTitle": "现有 {count} 份配置备份(共 {size},含明文密钥)", "compat.backups.title": "清理旧备份",
4684
+ // 侧栏入口的开关(按钮文字说"点下去会发生什么":开着 → 「关闭侧栏」)。
4685
+ "compat.sidebar.hide": "关闭侧栏", "compat.sidebar.show": "开启侧栏", "compat.sidebar.hideHint": "把本插件的入口从侧栏撤掉(「设置 → 工具管理」照旧可用)", "compat.sidebar.showHint": "在侧栏里挂出本插件的入口,点一下就在这里打开同一套页面", "compat.sidebar.failed": "侧栏开关没保存成功",
4412
4686
  "compat.backups.desc": "每份备份都是整份配置的副本,MCP 密钥与访问令牌在里面是明文。",
4413
4687
  "snapshot.title": "整机迁移", "snapshot.hint": "一次带走 MCP、技能、记忆、子智能体、提示词、场景档案与侧车配置,换机器时再导入。", "snapshot.browse": "选择",
4414
4688
  "snapshot.export.dir": "导出目录", "snapshot.export.btn": "导出", "snapshot.export.busy": "导出中…", "snapshot.export.done": "快照已写到:{dir}", "snapshot.export.failed": "导出失败",
@@ -4441,7 +4715,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4441
4715
  "compat.audit.op.personaSave": "保存人设", "compat.audit.op.personaDelete": "删除人设", "compat.audit.op.personaImport": "导入人设", "compat.audit.op.personaSwitch": "启停人设",
4442
4716
  "compat.audit.op.presetSave": "保存提示词", "compat.audit.op.quickSave": "保存快捷提示词", "compat.audit.op.quickSwitch": "启停快捷提示词", "compat.audit.op.quickDelete": "删除快捷提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
4443
4717
  "compat.audit.op.trashRestore": "从回收站恢复", "compat.audit.op.trashDelete": "永久删除",
4444
- "compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.backupsClean": "清理旧备份",
4718
+ "compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.sidebarSettings": "侧栏入口", "compat.audit.op.backupsClean": "清理旧备份",
4445
4719
  "compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.trashRetention": "改回收站保留期", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
4446
4720
  "compat.backups.count": "现有 {count} 份,共 {size}",
4447
4721
  "compat.backups.del": "删除份数:", "compat.backups.delOption": "{count} 份",
@@ -4465,6 +4739,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4465
4739
  "compat.op.native": "宿主原生入口", "compat.op.adapter": "插件适配层", "compat.op.unavailable": "不可用(已禁用)",
4466
4740
  "compat.degraded": "已降级能力", "compat.degraded.hint": "缺什么 + 后果", "compat.reason": "原因:",
4467
4741
  "compat.others": "其它能力项", "compat.others.hint": "不算降级:宿主没有这些入口时,由插件自己顶上",
4742
+ "compat.contracts": "宿主契约", "compat.contracts.hint": "本版适配依赖的宿主成员:缺了不抛错,但对应功能会少一半", "compat.contracts.missing": "不在位", "compat.contracts.allOk": "本版依赖的宿主契约全部在位。",
4743
+ "compat.bareModules": "裸模块解析", "compat.bareModules.hint": "插件在运行时按裸模块名 require 的官方包:它们不是 peer,所以不参与上面的模块比对", "compat.bareModules.present": "在位", "compat.bareModules.absent": "宿主未提供", "compat.bareModules.drift": "契约疑似漂移", "compat.bareModules.presentWhy": "影响:{feature}", "compat.bareModules.absentWhy": "宿主已不提供这个包:{feature}会降级", "compat.bareModules.driftWhy": "{feature}的契约标识有缺失({markers})—— 请人工核对该包的实现是否改过",
4468
4744
  "compat.fallback.substituted": "插件适配层(宿主无此入口)",
4469
4745
  "compat.fallback.native": "改走原生入口", "compat.fallback.refuse": "该操作会被拒绝", "compat.fallback.blocked": "相关按钮已禁用", "compat.fallback.inform": "不影响写入",
4470
4746
  "compat.modules": "模块实体(插件 vs 宿主)", "compat.modules.hint": "同一份模块才谈得上适配", "compat.module.same": "同一份模块", "compat.module.separate": "两份拷贝(需修复)", "compat.module.sameVersion": "同版本(分处归档内外)", "compat.module.unknown": "无法比较", "compat.module.realpath": "无法比较(路径取不到真实形态)", "compat.modules.realpath": "有 {count} 个包**比不了**:它们的路径取不到真实形态({names})—— 这不是「两份拷贝」,而是这项校验没做成。", "compat.modules.realpathSides": "宿主侧:{host};插件侧:{plugin}。修好路径(权限 / 路径长度 / 重新安装)后重新打开本页即重算。", "compat.modules.realpathNone": "无", "compat.modules.unverified": "有 {count} 个包没能比对(宿主的解析锚点里找不到:{names})—— 这不是「两份拷贝」,而是这项校验没做成。可运行 node scripts/doctor.mjs 查看。",
@@ -4496,7 +4772,11 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4496
4772
  "compat.ov.nativeDelete": "宿主原生删除入口", "compat.ov.nativeDelete.ok": "宿主未提供原生删除入口(本插件自有完整序列)",
4497
4773
  "compat.ov.hostIdentity": "宿主身份与模块", "compat.ov.hostIdentity.ok": "模块与宿主同源(宿主 DSH {version})", "compat.ov.hostIdentity.asar": "官方包与宿主同版本(DSH {version}),但宿主跑在 {name} 里、插件在归档外", "compat.ov.hostIdentity.degraded": "存在阻塞项:{list}",
4498
4774
  "compat.ov.memory": "记忆", "compat.ov.prompts": "提示词", "compat.ov.subagents": "子智能体", "compat.ov.scenes": "场景档案",
4775
+ "compat.ov.subagentPersona": "子智能体人设委派", "compat.ov.subagentToolRestrict": "子代理工具限制", "compat.ov.subagentCreatedEvent": "子代理创建时序", "compat.ov.webAdmission": "Web 连接准入",
4499
4776
  "compat.ov.plain.ok": "无降级上报(装配失败会出现在这里)",
4777
+ "compat.ov.contract.ok": "宿主契约在位(这一项缺了,对应功能会静默少一半)",
4778
+ "compat.ov.contract.degraded": "宿主契约不在位:{detail}",
4779
+ "compat.ov.domain.contract": "依赖的宿主模块在位:{detail}",
4500
4780
  // ── 斜杠命令「工具」段(对话输入框里直接管六个域;实现见 48-slash.js)──────
4501
4781
  // 段标题与六个域名出现在宿主 `/` 菜单里;域名要短 —— 菜单行右侧还要留位置给摘要与 › 箭头。
4502
4782
  "slash.section.tools": "工具",
@@ -4541,7 +4821,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4541
4821
  "trash.group.quick": "快捷提示词",
4542
4822
  "trash.section.quick.sub": "快捷提示词目录已移走:恢复能还原,永久删除会删掉文件",
4543
4823
  "slash.section.quick": "快捷提示词",
4544
- "slash.quick.empty": "这条快捷提示词没有正文,先到「设置 → 工具 → 提示词」补上",
4824
+ "slash.quick.empty": "这条快捷提示词没有正文,先到「设置 → 工具管理 → 提示词」补上",
4545
4825
  "compat.ov.slashCommands": "斜杠命令入口", "compat.ov.slashCommands.on": "两段都开(打 / 唤出「工具」与「快捷提示词」两段)",
4546
4826
  "compat.ov.slashCommands.onTools": "只开「工具」段", "compat.ov.slashCommands.onQuick": "只开「快捷提示词」段",
4547
4827
  "compat.ov.slashCommands.off": "两段都已关闭(本页可重新打开)",
@@ -4576,6 +4856,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4576
4856
  "compat.cap.workspaceIndexHeader": "索引会话头部", "compat.cap.workspaceArchiveNative": "宿主原生归档入口", "compat.cap.workspaceUnarchiveNative": "宿主原生恢复入口",
4577
4857
  "compat.cap.workspaceBatchNative": "宿主原生批量入口", "compat.cap.workspaceDeleteNative": "宿主原生删除入口", "compat.cap.sessionsDetachLive": "实时会话落盘与分离",
4578
4858
  "compat.cap.sessionsColdAnnounce": "冷会话移除广播", "compat.cap.projectionWrite": "投影缓存写入路径", "compat.cap.projectionTableDelete": "投影缓存行删除",
4859
+ "compat.cap.agentPresetRegister": "临时子预设注册", "compat.cap.toolsRestrictMethod": "工具限制施加(restrict)", "compat.cap.agentCreatedEvent": "子代理创建事件(agent/created)", "compat.cap.connectionAdmit": "连接准入(admit)",
4579
4860
  "compat.cap.opArchive": "归档", "compat.cap.opUnarchive": "恢复", "compat.cap.opBatch": "批量操作", "compat.cap.opDelete": "删除", "compat.cap.opList": "历史列表",
4580
4861
  "compat.reach": "预设注入边界", "compat.reach.hint": "每个预设下这些内容到不到得了模型",
4581
4862
  "compat.reach.suppressing": "压制型",
@@ -4590,7 +4871,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4590
4871
  "compat.token.sentence.off": "写操作不再要凭证;明文密钥仍然凭令牌查看。",
4591
4872
  "compat.token.sentence.locked": "改配置会被拒绝。",
4592
4873
  "compat.token.sentence.unlocked": "本次启动已解锁;重启 DSH 后要重新解锁。",
4593
- "compat.token.lock.composer": "未填写访问令牌,暂时无法对话 —— 请到「工具 → 兼容」页填写",
4874
+ "compat.token.lock.composer": "未填写访问令牌,暂时无法对话 —— 请到「工具管理 → 兼容」页填写",
4594
4875
  "compat.token.banner": "访问令牌未验证:现在发出的对话会被宿主拒绝(消息不会进入对话)。",
4595
4876
  "token.dialog.title": "填写访问令牌",
4596
4877
  "compat.token.hostHint": "写进配置文件,重启 DSH 后生效。",
@@ -4615,7 +4896,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4615
4896
  "compat.token.clearHost.btn": "删除令牌", "compat.token.offHost": "关闭保护", "compat.token.offHost.btn": "关闭保护",
4616
4897
  "compat.token.onHost": "开启保护", "compat.token.onHost.btn": "开启保护", "compat.token.onConfirm.yes": "确认开启",
4617
4898
  "compat.token.pendingRestart": "改动还没生效(胶囊说的是当前进程):重启 DSH 后才是当前状态。",
4618
- "compat.token.notFound": "当前未设置令牌,请点击「设置令牌」或改用环境变量 DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN。",
4899
+ "compat.token.notFound": "未设置令牌:点「设置令牌」,或设环境变量 DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN。",
4619
4900
  // 三种用法:一句话一条。上一版是一整段散文(讲了原理、机制、两种 code 的区别),
4620
4901
  // 用户裁定「写简单简洁一点,用户大致了解就行」——机制层面的东西留在这里的注释里。
4621
4902
  "compat.token.edit.current": "当前令牌", "compat.token.edit.new": "新令牌", "compat.token.edit.repeat": "再输一次", "compat.token.edit.save": "保存新令牌", "compat.token.edit.cancel": "取消",
@@ -4775,7 +5056,6 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4775
5056
  "scenes.skills.shadowed.hint": "被同名技能覆盖,在这个场景里不可能生效,因此不能勾选。要改用这一份,请到「技能」页点它的「启用这个」。",
4776
5057
  "scenes.skills.invalid.hint": "技能结构不完整(缺 frontmatter / 名字非法 / 描述为空),无法启停,因此不能勾选。",
4777
5058
  "scenes.filter.skills": "筛选技能/目录名称", "scenes.filter.subagents": "筛选人设(名称或描述)", "scenes.filter.servers": "筛选服务器",
4778
-
4779
5059
  "scenes.mem.title": "记忆", "scenes.mem.search": "在本场景内筛选记忆(名称或描述)",
4780
5060
  "scenes.mem.noMatch": "没有匹配项", "scenes.mem.emptyScene": "该场景还没有记忆",
4781
5061
  "scenes.mem.createNeedScene": "先在左边填好场景名", "scenes.mem.pending": "{count} 条待创建", "scenes.mem.pendingTag": "待创建", "scenes.mem.attach.tip": "带 {count} 个附件 · 共 {size}", "scenes.mem.removePending": "移除",
@@ -4899,8 +5179,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4899
5179
  "import.clear": "清空选择", "import.submit": "导入",
4900
5180
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
4901
5181
  "error.import.noFiles": "没有选择要导入的文件",
4902
- "nav.title": "工具",
4903
- "agm.stat.total": "全局提示词", "agm.stat.active": "生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.fileApplied": "它正被应用在 AGENTS.md 里(当前生效的那一份)", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.inactive": "未生效", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增", "agm.new.title": "新增提示词", "agm.btn.import": "导入", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "显示名称", "agm.field.id.hint": "允许中文、空格、点;不能含斜杠/反斜杠与 : * ? < > 等符号;≤64 字符,改 id 即目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown)", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "agm.unapply.title": "取消应用", "agm.unapply.body": "将把 ~/.dsh/AGENTS.md 恢复成开始应用提示词之前的那份内容(__last-applied__ 里保存的),此后不再有任何提示词预设生效。", "agm.unapply.note": "回到的是开始应用之前的那一份,中途应用过别的预设也一并撤销 —— 想换成其中某一份,直接点它自己的开关即可(效果等价)。这一步本身也会备份,可以再应用回来。", "agm.btn.confirmUnapply": "确认取消应用", "agm.unapply.switch.on": "AGENTS.md 里就是这一份 · 点一下取消应用(把 AGENTS.md 恢复成开始应用之前的内容)", "agm.unapply.switch.blocked": "AGENTS.md 里就是这一份,但没有「应用之前的状态」可恢复 —— 它不是由「应用」写进去的,所以关不掉。要改掉文件内容请应用别的预设或直接编辑 AGENTS.md;删除这份预设不会改动 AGENTS.md", "agm.unapply.switch.onRemove": "AGENTS.md 里就是这一份 · 点一下取消应用(开始应用之前没有 AGENTS.md,这一步会把它删掉)", "agm.unapply.bodyRemove": "开始应用提示词之前 ~/.dsh/AGENTS.md 并不存在,所以取消应用会把这个文件删掉(而不是写回内容),此后不再有任何提示词预设生效。", "agm.unapply.noteRemove": "删掉的是「应用」写进去的内容;这一份已备份到 __last-applied__(保留最近 5 代),需要时可以再应用回来。", "agm.btn.confirmUnapplyRemove": "确认取消应用并删除文件", "agm.result.unapplied": "已取消应用:AGENTS.md 已恢复成开始应用之前的内容", "agm.result.unappliedRemove": "已取消应用:AGENTS.md 已删除(开始应用之前它不存在)", "agm.unapply.unavailable": "AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),没有「应用之前的状态」可恢复,所以关不掉。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身;删除这份预设不会改动 AGENTS.md。", "agm.remove.fileNote": "注意:AGENTS.md 里仍然是这份内容,全局提示词照旧生效 —— 删除预设只把它移进回收站,不会改动 AGENTS.md。要让它不再生效,请改 AGENTS.md 本身或应用别的预设。", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "原目录不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "全局提示词",
5182
+ "nav.title": "工具管理",
5183
+ "agm.stat.total": "全局提示词", "agm.stat.active": "生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先解除引用再删 —— 场景绑定的去场景页换绑提示词、或退出场景;正被应用的那份先点左边那颗开关「取消应用」。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.fileApplied": "它正被应用在 AGENTS.md 里(当前生效的那一份)", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "处于「{scene}」场景:依旧可以更改配置,改动会同步档案,退出后状态还原。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.inactive": "未生效", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增", "agm.new.title": "新增提示词", "agm.btn.import": "导入", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "显示名称", "agm.field.id.hint": "允许中文、空格、点;不能含斜杠/反斜杠与 : * ? < > 等符号;≤64 字符,改 id 即目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown)", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "agm.unapply.title": "取消应用", "agm.unapply.body": "将把 ~/.dsh/AGENTS.md 恢复成开始应用提示词之前的那份内容(__last-applied__ 里保存的),此后不再有任何提示词预设生效。", "agm.unapply.note": "回到的是开始应用之前的那一份,中途应用过别的预设也一并撤销 —— 想换成其中某一份,直接点它自己的开关即可(效果等价)。这一步本身也会备份,可以再应用回来。", "agm.btn.confirmUnapply": "确认取消应用", "agm.unapply.switch.on": "AGENTS.md 里就是这一份 · 点一下取消应用(把 AGENTS.md 恢复成开始应用之前的内容)", "agm.unapply.switch.blocked": "AGENTS.md 里就是这一份,但没有「应用之前的状态」可恢复 —— 它不是由「应用」写进去的,所以关不掉。要改掉文件内容请应用别的预设或直接编辑 AGENTS.md;删除这份预设不会改动 AGENTS.md", "agm.unapply.switch.onRemove": "AGENTS.md 里就是这一份 · 点一下取消应用(开始应用之前没有 AGENTS.md,这一步会把它删掉)", "agm.unapply.bodyRemove": "开始应用提示词之前 ~/.dsh/AGENTS.md 并不存在,所以取消应用会把这个文件删掉(而不是写回内容),此后不再有任何提示词预设生效。", "agm.unapply.noteRemove": "删掉的是「应用」写进去的内容;这一份已备份到 __last-applied__(保留最近 5 代),需要时可以再应用回来。", "agm.btn.confirmUnapplyRemove": "确认取消应用并删除文件", "agm.result.unapplied": "已取消应用:AGENTS.md 已恢复成开始应用之前的内容", "agm.result.unappliedRemove": "已取消应用:AGENTS.md 已删除(开始应用之前它不存在)", "agm.unapply.unavailable": "AGENTS.md 的当前内容不是由「应用」写进去的(例如首次打开时自动生成的那份预设),没有「应用之前的状态」可恢复,所以关不掉。要改掉文件里的内容:应用别的预设,或直接编辑 AGENTS.md 本身;删除这份预设不会改动 AGENTS.md。", "agm.remove.fileNote": "注意:AGENTS.md 里仍然是这份内容,全局提示词照旧生效 —— 删除预设只把它移进回收站,不会改动 AGENTS.md。要让它不再生效,请改 AGENTS.md 本身或应用别的预设。", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "原目录不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "全局提示词",
4904
5184
  "scenes.field.prompt.none": "无", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.enter.personas": "启用 {count} 个子智能体:{names}", "scenes.preview.enter.personasNone": "子智能体一个都不启用",
4905
5185
  // 绑定三行(预览卡第一段):一律 `标签:值`,"保持现状"必须带当前那份的名字。
4906
5186
  "scenes.preview.bind.prompt": "全局提示词:切到「{name}」",
@@ -4915,7 +5195,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4915
5195
  "scenes.preview.bind.tool": "模型工具表:切到「{name}」(关 {count} 条)",
4916
5196
  "scenes.preview.bind.toolMissing": "模型工具表:「{name}」已不存在,这次不切换",
4917
5197
  "scenes.preview.bind.toolKeep": "模型工具表:保持现状(本场景未绑定)", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前设置", "scenes.field.quickPrompts.customCount": "自定义({picked}/{total})", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增",
4918
- "scenes.field.quickPrompts.noneOption": "无", "scenes.field.quickPrompts.list": "本场景启用", "scenes.field.quickPrompts.pick": "选择", "scenes.quickPick.title": "选择本场景启用的快捷提示词", "scenes.quickPick.hint": "勾选的进入本场景后启用,没勾的停用;退出场景时按进入前那批开关还原。", "scenes.quickPick.confirm": "确定", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "当前设置", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按快照里的正文恢复;快照存的是正文,删这份预设不影响恢复", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.select": "选择此会话", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入", "hist.btn.export": "导出", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "归档", "hist.stat.projects": "工作区", "hist.stat.retention": "保留期", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "工作区(可留空)", "hist.import.cwd.placeholder": "从已登记工作区选,或直接输入绝对路径;留空 = 归入未分组", "hist.import.workspace": "选工作区", "hist.import.workspace.none": "本机还没有已登记的工作区:可以直接输入绝对路径。", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.detail": "导出内容", "hist.export.detail.simple": "仅正文", "hist.export.detail.tools": "正文 + 工具", "hist.export.detail.full": "正文 + 工具 + 思考", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
5198
+ "scenes.field.quickPrompts.noneOption": "无", "scenes.field.quickPrompts.list": "本场景启用", "scenes.field.quickPrompts.pick": "选择", "scenes.quickPick.title": "选择本场景启用的快捷提示词", "scenes.quickPick.hint": "勾选的进入本场景后启用,没勾的停用;退出场景时按进入前那批开关还原。", "scenes.quickPick.confirm": "确定", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "当前设置", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按快照里的正文恢复;快照存的是正文,删这份预设不影响恢复", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "agm.agentsHome.present": "另有一份全局指令 {path} 也在注入(与别的 agent 工具共用);要改这里的内容去改那个文件。", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.select": "选择此会话", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入", "hist.btn.export": "导出", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "归档", "hist.stat.projects": "工作区", "hist.stat.retention": "保留期", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "工作区(可留空)", "hist.import.cwd.placeholder": "从已登记工作区选,或直接输入绝对路径;留空 = 归入未分组", "hist.import.workspace": "选工作区", "hist.import.workspace.none": "本机还没有已登记的工作区:可以直接输入绝对路径。", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.detail": "导出内容", "hist.export.detail.simple": "仅正文", "hist.export.detail.tools": "正文 + 工具", "hist.export.detail.full": "正文 + 工具 + 思考", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
4919
5199
  },
4920
5200
  en: {
4921
5201
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -4968,6 +5248,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
4968
5248
  "fm.label.name": "Name", "fm.label.description": "Description", "fm.label.when-to-use": "When to use", "fm.label.user-invocable": "User invocable", "fm.label.disable-model-invocation": "Model invocation disabled", "fm.label.allowed-tools": "Allowed tools", "fm.label.license": "License", "fm.label.version": "Version", "fm.label.author": "Author", "fm.label.category": "Category", "fm.label.tags": "Tags", "fm.label.metadata": "Metadata", "fm.label.homepage": "Homepage", "fm.yes": "Yes", "fm.no": "No",
4969
5249
  "error.rules.sceneNotFound": "Scene not found: {group} (create it on the Scenes page first)",
4970
5250
  "error.rules.reservedScene": "「{name}」 is a reserved scene: {action} is not allowed{reason}",
5251
+ "error.rules.sceneNotActive": "The scene is not started: start it before locking",
4971
5252
  "error.rules.singleSceneOnly": "Only one scene besides 「global」 may be enabled at a time{detail}",
4972
5253
  "error.agentsMd.sceneRebindFailed": "Cannot change the prompt binding of scene 「{scene}」 to 「{target}」: {reason}",
4973
5254
  "error.agentsMd.referenced": "“{id}” cannot be deleted: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "error.agentsMd.refProbeFailed": "Cannot tell whether “{id}” is still referenced, so the delete was refused: the reference probe failed (at least one of scene bindings / the current AGENTS.md content / the pre-scene baseline could not be read). Please try again later.", "error.agentsMd.noUnapplyBackup": "There is no “state from before the apply” to restore: the current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no starting point to go back to. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself.", "error.agentsMd.unapplyWhileScene": "Scene “{scene}” is driving the global baseline, so it cannot be un-applied here: leave the scene first, or change its prompt binding on the Scenes page.",
@@ -5004,6 +5285,23 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5004
5285
  "trash.preview.readonly": "Read-only preview: nothing can be edited and there is no save here.",
5005
5286
  "trash.preview.files": "{count} file(s)",
5006
5287
  "trash.preview.meta": "Source record",
5288
+ "trash.preview.entry": "Entry",
5289
+ "trash.preview.field.deletedAt": "Deleted at",
5290
+ "trash.preview.field.scene": "Scene",
5291
+ "trash.preview.field.form": "Form",
5292
+ "trash.preview.field.label": "Display name",
5293
+ "trash.preview.field.desc": "Description",
5294
+ "trash.preview.field.locked": "Locked",
5295
+ "trash.preview.field.mcp": "MCP",
5296
+ "trash.preview.sourceGone": "removed source",
5297
+ "trash.preview.field.skills": "Skills",
5298
+ "trash.preview.field.subagents": "Subagents",
5299
+ "trash.preview.field.quick": "Quick prompts",
5300
+ "trash.preview.field.toolTable": "Tool-table scheme",
5301
+ "trash.preview.field.prompt": "Prompt preset",
5302
+ "trash.preview.field.memories": "Memories deleted with the scene",
5303
+ "trash.preview.none": "(empty)",
5304
+ "trash.preview.yes": "yes",
5007
5305
  "trash.preview.field.scope": "Scope",
5008
5306
  "trash.preview.field.origin": "Origin",
5009
5307
  "trash.preview.field.path": "Folder",
@@ -5063,7 +5361,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5063
5361
  "mcp.msg.ok": "Done", "mcp.msg.failed": "Operation failed", "mcp.msg.loadFailed": "Load failed", "mcp.msg.warn": "Done, but the loader reported: {warning}", "mcp.msg.toolsRefreshed": "Fetched {count} tool(s)", "mcp.msg.restartWarn": "Restarted with warnings: {warning}", "mcp.msg.restarted": "Restarted: {name}",
5064
5362
  "mcp.form.addTitle": "Add MCP server", "mcp.form.editTitle": "Edit MCP server: ",
5065
5363
  "mcp.form.err.serverName": "Server name must be 1-32 chars of letters, digits, underscore or hyphen (no spaces, no CJK)", "mcp.form.err.url": "URL must start with http:// or https://", "mcp.form.err.command": "Command cannot be empty",
5066
- "mcp.form.maskedHint": "A masked value in a secret field (e.g. ••••••) is not the real value: saving leaves those entries untouched, because the plugin writes back the value found in the patch file. To read a secret in plain text, click Reveal secrets (if this plugin has an access token configured, enter the matching token first).",
5364
+ "mcp.form.maskedHint": "Secrets here are masked — you can edit them freely. To read one, open the details and click Reveal secrets.",
5067
5365
  "mcp.field.serverName": "Server name", "mcp.field.serverName.hint": "1-32 chars of [A-Za-z0-9_-]; the patch registers it under this name",
5068
5366
  "mcp.field.note": "Note (optional)", "mcp.field.note.placeholder": "A hint for the model, e.g. \“this one needs passwordless sudo\”; empty means nothing is stored",
5069
5367
  "mcp.field.transport": "Transport", "mcp.field.level": "Level",
@@ -5078,7 +5376,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5078
5376
  "mcp.compact.confirm": "Tidy now", "mcp.compact.done": "Patch file tidied: cleared {count} stale on/off record(s); no server changed state", "mcp.compact.clean": "The patch file is already clean — nothing stale to clear",
5079
5377
  "mcp.detail.entryId": "Entry id", "mcp.detail.status": "Status", "mcp.detail.loader": "Loader registration", "mcp.detail.loader.yes": "Registered", "mcp.detail.loader.no": "Not registered",
5080
5378
  "mcp.tools.stale": "from last run", "mcp.tools.expand": "Show more", "mcp.tools.collapse": "Show less",
5081
- "mcp.detail.note": "Note (sent to the model with the injected runtime snapshot)", "mcp.detail.note.placeholder": "e.g. fall back to B when A is unavailable", "mcp.detail.note.save": "Save note",
5379
+ "mcp.detail.note": "Note (sent to the model with the injected runtime snapshot)", "mcp.detail.note.empty": "No note yet",
5380
+ "mcp.msg.noteFailed": "The server was saved, but the note was not written.",
5082
5381
  "mcp.detail.tools": "Tools ({count})", "mcp.detail.toolsRefresh": "Refresh tools",
5083
5382
  "mcp.field.headersShort": "Headers", "mcp.field.envShort": "Environment",
5084
5383
  "mcp.tools.loading": "Fetching the tool list…", "mcp.tools.loadFailed": "Could not load tools: ", "mcp.tools.none": "This server has no registered tools.",
@@ -5123,6 +5422,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5123
5422
  "tabs.scenes": "Scenes", "tabs.skills": "Skills", "tabs.subagents": "Subagents", "tabs.prompts": "Prompts", "tabs.memory": "Memories", "tabs.sessions": "Sessions", "tabs.compat": "Host",
5124
5423
  "compat.title": "Host compatibility", "compat.desc": "How this plugin attaches to DSH (read-only).",
5125
5424
  "compat.backups.btn": "Clean old backups", "compat.backups.btnCount": "Clean old backups · {count}", "compat.backups.btnHint": "Delete surplus config backup copies (they hold secrets in cleartext)", "compat.backups.btnTitle": "{count} config backups exist ({size}, secrets in cleartext)", "compat.backups.title": "Clean old backups",
5425
+ "compat.sidebar.hide": "Hide from sidebar", "compat.sidebar.show": "Show in sidebar", "compat.sidebar.hideHint": "Remove this plugin's row from the sidebar (Settings → Tools Management still works)", "compat.sidebar.showHint": "Add this plugin's row to the sidebar; it opens the same pages right here", "compat.sidebar.failed": "Could not save the sidebar toggle",
5126
5426
  "compat.backups.desc": "Every backup is a full copy of the config — MCP secrets and the access token included, in cleartext.",
5127
5427
  "snapshot.title": "Machine migration", "snapshot.hint": "Carry MCP, skills, memories, personas, prompts, scene profiles and the sidecar configs in one go, then import them on the other machine.", "snapshot.browse": "Browse",
5128
5428
  "snapshot.export.dir": "Export directory", "snapshot.export.btn": "Export", "snapshot.export.busy": "Exporting…", "snapshot.export.done": "Snapshot written to: {dir}", "snapshot.export.failed": "Export failed",
@@ -5155,7 +5455,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5155
5455
  "compat.audit.op.personaSave": "Saved a persona", "compat.audit.op.personaDelete": "Deleted a persona", "compat.audit.op.personaImport": "Imported a persona", "compat.audit.op.personaSwitch": "Enabled/disabled a persona",
5156
5456
  "compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.quickSave": "Saved a quick prompt", "compat.audit.op.quickSwitch": "Toggled a quick prompt", "compat.audit.op.quickDelete": "Deleted a quick prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
5157
5457
  "compat.audit.op.trashRestore": "Restored from trash", "compat.audit.op.trashDelete": "Deleted forever",
5158
- "compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.backupsClean": "Cleaned old backups",
5458
+ "compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.sidebarSettings": "Sidebar entry", "compat.audit.op.backupsClean": "Cleaned old backups",
5159
5459
  "compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.trashRetention": "Changed trash retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
5160
5460
  "compat.backups.count": "{count} copies, {size} in total",
5161
5461
  "compat.backups.del": "How many to delete:", "compat.backups.delOption": "{count}",
@@ -5179,6 +5479,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5179
5479
  "compat.op.native": "Host-native entry", "compat.op.adapter": "Plugin adapter", "compat.op.unavailable": "Unavailable (disabled)",
5180
5480
  "compat.degraded": "Degraded capabilities", "compat.degraded.hint": "what is missing + the consequence", "compat.reason": "Reason:",
5181
5481
  "compat.others": "Other capability slots", "compat.others.hint": "not degraded: the plugin supplies its own implementation when the host has no such entry",
5482
+ "compat.contracts": "Host contracts", "compat.contracts.hint": "host members this release depends on: absence never throws, but the matching feature loses half of itself", "compat.contracts.missing": "absent", "compat.contracts.allOk": "Every host contract this release depends on is present.",
5483
+ "compat.bareModules": "Bare module resolution", "compat.bareModules.hint": "official packages the plugin requires by bare name at runtime; they are not peers, so they take no part in the module comparison above", "compat.bareModules.present": "present", "compat.bareModules.absent": "not shipped by host", "compat.bareModules.drift": "contract drift suspected", "compat.bareModules.presentWhy": "affects: {feature}", "compat.bareModules.absentWhy": "the host no longer ships this package: {feature} degrades", "compat.bareModules.driftWhy": "{feature} is missing contract markers ({markers}) — please re-check that package's implementation manually",
5182
5484
  "compat.fallback.substituted": "Plugin adapter (no host entry)",
5183
5485
  "compat.fallback.native": "Falls back to native entry", "compat.fallback.refuse": "Operation refused", "compat.fallback.blocked": "Affected buttons disabled", "compat.fallback.inform": "Writes unaffected",
5184
5486
  "compat.modules": "Module identity (plugin vs host)", "compat.modules.hint": "adapting only works on one shared module", "compat.module.same": "same module", "compat.module.separate": "separate copy (repair needed)", "compat.module.sameVersion": "same version (inside/outside the archive)", "compat.module.unknown": "not comparable", "compat.module.realpath": "not comparable (real path unavailable)", "compat.modules.realpath": "{count} package(s) could **not be compared**: their real path could not be read ({names}) — that is not \"a separate copy\", it means the check could not run.", "compat.modules.realpathSides": "Host side: {host}; plugin side: {plugin}. Fix the path (permissions / path length / reinstall) and reopen this page to re-check.", "compat.modules.realpathNone": "none", "compat.modules.unverified": "{count} package(s) could not be compared (not resolvable from the host anchor: {names}) — that is not \"a separate copy\", it means the check could not run. Try node scripts/doctor.mjs.",
@@ -5210,7 +5512,11 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5210
5512
  "compat.ov.nativeDelete": "Host native delete entry", "compat.ov.nativeDelete.ok": "The host offers no native delete entry (this plugin ships its own full sequence)",
5211
5513
  "compat.ov.hostIdentity": "Host identity & modules", "compat.ov.hostIdentity.ok": "Modules are shared with the host (host DSH {version})", "compat.ov.hostIdentity.asar": "Official packages match the host version (DSH {version}), but the host runs inside {name} while the plugin sits outside it", "compat.ov.hostIdentity.degraded": "Blockers present: {list}",
5212
5514
  "compat.ov.memory": "Memory", "compat.ov.prompts": "Prompts", "compat.ov.subagents": "Subagents", "compat.ov.scenes": "Scene profiles",
5515
+ "compat.ov.subagentPersona": "Subagent persona delegation", "compat.ov.subagentToolRestrict": "Subagent tool restriction", "compat.ov.subagentCreatedEvent": "Subagent creation ordering", "compat.ov.webAdmission": "Web connection admission",
5213
5516
  "compat.ov.plain.ok": "No degradation reported (assembly failures would appear here)",
5517
+ "compat.ov.contract.ok": "Host contract present (its absence would silently halve the feature)",
5518
+ "compat.ov.contract.degraded": "Host contract absent: {detail}",
5519
+ "compat.ov.domain.contract": "Required host modules present: {detail}",
5214
5520
  // Slash-command "Tools" section (chat composer). Domain labels stay short — the row keeps
5215
5521
  // room on the right for the summary and the drill chevron.
5216
5522
  "slash.section.tools": "Tools",
@@ -5254,7 +5560,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5254
5560
  "trash.group.quick": "Quick prompts",
5255
5561
  "trash.section.quick.sub": "Quick-prompt folders were moved: restore brings them back, delete-forever removes them",
5256
5562
  "slash.section.quick": "Quick prompts",
5257
- "slash.quick.empty": "This quick prompt has no text — add it under Settings → Tools → Prompts",
5563
+ "slash.quick.empty": "This quick prompt has no text — add it under Settings → Tools Management → Prompts",
5258
5564
  "compat.ov.slashCommands": "Slash command entry", "compat.ov.slashCommands.on": "Both on (type / for the “Tools” and “Quick prompts” blocks)",
5259
5565
  "compat.ov.slashCommands.onTools": "Only the “Tools” section is on", "compat.ov.slashCommands.onQuick": "Only the “Quick prompts” section is on",
5260
5566
  "compat.ov.slashCommands.off": "Both sections turned off (re-enable them on this page)",
@@ -5289,6 +5595,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5289
5595
  "compat.cap.workspaceIndexHeader": "Index session headers", "compat.cap.workspaceArchiveNative": "Host native archive entry", "compat.cap.workspaceUnarchiveNative": "Host native restore entry",
5290
5596
  "compat.cap.workspaceBatchNative": "Host native batch entry", "compat.cap.workspaceDeleteNative": "Host native delete entry", "compat.cap.sessionsDetachLive": "Persist & detach live sessions",
5291
5597
  "compat.cap.sessionsColdAnnounce": "Cold session removal broadcast", "compat.cap.projectionWrite": "Projection cache write path", "compat.cap.projectionTableDelete": "Projection cache row deletion",
5598
+ "compat.cap.agentPresetRegister": "Temporary child-preset registration", "compat.cap.toolsRestrictMethod": "Tool restriction (restrict)", "compat.cap.agentCreatedEvent": "Subagent creation event (agent/created)", "compat.cap.connectionAdmit": "Connection admission (admit)",
5292
5599
  "compat.cap.opArchive": "Archive", "compat.cap.opUnarchive": "Restore", "compat.cap.opBatch": "Batch operations", "compat.cap.opDelete": "Delete", "compat.cap.opList": "History list",
5293
5600
  "compat.reach": "Preset injection reach", "compat.reach.hint": "whether these contents reach the model under each preset",
5294
5601
  "compat.reach.suppressing": "suppressing",
@@ -5302,7 +5609,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5302
5609
  "compat.token.sentence.off": "Writes no longer need a token; revealing plaintext still does.",
5303
5610
  "compat.token.sentence.locked": "Config changes will be rejected.",
5304
5611
  "compat.token.sentence.unlocked": "Unlocked for this run; unlock again after DSH restarts.",
5305
- "compat.token.lock.composer": "Access token not entered — chatting is off until you fill it in (Tools → Compat)",
5612
+ "compat.token.lock.composer": "Access token not entered — chatting is off until you fill it in (Tools Management → Compat)",
5306
5613
  "compat.token.banner": "Access token not verified: prompts you send now are refused by the Host (the message never enters the conversation).",
5307
5614
  "token.dialog.title": "Enter the access token",
5308
5615
  "compat.token.hostHint": "Written to the config file; takes effect after a DSH restart.",
@@ -5327,7 +5634,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5327
5634
  "compat.token.clearHost.btn": "Delete token", "compat.token.offHost": "Turn protection off", "compat.token.offHost.btn": "Turn off",
5328
5635
  "compat.token.onHost": "Turn protection on", "compat.token.onHost.btn": "Turn on", "compat.token.onConfirm.yes": "Turn it on",
5329
5636
  "compat.token.pendingRestart": "The change is not in effect yet (the pill describes the running process): restart DSH and it becomes the current state.",
5330
- "compat.token.notFound": "No token is set right now — click “Set token”, or use the DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN environment variable.",
5637
+ "compat.token.notFound": "No token set — click “Set token”, or set DSH_PLUGIN_TOOL_MANAGEMENT_TOKEN.",
5331
5638
  "compat.token.edit.current": "Current token", "compat.token.edit.new": "New token", "compat.token.edit.repeat": "Type it again", "compat.token.edit.save": "Save new token", "compat.token.edit.cancel": "Cancel",
5332
5639
  "compat.token.edit.mismatch": "The two entries do not match.",
5333
5640
  "compat.token.empty": "Enter a token first",
@@ -5482,7 +5789,6 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5482
5789
  "scenes.skills.shadowed.hint": "Shadowed by a same-name skill, so it can never take effect in this scene and cannot be checked. To use this copy instead, click “Use this one” for it on the Skills page.",
5483
5790
  "scenes.skills.invalid.hint": "Incomplete skill (missing frontmatter / invalid name / empty description): it cannot be enabled or disabled, so it cannot be checked.", "scenes.subagents.hint": "Check the personas this scene should turn on; anything unchecked (or with no section at all) is turned off when you enter, and restored to its previous switch when you exit.", "scenes.subagents.hint.short": "Add the section, then check the personas to turn on", "scenes.subagents.empty": "No personas yet — create one on the Subagents page.",
5484
5791
  "scenes.filter.skills": "Filter skills / directories", "scenes.filter.subagents": "Filter personas (name or description)", "scenes.filter.servers": "Filter servers",
5485
-
5486
5792
  "scenes.mem.title": "Memories", "scenes.mem.search": "Filter memories in this scene (name or description)",
5487
5793
  "scenes.mem.noMatch": "Nothing matches", "scenes.mem.emptyScene": "This scene has no memories yet",
5488
5794
  "scenes.mem.createNeedScene": "Fill in the scene name first", "scenes.mem.pending": "{count} pending", "scenes.mem.pendingTag": "Pending", "scenes.mem.attach.tip": "Carries {count} attachment(s) · {size} total", "scenes.mem.removePending": "Remove",
@@ -5609,8 +5915,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5609
5915
  "import.clear": "Clear", "import.submit": "Import",
5610
5916
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
5611
5917
  "error.import.noFiles": "No files selected",
5612
- "nav.title": "Tools",
5613
- "agm.stat.total": "Global prompts", "agm.stat.active": "In effect", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.fileApplied": "it is applied in AGENTS.md (the one currently in effect)", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.inactive": "Not applied", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New", "agm.new.title": "New prompt", "agm.btn.import": "Import", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "Display name", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "agm.unapply.title": "Un-apply", "agm.unapply.body": "This restores ~/.dsh/AGENTS.md to the content from before you started applying presets (kept in __last-applied__); no preset takes effect afterwards.", "agm.unapply.note": "What comes back is the content from before you started applying: presets you applied in between are rolled back too — to switch to one of them instead, just flip its own switch (same result). This step is backed up too, so you can apply it again.", "agm.btn.confirmUnapply": "Un-apply", "agm.unapply.switch.on": "This one is in AGENTS.md · click to un-apply (restore AGENTS.md to the content from before you started applying)", "agm.unapply.switch.blocked": "This one is in AGENTS.md, but there is no “state from before the apply” to restore — it was not written by “Apply”, so it cannot be turned off. To change the file, apply another preset or edit AGENTS.md directly; deleting this preset does not touch AGENTS.md", "agm.unapply.switch.onRemove": "This one is in AGENTS.md · click to un-apply (there was no AGENTS.md before you started applying, so this deletes the file)", "agm.unapply.bodyRemove": "There was no ~/.dsh/AGENTS.md before you started applying presets, so un-applying deletes that file instead of restoring content; no preset takes effect afterwards.", "agm.unapply.noteRemove": "What gets deleted is the content “Apply” wrote; that content is backed up to __last-applied__ (last 5 kept), so you can apply it back if needed.", "agm.btn.confirmUnapplyRemove": "Un-apply and delete the file", "agm.result.unapplied": "Un-applied: AGENTS.md is back to the content from before you started applying", "agm.result.unappliedRemove": "Un-applied: AGENTS.md was deleted (it did not exist before you started applying)", "agm.unapply.unavailable": "The current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no “state from before the apply” to restore and it cannot be turned off. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself; deleting this preset does not touch AGENTS.md.", "agm.remove.fileNote": "Note: AGENTS.md still holds this content and your global prompt stays in effect — deleting the preset only moves it to the trash and does not touch AGENTS.md. To stop it taking effect, edit AGENTS.md itself or apply another preset.", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Original folder missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Global prompt",
5918
+ "nav.title": "Tools Management",
5919
+ "agm.stat.total": "Global prompts", "agm.stat.active": "In effect", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Remove that reference first — for a scene binding, rebind the prompt on the scene page or leave the scene; for the one currently applied, flip the switch on the left to un-apply.", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.fileApplied": "it is applied in AGENTS.md (the one currently in effect)", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "In scene “{scene}”: you can still change settings — edits sync to the profile and the state is restored when you leave.", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.inactive": "Not applied", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New", "agm.new.title": "New prompt", "agm.btn.import": "Import", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "Display name", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "agm.unapply.title": "Un-apply", "agm.unapply.body": "This restores ~/.dsh/AGENTS.md to the content from before you started applying presets (kept in __last-applied__); no preset takes effect afterwards.", "agm.unapply.note": "What comes back is the content from before you started applying: presets you applied in between are rolled back too — to switch to one of them instead, just flip its own switch (same result). This step is backed up too, so you can apply it again.", "agm.btn.confirmUnapply": "Un-apply", "agm.unapply.switch.on": "This one is in AGENTS.md · click to un-apply (restore AGENTS.md to the content from before you started applying)", "agm.unapply.switch.blocked": "This one is in AGENTS.md, but there is no “state from before the apply” to restore — it was not written by “Apply”, so it cannot be turned off. To change the file, apply another preset or edit AGENTS.md directly; deleting this preset does not touch AGENTS.md", "agm.unapply.switch.onRemove": "This one is in AGENTS.md · click to un-apply (there was no AGENTS.md before you started applying, so this deletes the file)", "agm.unapply.bodyRemove": "There was no ~/.dsh/AGENTS.md before you started applying presets, so un-applying deletes that file instead of restoring content; no preset takes effect afterwards.", "agm.unapply.noteRemove": "What gets deleted is the content “Apply” wrote; that content is backed up to __last-applied__ (last 5 kept), so you can apply it back if needed.", "agm.btn.confirmUnapplyRemove": "Un-apply and delete the file", "agm.result.unapplied": "Un-applied: AGENTS.md is back to the content from before you started applying", "agm.result.unappliedRemove": "Un-applied: AGENTS.md was deleted (it did not exist before you started applying)", "agm.unapply.unavailable": "The current AGENTS.md was not written by “Apply” (the preset created automatically the first time you open this page is one such case), so there is no “state from before the apply” to restore and it cannot be turned off. To change what AGENTS.md holds: apply another preset, or edit AGENTS.md itself; deleting this preset does not touch AGENTS.md.", "agm.remove.fileNote": "Note: AGENTS.md still holds this content and your global prompt stays in effect — deleting the preset only moves it to the trash and does not touch AGENTS.md. To stop it taking effect, edit AGENTS.md itself or apply another preset.", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Original folder missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Global prompt",
5614
5920
  "scenes.field.prompt.none": "None", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.enter.personas": "{count} subagent(s) enabled: {names}", "scenes.preview.enter.personasNone": "No subagent stays enabled",
5615
5921
  "scenes.preview.bind.prompt": "Global prompt: switch to “{name}”",
5616
5922
  "scenes.preview.bind.promptSame": "Global prompt: unchanged (still “{name}”)",
@@ -5624,7 +5930,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5624
5930
  "scenes.preview.bind.tool": "Tool table: switch to scheme “{name}” (hides {count})",
5625
5931
  "scenes.preview.bind.toolMissing": "Tool table: scheme “{name}” no longer exists — nothing switches this time",
5626
5932
  "scenes.preview.bind.toolKeep": "Tool table: stays as it is (this scene binds none)", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "Current settings", "scenes.field.quickPrompts.customCount": "Custom ({picked}/{total})", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page",
5627
- "scenes.field.quickPrompts.noneOption": "None", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.field.quickPrompts.pick": "Choose", "scenes.quickPick.title": "Choose the quick prompts enabled in this scene", "scenes.quickPick.hint": "Checked ones are enabled when you enter this scene, unchecked ones are disabled; leaving the scene restores the switches from before you entered.", "scenes.quickPick.confirm": "Done", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Current settings", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it from the snapshot's text, so deleting this preset does not break the restore", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.select": "Select this session", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import", "hist.btn.export": "Export", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": "Archived", "hist.stat.projects": "Workspaces", "hist.stat.retention": "Retention", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick a registered workspace or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path.", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
5933
+ "scenes.field.quickPrompts.noneOption": "None", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.field.quickPrompts.pick": "Choose", "scenes.quickPick.title": "Choose the quick prompts enabled in this scene", "scenes.quickPick.hint": "Checked ones are enabled when you enter this scene, unchecked ones are disabled; leaving the scene restores the switches from before you entered.", "scenes.quickPick.confirm": "Done", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Current settings", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it from the snapshot's text, so deleting this preset does not break the restore", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "agm.agentsHome.present": "Another global instruction file at {path} is also injected (shared with other agent tools); edit that file to change it.", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.select": "Select this session", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import", "hist.btn.export": "Export", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": "Archived", "hist.stat.projects": "Workspaces", "hist.stat.retention": "Retention", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick a registered workspace or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path.", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
5628
5934
  }
5629
5935
  };
5630
5936
 
@@ -5911,7 +6217,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5911
6217
  * 原文:"the pre-step rejection that produced it discarded the claimed messages")。
5912
6218
  * 没有这条横幅,用户看到的就是"消息没了,也没有任何解释"。
5913
6219
  *
5914
- * 出口写在文案里:解铃在「设置 → 工具 → 兼容」的访问令牌框 —— 那条路全是只读 op,
6220
+ * 出口写在文案里:解铃在「设置 → 工具管理 → 兼容」的访问令牌框 —— 那条路全是只读 op,
5915
6221
  * 令牌没验过照样打得开(见 request-gate.ts 的白名单)。
5916
6222
  */
5917
6223
  let tokenBanner = null
@@ -5942,7 +6248,7 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
5942
6248
  tokenBannerText.className = 'dsm-token-banner-text'
5943
6249
  tokenBanner.appendChild(tokenBannerText)
5944
6250
  // 「填写令牌」:点开一个自包含的弹窗,填对自动关(用户 2026-10-12 提案)。
5945
- // 此前这颗键是"跳到设置 → 工具 → 兼容",但横幅是在**任何**页面都可能出现的,
6251
+ // 此前这颗键是"跳到设置 → 工具管理 → 兼容",但横幅是在**任何**页面都可能出现的,
5946
6252
  // 从对话页点它时设置面板往往根本没开 —— 而宿主没有"打开设置"的公开服务,
5947
6253
  // 于是那颗键要么没反应、要么得靠认领宿主 DOM 里自己那一行导航(又脆又绕)。
5948
6254
  // 改成弹窗之后,这条键不再依赖用户当时在哪一页,也不再碰宿主 DOM。
@@ -6079,6 +6385,166 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6079
6385
  // 「整串都是 `•`」,客户端再引入 `****` 会让那个判据失效。
6080
6386
 
6081
6387
  // 顶部栏右端:GitHub 链接(issues 在仓库)+ 版本号(全界面唯一显示处)。
6388
+
6389
+ // ── 设置侧栏「工具管理」那一行的图标(2026-10-18)─────────────────────────
6390
+ //
6391
+ // 为什么需要它:宿主 ui-settings-general 的 SettingsPanel 用 `navIcon(row.id)`
6392
+ // 挑侧栏字形,那是一张封闭名单 —— account / models / agent-presets /
6393
+ // plugins / archived-sessions 各有专用图标,**其余 id 一律落到默认齿轮**。
6394
+ // `settings.section` 这个 slot 只投影 id / order / label 三项(宿主 bundle 里
6395
+ // SlotCore 的注册描述符与运行时 slot 清单都只有这三个字段),注册方没有图标可传,
6396
+ // 所以每一个第三方分区都戴着那颗齿轮,本插件也是。
6397
+ //
6398
+ // 做法(dshmarket / dsh-better-sidebar / dsh-skill-mcp-panel 三家都是这么干的,
6399
+ // 先例见 dshmarket 的 settings-nav-icon.ts):本插件的客户端 fiber 在场时,
6400
+ // 认领设置面板里**可见文字等于本分区当前 label** 的那一行,给它打一个标记属性,
6401
+ // 再用一条样式把宿主的齿轮 svg 藏起来、在 ::before 上画我们自己的图。
6402
+ //
6403
+ // 几条边界,逐条对应"会不会出事":
6404
+ // · 认领判据只有**文案全等**(isOwnNavRow)。空 label 匹配不到任何行 ——
6405
+ // 语言还没解析出来时不能把整条 nav 都标上。
6406
+ // · 只给标记加属性、注入一条样式,**不碰宿主的 DOM 结构与 React 树**。
6407
+ // · 样式与 observer 都长在 ctx.effect 里,fiber 卸载时一起撤(撤的时候顺手
6408
+ // 把标记擦掉,不会留下污染给下一个打开的面板)。
6409
+ // · 宿主重绘面板时 React 会把我们换掉的齿轮**改回来**,所以用 MutationObserver
6410
+ // 盯着 document.body 重新贴;这一点不是锦上添花,是这条路能成立的唯一原因。
6411
+ // · 宿主哪天给 settings.section 加了 icon 字段,这个模块(连同调用点)就可以删。
6412
+ //
6413
+ // 图形用 CSS `mask-image` 而不是 <img>:mask 只读 alpha,可见颜色取自元素的
6414
+ // `background-color: currentColor`,于是侧栏选中/悬停/主题切换时图标自动跟着变,
6415
+ // 与旁边那排宿主图标完全一致。<img> 做不到这一点(它是死颜色)。
6416
+ // 代价是 SVG 必须**纯黑**绘制 —— mask 只认 alpha,任何非黑填充都会变成实心块。
6417
+ //
6418
+ // ⚠️ 本片整段都在 `apply()` 体内(40 片那个 `apply(ctx) {` 之后的所有分片都是),
6419
+ // 所以 `installSettingsNavIcon(...)` 的调用必须写在**这些 const 之后** ——
6420
+ // 写在 40 片那个位置会撞 const 的 TDZ(`Cannot access 'NAV_ICON_MARKER'
6421
+ // before initialization`),宿主当场 boot 失败。2026-10-18 就是这么挂的。
6422
+
6423
+ /** 标记本插件自己那一行的属性名(前缀跟着本插件,避免与别家插件的标记撞名)。 */
6424
+ var NAV_ICON_MARKER = 'data-dsm-nav-icon'
6425
+
6426
+ /**
6427
+ * 设置面板的侧栏行。宿主把每个 settings.section 条目渲染成面板 <nav> 里的
6428
+ * 一个 <button>(SettingsPanel 在 dsh-client-ui-settings-general)。
6429
+ */
6430
+ var NAV_ROW_SELECTOR = '[role="dialog"] nav button'
6431
+
6432
+ /**
6433
+ * 图标本体:三层叠放的方块(工具管理),取自 docs/images/icon.svg。
6434
+ *
6435
+ * 纯黑是刻意的(见上面说明):mask 只读 alpha。viewBox 与源文件一致(1024),
6436
+ * 渲染尺寸由 CSS 的 mask-size 决定,所以这里不写宽高。
6437
+ */
6438
+ function navIconMaskSvg() {
6439
+ return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" fill="#000">'
6440
+ + '<path d="M592 336h-416c-52.928 0-96 43.072-96 96v416c0 52.928 43.072 96 96 96h416c52.928 0 96-43.072 96-96v-416c0-52.928-43.072-96-96-96z m32 512a32 32 0 0 1-32 32h-416c-17.632 0-32-14.336-32-32v-416c0-17.632 14.368-32 32-32h416c17.664 0 32 14.368 32 32v416z"/>'
6441
+ + '<path d="M720 208h-416a32 32 0 0 0 0 64h416c17.664 0 32 14.368 32 32v416a32 32 0 1 0 64 0v-416c0-52.928-43.072-96-96-96zM528 752h-288a32 32 0 1 0 0 64h288a32 32 0 1 0 0-64z"/>'
6442
+ + '<path d="M848 80h-416a32 32 0 0 0 0 64h416c17.664 0 32 14.368 32 32v416a32 32 0 1 0 64 0v-416c0-52.928-43.072-96-96-96z"/>'
6443
+ + '</svg>'
6444
+ }
6445
+
6446
+ /** mask 的 data URL(运行时编码,不手写转义)。 */
6447
+ function navIconMaskUrl(svg) {
6448
+ return 'data:image/svg+xml,' + encodeURIComponent(svg || navIconMaskSvg())
6449
+ }
6450
+
6451
+ /**
6452
+ * 这一行是不是本插件的。
6453
+ *
6454
+ * 纯函数,也是这套逻辑**唯一**的判定:可见文字等于宿主此刻正在投影的本分区
6455
+ * label。空 label 一律不匹配 —— 语言没解析出来时不能把整条 nav 都认下来。
6456
+ */
6457
+ function isOwnNavRow(rowText, wantedLabel) {
6458
+ var wanted = String(wantedLabel == null ? '' : wantedLabel).trim()
6459
+ if (wanted.length === 0) return false
6460
+ return String(rowText == null ? '' : rowText).trim() === wanted
6461
+ }
6462
+
6463
+ /**
6464
+ * 标记行的样式:藏掉宿主的齿轮,在 ::before 上画我们的图。
6465
+ *
6466
+ * 16px 与宿主那排字形同尺寸(它的 navIcon 走 size: 16),位置由 flex 排在
6467
+ * 标签之前 —— 按钮本身是 flex 行,::before 作为第一个子元素天然就位。
6468
+ */
6469
+ function navIconCss(maskUrl) {
6470
+ return [
6471
+ '[' + NAV_ICON_MARKER + '] > svg { display: none; }',
6472
+ '[' + NAV_ICON_MARKER + ']::before {',
6473
+ " content: '';",
6474
+ ' flex: none;',
6475
+ ' width: 16px;',
6476
+ ' height: 16px;',
6477
+ ' background-color: currentColor;',
6478
+ ' -webkit-mask-image: url("' + maskUrl + '");',
6479
+ ' mask-image: url("' + maskUrl + '");',
6480
+ ' -webkit-mask-repeat: no-repeat;',
6481
+ ' mask-repeat: no-repeat;',
6482
+ ' -webkit-mask-position: center;',
6483
+ ' mask-position: center;',
6484
+ ' -webkit-mask-size: 16px 16px;',
6485
+ ' mask-size: 16px 16px;',
6486
+ '}'
6487
+ ].join('\n')
6488
+ }
6489
+
6490
+ /**
6491
+ * 装上侧栏图标。
6492
+ *
6493
+ * @param ctx 客户端上下文(effect 的归属者)。
6494
+ * @param resolveLabel 本分区当前 label —— 与 settings.section 注册时传的是
6495
+ * 同一个 thunk,所以切语言时两边永远一致(label 变了,判据跟着变,不会认错行)。
6496
+ */
6497
+ function installSettingsNavIcon(ctx, resolveLabel) {
6498
+ if (typeof document === 'undefined') return
6499
+ ctx.effect(() => {
6500
+ const tag = document.createElement('style')
6501
+ tag.textContent = navIconCss(navIconMaskUrl())
6502
+ document.head.appendChild(tag)
6503
+ let disposed = false
6504
+ let scheduled = false
6505
+ const sync = () => {
6506
+ scheduled = false
6507
+ if (disposed) return
6508
+ const wanted = resolveLabel()
6509
+ const rows = document.querySelectorAll(NAV_ROW_SELECTOR)
6510
+ for (let i = 0; i < rows.length; i += 1) {
6511
+ const row = rows[i]
6512
+ if (isOwnNavRow(row.textContent, wanted)) row.setAttribute(NAV_ICON_MARKER, '')
6513
+ else row.removeAttribute(NAV_ICON_MARKER)
6514
+ }
6515
+ }
6516
+ // 宿主每次重绘面板(打开/关闭、切语言、内容变化)都会重建那几行,
6517
+ // 重建后我们换掉的齿轮会被 React 改回去 —— 所以必须能重新贴。
6518
+ // queueMicrotask 合批:一次交互往往连触发多轮 mutation,不必每条都扫一遍。
6519
+ const schedule = () => {
6520
+ if (scheduled || disposed) return
6521
+ scheduled = true
6522
+ queueMicrotask(sync)
6523
+ }
6524
+ sync()
6525
+ const observer = new MutationObserver(schedule)
6526
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true })
6527
+ return () => {
6528
+ disposed = true
6529
+ observer.disconnect()
6530
+ const marked = document.querySelectorAll('[' + NAV_ICON_MARKER + ']')
6531
+ for (let i = 0; i < marked.length; i += 1) marked[i].removeAttribute(NAV_ICON_MARKER)
6532
+ tag.remove()
6533
+ }
6534
+ }, 'dsh-plugin-tool-management: settings nav icon')
6535
+ }
6536
+
6537
+ // 调用点放在本片末尾(见上面 ⚠️ 那段):必须在这些 const 初始化之后。
6538
+ //
6539
+ // 整段包在 try 里:侧栏图标是**纯装饰**,取不到 DOM 或宿主换了面板结构时最坏
6540
+ // 应该只是"还是那颗齿轮",绝不能把 apply 带崩 —— 2026-10-18 那次 TDZ 就是
6541
+ // 一个装饰性功能让整个宿主 boot 失败("entry did not activate")。
6542
+ try {
6543
+ installSettingsNavIcon(ctx, function () { return t('nav.title') })
6544
+ } catch (e) {
6545
+ try { console.warn('[dsh-plugin-tool-management] 设置侧栏图标不可用,沿用宿主默认图标:' + errMsg(e)) } catch (e2) { /* 控制台不可用 */ }
6546
+ }
6547
+
6082
6548
  function FeedbackLinks() {
6083
6549
  return React.createElement('div', { className: 'dsm-feedback-links' },
6084
6550
  React.createElement('a', { className: 'dsm-feedback-link', href: 'https://github.com/ouli-1242/dsh-plugin-tool-management', target: '_blank', rel: 'noreferrer', 'aria-label': t('link.project'), title: t('link.project') },
@@ -6109,14 +6575,6 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6109
6575
  const [confirmRow, setConfirmRow] = React.useState(null)
6110
6576
  const [reveal, setReveal] = React.useState(false)
6111
6577
  const [settings, setSettings] = React.useState(null)
6112
- const [noteDraft, setNoteDraft] = React.useState('')
6113
- // 备注保存的**就地**回执(用户 2026-10-12:「mcp 备注在锁定情况下这个红色的提示词
6114
- // 不应该显示在 mcp 页,应该显示在详细页」)。此前 saveNote 走的是共用的 `run()`,
6115
- // 结果一律 setMsg —— 那是**页面级**提示条,而动作发生在弹窗里:锁定期间点「保存备注」
6116
- // 被冻结闸拒掉,红字却出现在弹窗**后面**的页面上,关上弹窗才看得见,根本对不上号。
6117
- // 备注这一个动作的回执只属于这个弹窗,所以单独一个槽(成功 / 失败 / 告警都走它),
6118
- // 打开与关闭详情时清空,只在备注小节里渲染。
6119
- const [noteNotice, setNoteNotice] = React.useState(null)
6120
6578
  const [compactConfirm, setCompactConfirm] = React.useState(false)
6121
6579
  // A1:粘贴 JSON 导入 / 导出两个弹窗(0.15.0)。
6122
6580
  const [jsonImportOpen, setJsonImportOpen] = React.useState(false)
@@ -6280,6 +6738,11 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6280
6738
  headers: kvToLines(row.headers),
6281
6739
  env: kvToLines(row.env),
6282
6740
  level: row.level === 'global' ? 'global' : 'project',
6741
+ // 备注也在编辑表单里改(用户 2026-10-18:「mcp 备注编辑功能应该在编辑页,
6742
+ // 详细页只能看」):详情页是只读视图,写动作一律回到这张表。
6743
+ // `origNote` 留一份原文用于判断"这次到底改没改备注"—— 没改就不多写一次侧车。
6744
+ note: row.notes || '',
6745
+ origNote: row.notes || '',
6283
6746
  })
6284
6747
  }
6285
6748
  const closeForm = () => { setFormError(null); setFormModal(null) }
@@ -6323,7 +6786,17 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6323
6786
  if (payload.mode === 'edit') args.id = payload.id
6324
6787
  // 备注只在新增时带:服务端在写补丁的同一次锁里落进侧车(见 mcpmAdd)。
6325
6788
  else args.note = payload.note
6326
- run(payload.mode === 'edit' ? 'mcpm-edit' : 'mcpm-add', args, 'form', closeForm)
6789
+ // 编辑态:备注在**保存之后**单独写一次(`mcpm-edit` 不碰侧车)。改过才写 ——
6790
+ // 没改就多一次写盘 + 刷新 updatedAt,没有意义。
6791
+ const noteChanged = payload.mode === 'edit' && String(payload.note || '').trim() !== String(payload.origNote || '').trim()
6792
+ run(payload.mode === 'edit' ? 'mcpm-edit' : 'mcpm-add', args, 'form', function () {
6793
+ closeForm()
6794
+ if (noteChanged) {
6795
+ apiCall('mcpm-note', { id: payload.id, note: payload.note }).then(function (res) {
6796
+ if (res && res.ok === false) setMsg({ kind: 'warn', text: mt('mcp.msg.noteFailed') })
6797
+ }).catch(function () { setMsg({ kind: 'warn', text: mt('mcp.msg.noteFailed') }) }).then(function () { refresh() })
6798
+ }
6799
+ })
6327
6800
  }
6328
6801
 
6329
6802
  // 「启动中」的乐观记账(用户 2026-10-15:呼吸灯要过一会才出现):翻开开关那一刻宿主
@@ -6359,7 +6832,13 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6359
6832
  setState((prev) => Object.assign({}, prev, {
6360
6833
  rows: (prev.rows || []).map((item) => (
6361
6834
  item.id === row.id && item.level === row.level
6362
- ? Object.assign({}, item, { disabled: nextDisabled })
6835
+ // `live.enabled` 一起乐观翻掉(用户 2026-10-18:「关闭后绿灯应该立即消失变灰,
6836
+ // 现在有延迟」):列表里那盏绿灯读的是宿主上一轮轮询的 `live`,只改 `disabled`
6837
+ // 的话它得等下一次刷新(默认 5 秒)才灭 —— 点完开关的五秒里界面还在说"它在跑"。
6838
+ ? Object.assign({}, item, {
6839
+ disabled: nextDisabled,
6840
+ live: item.live ? Object.assign({}, item.live, { enabled: !nextDisabled }) : item.live,
6841
+ })
6363
6842
  : item
6364
6843
  )),
6365
6844
  }))
@@ -6412,23 +6891,8 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6412
6891
  if (fresh) setDetail(Object.assign({}, detail, { row: fresh }))
6413
6892
  })
6414
6893
  }
6415
- // 备注保存:不走共用的 `run()`(那个把结果写进页面级 `msg`,见 noteNotice 的说明)。
6416
- // 成功 / 失败都只回写到详情弹窗的备注小节;文案沿用 `opMsg` / `mcp.msg.failed`,
6417
- // 失败时**原样透出服务端那句**(锁定态就是冻结闸的「场景已锁定(…):先到场景页解锁
6418
- // 再修改」)—— 它正是用户要看到的那句话,只是位置从页面挪进了弹窗。
6419
- const saveNote = () => {
6420
- const row = detail && detail.row
6421
- if (!row) return
6422
- setNoteNotice(null)
6423
- setBusy('note')
6424
- apiCall('mcpm-note', { id: row.id, note: noteDraft }).then((res) => {
6425
- if (res && res.ok) {
6426
- setDetail(Object.assign({}, detail, { row: Object.assign({}, row, { notes: noteDraft.trim() }) }))
6427
- setNoteNotice(opMsg(res))
6428
- refresh()
6429
- } else setNoteNotice({ kind: 'err', text: (res && res.error) || mt('mcp.msg.failed') })
6430
- }).catch((e) => setNoteNotice({ kind: 'err', text: errMsg(e) })).then(() => setBusy(null))
6431
- }
6894
+ // 备注的写入随「编辑」一起提交(见 submitForm 的 noteChanged),详情页只读 ——
6895
+ // 原先这里那颗 saveNote / noteDraft 随「详细页只能看」一并删除。
6432
6896
  const confirmRemove = () => {
6433
6897
  const row = confirmRow
6434
6898
  setConfirmRow(null)
@@ -6462,10 +6926,6 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6462
6926
  // 请求序号(每次开窗 +1):两处详情弹窗共用同一份 `detail`,连点两台服务器时
6463
6927
  // 先发的响应可能后到并覆盖后发的那份 —— 序号对不上就丢弃。
6464
6928
  const seq = ++detailSeqRef.current
6465
- setNoteDraft(row.notes || '')
6466
- // 换一台服务器 = 换一份备注:上一台的保存回执必须一起清掉,否则新弹窗会顶着
6467
- // 另一台服务器的红字(成功回执同理)。
6468
- setNoteNotice(null)
6469
6929
  setDetail({ row, loading: true, error: null, tools: [] })
6470
6930
  apiCall('mcpm-tools', { serverName: row.serverName }).then((res) => {
6471
6931
  if (seq !== detailSeqRef.current) return
@@ -6480,7 +6940,6 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6480
6940
  const closeDetail = () => {
6481
6941
  detailSeqRef.current += 1
6482
6942
  setDetail(null)
6483
- setNoteNotice(null)
6484
6943
  }
6485
6944
 
6486
6945
  const toggleTool = (row, tool) => {
@@ -6802,9 +7261,10 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6802
7261
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.headers, placeholder: 'Authorization=Bearer xxx', onChange: setFormField('headers') }))
6803
7262
  : null,
6804
7263
  // 备注放在表单**最下面**:它是可选项,说的又是"这台怎么用",不该插在必填的连接参数中间。
6805
- // 默认不填 = 侧车里不产生条目;只在新增时出现(`mcpm-edit` 不碰侧车,改备注走详情里那枚
6806
- // 输入框)。上限就是注入段的截断长度 `MCP_NOTE_MAX` —— 写多少模型就能读到多少。
6807
- formModal.mode === 'edit' ? null : React.createElement('label', { className: 'dsm-field' },
7264
+ // 默认不填 = 侧车里不产生条目。新增与编辑**都出现**(用户 2026-10-18:「备注编辑功能
7265
+ // 应该在编辑页,详细页只能看」):详情页只读,写备注这一件事只在这里做。
7266
+ // 上限就是注入段的截断长度 `MCP_NOTE_MAX` —— 写多少模型就能读到多少。
7267
+ React.createElement('label', { className: 'dsm-field' },
6808
7268
  React.createElement('div', { className: 'dsm-budget-meta' },
6809
7269
  React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.note')),
6810
7270
  React.createElement('span', { className: 'dsm-char-count' }, String(formModal.note || '').length + ' / ' + MCP_NOTE_MAX)),
@@ -6854,26 +7314,14 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
6854
7314
  React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.status')),
6855
7315
  React.createElement('div', { className: 'dsm-feedback' + (detailStatus.tone === 'bad' ? ' dsm-error' : ' dsm-warning') }, detailHint)) : null,
6856
7316
  React.createElement('div', { className: 'dsm-detail-section' },
6857
- // 标题行只留标题 + 字数计数:备注的上限就是注入段的截断长度(`MCP_NOTE_MAX`)。
6858
- // 超限标红沿用记忆页描述那套(`dsm-char-over`)—— 服务端不校验备注长度,
6859
- // 这条之前存的长备注会整条存着、注入时被截掉,得让人看得见超了多少。
6860
- React.createElement('div', { className: 'dsm-detail-title dsm-detail-title-row' },
6861
- mt('mcp.detail.note'),
6862
- React.createElement('span', { className: 'dsm-char-count' + (String(noteDraft || '').length > MCP_NOTE_MAX ? ' dsm-char-over' : '') }, String(noteDraft || '').length + ' / ' + MCP_NOTE_MAX)),
6863
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, maxLength: MCP_NOTE_MAX, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
6864
- // 「保存备注」贴着自己要提交的输入框下方右对齐,且是全弹窗唯一的实心主按钮
6865
- // (用户 2026-10-15:上一版把它与「刷新工具」都摆成标题行里的 quiet 键,
6866
- // 两颗一模一样、还落在相邻两节的同一高度 —— 备注输完下意识就点到「刷新
6867
- // 工具」。两个信号把两颗分开:位置贴着 textarea,提交动作跟着被提交的
6868
- // 字段走;权重用实心主色,它才是备注区的提交,「刷新工具」保持 quiet
6869
- // 的工具区动作)。
6870
- React.createElement('div', { className: 'dsm-modal-actions' },
6871
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, mt('mcp.detail.note.save'))),
6872
- // 备注保存的回执就地渲染(见 noteNotice 的说明)。文案原样来自服务端:
6873
- // 成功走 Notice 的 ok 档(右下角浮出的 toast,与全站成功提示同一副长相);
6874
- // 被冻结闸拒掉是弹窗内的一条红色 feedback —— 这正是用户要看到的那句话,
6875
- // 只是位置从页面挪进了弹窗。
6876
- React.createElement(Notice, { kind: noteNotice && noteNotice.kind, text: noteNotice && noteNotice.text })),
7317
+ // 备注在详情页**只读**(用户 2026-10-18:「mcp 备注编辑功能应该在编辑页,详细页只能看」):
7318
+ // 详情是"看这台现在配了什么"的地方,要改就走 ⋯ →「编辑」那张表 ——
7319
+ // 写动作只有一个入口,不会出现"改了这里忘了那里"。
7320
+ // 没备注时**也渲染同一个框**(用户 2026-10-18:「没备注也应该有边框,只是显示灰色字
7321
+ // 当前没有备注」):有备注时是一块正文,没备注时只剩标题、这一节看着像没渲染出来。
7322
+ React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
7323
+ React.createElement('p', { className: 'dsm-detail-note' + (detail.row.notes ? '' : ' dsm-detail-note-empty') },
7324
+ detail.row.notes || mt('mcp.detail.note.empty'))),
6877
7325
  React.createElement('div', { className: 'dsm-detail-section' },
6878
7326
  React.createElement('div', { className: 'dsm-detail-title dsm-detail-title-row' },
6879
7327
  mt('mcp.detail.tools', { count: detail.loading ? '…' : (detail.tools || []).length }),
@@ -7598,6 +8046,13 @@ button.dsm-rowselect-box:disabled{cursor:default;opacity:.45}
7598
8046
  // 「锁定时只显示锁定横幅」,提示词页此前漏了这层互斥,于是同一件事说了两遍
7599
8047
  // (用户 2026-09-19 截图:提示词页两条黄条,其他管理页只有一条)。
7600
8048
  (state.anyLocked !== true && sceneDriver) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('agm.sceneLock.notice', { scene: sceneDriverLabel, id: sceneDriver.presetId })) : null,
8049
+ // 共享 agents 根那一份 AGENTS.md(`~/.agents/AGENTS.md`)**只读提示**:0.2.1 起
8050
+ // 官方把用户全局链扩成两份并列(`$DSH_HOME/AGENTS.md` → `<agentsHome>/AGENTS.md`),
8051
+ // 这一页只管得到前一份。所以"改了没生效"有了第二个解释 —— 真正在说话的是后一份。
8052
+ // 只提示、不给写入入口:这不是本插件的预设,写它会与官方的去重/刷新规则互相覆盖。
8053
+ (state.current && state.current.agentsHomeAgentsMd && state.current.agentsHomeAgentsMd.exists)
8054
+ ? React.createElement('div', { className: 'dsm-feedback', role: 'status' }, t('agm.agentsHome.present', { path: state.current.agentsHomeAgentsMd.path }))
8055
+ : null,
7601
8056
  // 一次性拒绝说明(被引用的预设删不掉):不替换列表,只加一条横幅。
7602
8057
  state.deny ? React.createElement(Notice, { kind: 'warn', text: state.deny }) : null,
7603
8058
  // 搜索框:页头之下、列表之前(与 MCP / 技能 / 记忆页同一位置与样式)。
@@ -8291,7 +8746,7 @@ function enabledMemoryCount(rules) {
8291
8746
  /**
8292
8747
  * 「填写令牌」弹窗(用户 2026-10-12 提案:横幅那颗键点开一个弹窗,填对自动关)。
8293
8748
  *
8294
- * 为什么是弹窗而不是"跳到设置 → 工具 → 兼容":那条路要求设置面板已经挂载
8749
+ * 为什么是弹窗而不是"跳到设置 → 工具管理 → 兼容":那条路要求设置面板已经挂载
8295
8750
  * (`navigateToTab` 只在 ToolsSection 挂载时才有值),从对话页点它就得先想办法把宿主的
8296
8751
  * 设置对话框打开 —— 宿主没有这个服务,只能去 DOM 里认领它自己的设置导航行,又脆又绕。
8297
8752
  * 一个自包含的输入框把整条依赖都去掉了:不碰宿主 DOM、不需要跳页、也不必知道用户当时
@@ -8974,6 +9429,10 @@ function enabledMemoryCount(rules) {
8974
9429
  const anyLocked = ysx[0], setAnyLocked = ysx[1]
8975
9430
  const zsx = React.useState([])
8976
9431
  const sceneList = zsx[0], setSceneList = zsx[1]
9432
+ // 技能来源表:只给回收站详情把 `<rootKey>/<name>` 里的根键翻成人看的名字用
9433
+ //(`custom-<uuid>` 那种串摆给人看没有意义,见 manifestInfoRows 的注释)。
9434
+ const rxs = React.useState([])
9435
+ const skillRoots = rxs[0], setSkillRoots = rxs[1]
8977
9436
  React.useEffect(function () { load(); return undefined }, [])
8978
9437
  const dataOf = function (id) { return (data && data[id]) || { loading: true, error: null, entries: [] } }
8979
9438
  function setSlot(id, slot) {
@@ -8989,6 +9448,12 @@ function enabledMemoryCount(rules) {
8989
9448
  })
8990
9449
  // 锁定态(只读判断,失败不拦 —— 写操作到了服务端还有 frozen 这道闸);
8991
9450
  // `sceneList` 只有记忆组用得上(每条要说清属于哪个场景)。
9451
+ // 技能来源表只给回收站详情翻根键用:读不到就退回原键,不因为这一趟失败影响列表。
9452
+ if (groupIds.indexOf('skills') >= 0) {
9453
+ apiCall('skill-state', {}).then(function (res) {
9454
+ if (res && res.ok) setSkillRoots(res.roots || [])
9455
+ }).catch(function () { })
9456
+ }
8992
9457
  var needLock = groupIds.some(function (gid) { return GROUPS[gid].frozen === true || gid === 'memories' })
8993
9458
  if (!needLock) return
8994
9459
  apiCall('rules-list', {}).then(function (res) {
@@ -9170,7 +9635,7 @@ function enabledMemoryCount(rules) {
9170
9635
  function openPreview(g, item) {
9171
9636
  const id = String(idOfOf(g)(item))
9172
9637
  const meta = g.previewMeta ? g.previewMeta(item) : null
9173
- setPreview({ group: g.id, id: id, name: g.nameOf(item), meta: meta, loading: meta === null, error: null, files: [] })
9638
+ setPreview({ group: g.id, id: id, name: g.nameOf(item), meta: meta, loading: meta === null, error: null, files: [], manifest: null })
9174
9639
  if (meta !== null) return
9175
9640
  const patch = function (fields) {
9176
9641
  setPreview(function (cur) {
@@ -9179,7 +9644,7 @@ function enabledMemoryCount(rules) {
9179
9644
  })
9180
9645
  }
9181
9646
  apiCall('trash-preview', { domain: g.id, id: id }).then(function (res) {
9182
- if (res && res.ok) patch({ loading: false, error: null, files: res.files || [] })
9647
+ if (res && res.ok) patch({ loading: false, error: null, files: res.files || [], manifest: res.manifest || null })
9183
9648
  else patch({ loading: false, error: translateError(t, res), files: [] })
9184
9649
  }).catch(function (e) { patch({ loading: false, error: errMsg(e), files: [] }) })
9185
9650
  }
@@ -9393,21 +9858,59 @@ function enabledMemoryCount(rules) {
9393
9858
  body = React.createElement('div', { className: 'dsm-empty' }, t('trash.preview.loading'))
9394
9859
  } else {
9395
9860
  const files = preview.files || []
9861
+ // 记忆 / 场景 / 技能这类条目:清单里写着"删掉的是什么"(场景档案勾了什么、记忆属于
9862
+ // 哪个场景、什么形态),文件列表只是"里面有哪些文件"。用户 2026-10-18:「记忆回收站
9863
+ // 的详细应该记忆在第一栏,附件在下面」—— 所以顺序固定为**条目信息在前、文件在后**,
9864
+ // 不是把两类混成一张表。
9865
+ const info = manifestInfoRows(t, preview.group, preview.manifest, skillRoots)
9866
+ // 技能条目套用「技能详情」弹窗的排版(用户 2026-10-18:「技能的回收站详细还是挤在一起,
9867
+ // 不能套用技能详细弹窗吗」):正文那份单独成节、其余文件是附件清单,不再一份文件一张
9868
+ // 带边框的卡 —— 那种画法把十个文件挤成十条 240px 的代码窗,什么也读不出来。
9869
+ const isSkill = preview.group === 'skills'
9870
+ const bodyFile = isSkill ? (files.filter(function (f) { return /(^|\/)SKILL\.md$/i.test(String(f.path || '')) })[0] || files[0]) : null
9871
+ const attachFiles = isSkill ? files.filter(function (f) { return f !== bodyFile }) : files
9396
9872
  body = React.createElement('div', { className: 'dsm-preview' },
9873
+ info.length
9874
+ ? React.createElement(React.Fragment, null,
9875
+ formSection(t('trash.preview.entry')),
9876
+ React.createElement('div', { className: 'dsm-fm' }, info.map(function (row) {
9877
+ return React.createElement('div', { key: row[0], className: 'dsm-fm-row' },
9878
+ React.createElement('div', { className: 'dsm-fm-key' }, row[0]),
9879
+ React.createElement('div', { className: 'dsm-fm-val' }, row[1]))
9880
+ })))
9881
+ : null,
9882
+ isSkill && attachFiles.length
9883
+ ? React.createElement(React.Fragment, null,
9884
+ formSection(t('detail.attachments') + ' · ' + attachFiles.length),
9885
+ React.createElement('div', { className: 'dsm-attach-list dsm-attach-cap' }, attachFiles.map(function (f) {
9886
+ return React.createElement('div', { key: f.path, className: 'dsm-file', title: f.path },
9887
+ React.createElement('span', { className: 'dsm-file-name' }, f.path),
9888
+ React.createElement('span', { className: 'dsm-file-meta' }, fmtBytes(f.size)))
9889
+ })))
9890
+ : null,
9891
+ isSkill && bodyFile
9892
+ ? React.createElement('div', { className: 'dsm-detail-section' },
9893
+ React.createElement('div', { className: 'dsm-detail-title' }, t('detail.body')),
9894
+ bodyFile.text === undefined
9895
+ ? React.createElement('div', { className: 'dsm-preview-flag' }, t('trash.preview.binary'))
9896
+ : React.createElement('pre', { className: 'dsm-code dsm-code-fill' }, bodyFile.text))
9897
+ : null,
9397
9898
  React.createElement('div', { className: 'dsm-preview-note' },
9398
9899
  t('trash.preview.readonly') + ' · ' + t('trash.preview.files', { count: files.length })),
9399
- files.length
9400
- ? React.createElement('div', { className: 'dsm-preview-files' }, files.map(function (f, i) {
9401
- return React.createElement('section', { key: f.path || i, className: 'dsm-preview-file' },
9402
- React.createElement('div', { className: 'dsm-preview-file-head' },
9403
- React.createElement('span', { className: 'dsm-preview-path', title: f.path }, f.path),
9404
- React.createElement('span', { className: 'dsm-preview-size' }, fmtBytes(f.size))),
9405
- f.text === undefined ? null : React.createElement('pre', { className: 'dsm-code' }, f.text),
9406
- // 二进制 / 截断各自如实标一句:不标的话用户会以为"这条记录就这么点内容"。
9407
- f.binary === true ? React.createElement('div', { className: 'dsm-preview-flag' }, t('trash.preview.binary')) : null,
9408
- f.truncated === true ? React.createElement('div', { className: 'dsm-preview-flag' }, t('trash.preview.truncated')) : null)
9409
- }))
9410
- : React.createElement(EmptyState, { text: t('trash.preview.empty') }))
9900
+ isSkill
9901
+ ? null
9902
+ : files.length
9903
+ ? React.createElement('div', { className: 'dsm-preview-files' }, files.map(function (f, i) {
9904
+ return React.createElement('section', { key: f.path || i, className: 'dsm-preview-file' },
9905
+ React.createElement('div', { className: 'dsm-preview-file-head' },
9906
+ React.createElement('span', { className: 'dsm-preview-path', title: f.path }, f.path),
9907
+ React.createElement('span', { className: 'dsm-preview-size' }, fmtBytes(f.size))),
9908
+ f.text === undefined ? null : React.createElement('pre', { className: 'dsm-code' }, f.text),
9909
+ // 二进制 / 截断各自如实标一句:不标的话用户会以为"这条记录就这么点内容"。
9910
+ f.binary === true ? React.createElement('div', { className: 'dsm-preview-flag' }, t('trash.preview.binary')) : null,
9911
+ f.truncated === true ? React.createElement('div', { className: 'dsm-preview-flag' }, t('trash.preview.truncated')) : null)
9912
+ }))
9913
+ : React.createElement(EmptyState, { text: t('trash.preview.empty') }))
9411
9914
  }
9412
9915
  // 只读预览单开一档(见 10-css.js 的 .dsm-modal-preview):这里装的是代码正文,
9413
9916
  // S 档 440 一行要折好几次。负载文件那一档再挂 fill(定高 + 代码区吃掉剩余高度)。
@@ -11289,12 +11792,15 @@ function enabledMemoryCount(rules) {
11289
11792
  // 右侧那两颗按钮整块挡冒泡 —— 点「恢复」不该顺带把这条勾上。
11290
11793
  var at = it.archivedAt || it.createdAt || 0
11291
11794
  var metaBits = []
11292
- if (it.cwd) metaBits.push(React.createElement('span', { key: 'cwd', className: 'dsm-listrow-meta-desc', title: it.cwd }, it.cwd))
11795
+ if (it.cwd) metaBits.push(React.createElement('span', { key: 'cwd', className: 'dsm-listrow-meta-desc dsm-listrow-meta-path', title: it.cwd }, it.cwd))
11293
11796
  // 归档时间排在目录**后面且不可收缩**:长目录会被截,但"什么时候收进来的"必须留下
11294
11797
  //(保留期默认 30 天,它其实就是"还剩几天")。宿主没给时间就不显示,不拿未知占位。
11295
11798
  if (at) metaBits.push(React.createElement('span', { key: 'at', className: 'dsm-listrow-meta-info' }, new Date(at).toLocaleDateString()))
11296
11799
  return React.createElement(ListRow, {
11297
11800
  key: it.sessionId,
11801
+ // 行比别处松一档(用户 2026-10-18:「归档会话太拥挤了」):这一页的行夹着
11802
+ // 目录 + 日期两段文字,还常驻两颗按钮,48px 的通用行高塞下来标题和元信息贴在一起。
11803
+ className: 'dsm-hist-row',
11298
11804
  leading: React.createElement(RowCheck, { checked: selected.has(it.sessionId), label: t('hist.select'), onChange: function () { toggleSelect(it.sessionId) } }),
11299
11805
  onToggle: function () { toggleSelect(it.sessionId) },
11300
11806
  name: it.title || ('Session ' + shortId(it.sessionId)),
@@ -11332,21 +11838,25 @@ function enabledMemoryCount(rules) {
11332
11838
  React.createElement(RowCheck, { checked: allChecked, label: t('hist.selectGroup', { title: g.title }), onChange: function () { toggleGroup(g.key, groupIds) } }),
11333
11839
  React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleGroupCollapse(g.key, baseOpen) } },
11334
11840
  React.createElement('span', { className: 'dsm-source-title dsm-hist-group-title', title: g.title }, g.title),
11335
- React.createElement('span', { className: 'dsm-count' }, String(g.items.length))),
11841
+ React.createElement('span', { className: 'dsm-count' }, String(g.items.length)),
11842
+ // 目录挪进标题栏、落到第二行(靠 flex-wrap + width:100%):起点与标题左端对齐,
11843
+ // 不再被右侧那颗标签挤着走(用户 2026-10-18:「应该开头对齐……放在标题下面是不是更好」)。
11844
+ g.path ? React.createElement('span', { className: 'dsm-path', title: g.path }, g.path) : null),
11336
11845
  // 目录已被删除(dirMissing)时:既不给「重新登记」入口,也不再并列「已移除」标签 ——
11337
11846
  // 只留一枚醒目告警,明确告诉用户这个工作区的磁盘文件夹已经不存在了
11338
11847
  // (用户 2026-10-12:「如果工作区已经被删除应该不能恢复……要怎么及时知道这个工作区
11339
11848
  // 实际文件夹已经被删除了」)。宿主 registerWorkspace 的 realpath 本来就会拒绝
11340
11849
  // 不存在的目录,这里把入口提前收掉,避免点了才报错。
11850
+ // 目录随标题走:放进 .dsm-source-head-main 里,靠 flex-wrap 落到第二行、
11851
+ // 与标题左端对齐(用户 2026-10-18:「应该开头对齐……放在标题下面是不是更好」)。
11341
11852
  (detached || g.dirMissing) ? React.createElement('div', { className: 'dsm-hist-group-extra' },
11342
11853
  g.dirMissing
11343
11854
  // 「原目录不存在」:红色描边标签(用户 2026-10-12:「改成『原目录不存在』
11344
11855
  // 也加一个边框,但是不能点击」)。边框是它唯一的视觉标记 —— 原先靠行首的
11345
11856
  // ⚠ 符号,混在一堆文字里看不出这是一枚"状态标签"。它是个 span,本来就不可点。
11346
11857
  ? React.createElement('span', { className: 'dsm-hist-dir-missing', title: t('hist.group.dirMissing.hint') }, t('hist.group.dirMissing'))
11347
- : React.createElement('span', { className: 'dsm-tag', title: t('hist.group.removed.hint') }, t('hist.group.removed')),
11348
- detached && !g.dirMissing && g.canRegister ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, title: t('hist.btn.registerWs.hint'), onClick: function () { setModal({ type: 'register', path: g.path, title: g.title }) } }, t('hist.btn.registerWs')) : null) : null,
11349
- g.path ? React.createElement('span', { className: 'dsm-path', title: g.path }, g.path) : null),
11858
+ : detached ? React.createElement('span', { className: 'dsm-tag', title: t('hist.group.removed.hint') }, t('hist.group.removed')) : null,
11859
+ detached && !g.dirMissing && g.canRegister ? React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, title: t('hist.btn.registerWs.hint'), onClick: function () { setModal({ type: 'register', path: g.path, title: g.title }) } }, t('hist.btn.registerWs')) : null) : null),
11350
11860
  open ? React.createElement('div', { className: 'dsm-source-body' }, g.items.map(renderRow)) : null)
11351
11861
  }
11352
11862
 
@@ -11580,8 +12090,9 @@ function enabledMemoryCount(rules) {
11580
12090
  /**
11581
12091
  * 场景描述最多存多少个字(新建/编辑表单的 `maxLength` 与字数计数都用它)。
11582
12092
  *
11583
- * 卡片那一行**不靠这个数撑腰**:`.dsm-scene-tile-desc` 是 `nowrap` + 省略号,
11584
- * 再长也只占一行、不会把卡片撑成纵向。显示层仍按它裁一次,兜住历史数据。
12093
+ * 行内那一格**不靠这个数撑腰**:`.dsm-scene-tile-desc` 是 `nowrap` + 省略号,
12094
+ * 再长也只占一行、不会把行撑成纵向(2026-10-18 起描述与名字同一行)。显示层仍按它裁一次,
12095
+ * 兜住历史数据。
11585
12096
  */
11586
12097
  var SCENE_DESC_MAX = 300
11587
12098
  /** 记忆描述在勾选行里最多显示多少个字:记忆正文可能很长,行内只留一行,超长截断(全文进 title)。 */
@@ -11633,6 +12144,68 @@ function enabledMemoryCount(rules) {
11633
12144
  return clipText(scene && scene.description, SCENE_DESC_MAX)
11634
12145
  }
11635
12146
 
12147
+ /**
12148
+ * 回收站详情里「条目信息」那几行(用户 2026-10-18:「回收站的详细功能做的不够好,显示的内容
12149
+ * 不够全,例如场景没有显示勾选了什么,设置了什么」)。
12150
+ *
12151
+ * 数据来自 `trash-preview` 回传的 `manifest`(就是条目目录里那份 `manifest.json`),按域各取:
12152
+ * · scenes —— 记录(显示名 / 备注 / 锁定)+ 档案四段勾了多少条 + 绑的提示词与工具表;
12153
+ * · memories —— 属于哪个场景、形态(flat / bundle)、删除时间;
12154
+ * · skills / presets / quick / agents —— 名字 + 删除时间(它们的"内容"就是文件本身)。
12155
+ *
12156
+ * 只报**有值的**:段未定义 = 该域不碰(不是"勾了 0 个"),两者不能混成一句话。
12157
+ */
12158
+ function manifestInfoRows(t, domain, manifest, skillRoots) {
12159
+ var rows = []
12160
+ var m = manifest && typeof manifest === 'object' ? manifest : null
12161
+ if (!m) return rows
12162
+ var deleted = m.deletedAt ? new Date(m.deletedAt).toLocaleString() : ''
12163
+ if (deleted) rows.push([t('trash.preview.field.deletedAt'), deleted])
12164
+ if (domain === 'memories') {
12165
+ if (m.group !== undefined) rows.push([t('trash.preview.field.scene'), sceneLabel([], String(m.group || ''))])
12166
+ if (m.form) rows.push([t('trash.preview.field.form'), t('memory.form.' + (m.form === 'bundle' ? 'bundle' : 'flat'))])
12167
+ return rows
12168
+ }
12169
+ if (domain !== 'scenes') return rows
12170
+ var data = (m.data && typeof m.data === 'object') ? m.data : {}
12171
+ var rec = (data.record && typeof data.record === 'object') ? data.record : {}
12172
+ var arc = (data.archive && typeof data.archive === 'object') ? data.archive : {}
12173
+ if (rec.label) rows.push([t('trash.preview.field.label'), String(rec.label)])
12174
+ if (rec.description) rows.push([t('trash.preview.field.desc'), String(rec.description)])
12175
+ if (rec.locked === true) rows.push([t('trash.preview.field.locked'), t('trash.preview.yes')])
12176
+ // 档案四段:只有**段已定义**才报(undefined = 这个域不动,与"勾了 0 个"是两件事)。
12177
+ if (arc.mcp && typeof arc.mcp === 'object') {
12178
+ var servers = Object.keys(arc.mcp)
12179
+ // 工具级收窄:`'*'` 是整台,数组是指定工具 —— 明细放在同一格里,别让人以为只是"勾了几台"。
12180
+ var detail = servers.map(function (k) {
12181
+ var v = arc.mcp[k]
12182
+ return Array.isArray(v) ? k + '(' + v.length + ')' : k
12183
+ }).join('、')
12184
+ rows.push([t('trash.preview.field.mcp'), detail || t('trash.preview.none')])
12185
+ }
12186
+ if (Array.isArray(arc.skills)) {
12187
+ // 技能 id 是 `<rootKey>/<name>`,而 rootKey 对「手动添加的目录」是一串
12188
+ // `custom-<uuid前16位>`(用户 2026-10-18:「场景详细页的 custom-d38da02b873bdb5c
12189
+ // 是什么鬼」)。那一串只是磁盘上的去重键,对人没有意义 —— 详情里把根键换成人看的
12190
+ // 名字,查不到的(来源已被移除)就退回"名字 + 已移除来源",不把 uuid 摆给人看。
12191
+ rows.push([t('trash.preview.field.skills'), arc.skills.map(function (id) {
12192
+ var s = String(id)
12193
+ var at = s.indexOf('/')
12194
+ if (at < 0) return s
12195
+ var rootKey = s.slice(0, at)
12196
+ var root = (skillRoots || []).filter(function (r) { return r.key === rootKey })[0]
12197
+ var rootName = root ? rootDisplayName(t, root) : (rootKey.indexOf('custom-') === 0 ? t('trash.preview.sourceGone') : rootKey)
12198
+ return rootName + ' / ' + s.slice(at + 1)
12199
+ }).join('、') || t('trash.preview.none')])
12200
+ }
12201
+ if (Array.isArray(arc.subagents)) rows.push([t('trash.preview.field.subagents'), arc.subagents.join('、') || t('trash.preview.none')])
12202
+ if (Array.isArray(arc.quickPrompts)) rows.push([t('trash.preview.field.quick'), arc.quickPrompts.join('、') || t('trash.preview.none')])
12203
+ if (arc.toolTablePreset) rows.push([t('trash.preview.field.toolTable'), String(arc.toolTablePreset)])
12204
+ if (rec.prompt) rows.push([t('trash.preview.field.prompt'), String(rec.prompt)])
12205
+ if (Array.isArray(data.memories)) rows.push([t('trash.preview.field.memories'), String(data.memories.length)])
12206
+ return rows
12207
+ }
12208
+
11636
12209
  // ---------- 场景记忆页(场景 = scene-memory/ 下的一级目录;内容自动生效)----------
11637
12210
  //
11638
12211
  // 变更单 01 之后 Rules 与 Scenes 合并为本页:**场景(一级目录)是分组维度,
@@ -13380,7 +13953,9 @@ function enabledMemoryCount(rules) {
13380
13953
  return parts.join(' · ')
13381
13954
  }
13382
13955
  // 当前模式场景被锁 → 「退出模式」按钮与开关一起禁用(服务端守卫兜底)。
13383
- var modeSceneLocked = !!(modeScene && (data.scenes || []).some(function (s) { return s.name === modeScene && s.locked === true }))
13956
+ // `modeSceneObj` 是那个场景的记录本身:横幅里那颗「锁定」要拿它去调 toggleSceneLock。
13957
+ var modeSceneObj = modeScene ? (data.scenes || []).filter(function (s) { return s.name === modeScene })[0] : null
13958
+ var modeSceneLocked = !!(modeSceneObj && modeSceneObj.locked === true)
13384
13959
  var modeSummary = modeScene ? archiveSummary(data.archives[modeScene], modeScene) : ''
13385
13960
  return React.createElement('section', { className: 'dsm-section' },
13386
13961
  // 页头一行化(REDESIGN §3 + §6):新增(实底) · 开启提醒 · 回收站;统计内联。
@@ -13423,7 +13998,13 @@ function enabledMemoryCount(rules) {
13423
13998
  t('scenes.mode.active', { name: sceneLabel(data.scenes, modeScene) || modeScene })),
13424
13999
  React.createElement('div', { className: 'dsm-mode-sub' },
13425
14000
  modeSummary ? t('scenes.mode.profile', { parts: modeSummary }) : t('scenes.mode.noProfile'))),
13426
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || modeSceneLocked, title: modeSceneLocked ? t('scenes.lock.blockedExit') : '', onClick: exitMode }, t('memory.mode.exit'))) : null,
14001
+ // 「锁定」搬到这条横幅里、挨着「退出模式」(用户 2026-10-18:「锁定改成只有开启后
14002
+ // 才出现,而且出现在退出模式旁边」):锁的是**当前正在用的这个场景**,卡片上每张
14003
+ // 都挂一颗、再按 active 置灰,读起来像"这张卡有锁功能但用不了"。只有进入之后才有
14004
+ // 锁定这回事,于是它只在这条横幅里出现。
14005
+ React.createElement('span', { className: 'dsm-mode-acts' },
14006
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-lock', disabled: busy, title: t('scenes.lock.hint'), onClick: function () { toggleSceneLock(modeSceneObj) } }, modeSceneLocked ? t('scenes.lock.unlock') : t('scenes.lock.lock')),
14007
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy || modeSceneLocked, title: modeSceneLocked ? t('scenes.lock.blockedExit') : '', onClick: exitMode }, t('memory.mode.exit')))) : null,
13427
14008
  React.createElement(Notice, { key: 'notice', kind: result && result.ok ? 'ok' : 'err', text: result && result.text }),
13428
14009
  React.createElement(UndoBar, { key: 'undo', undo: undo, busy: busy, t: t, onUndo: undoMemories, onClose: dismissUndo }),
13429
14010
  // 历史「全部启用」(active=null)遗留:单选模型下多名场景同时在场是歧义状态。
@@ -13476,30 +14057,35 @@ function enabledMemoryCount(rules) {
13476
14057
  // 不一致就如实标出来,别让用户以为文件里就是它。
13477
14058
  var promptMismatch = promptLive && !promptMissing && data.scenePrompt.duplicate !== true
13478
14059
  return React.createElement('article', { key: 's:' + name, className: 'dsm-scene-tile' + (modeScene === name ? ' dsm-scene-tile-mode' : '') },
13479
- React.createElement('div', { className: 'dsm-scene-tile-head' },
13480
- React.createElement('span', { className: 'dsm-scene-tile-name', title: label }, label),
13481
- // 显示名与磁盘目录名不同时才标出真名,方便对文件核对。
13482
- name === label ? null : React.createElement('span', { className: 'dsm-scene-tile-key' }, name),
13483
- locked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
13484
- modeScene === name ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current')) : null,
13485
- sceneLocked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('scenes.lock.tag')) : null,
13486
- !locked && scene.active === false ? React.createElement('span', { className: 'dsm-tag' }, t('memory.scene.off')) : null,
13487
- boundPrompt ? React.createElement('span', {
13488
- className: 'dsm-tag' + (promptMissing ? ' dsm-tag-off' : promptLive ? ' dsm-tag-on' : ''),
13489
- title: promptMismatch ? t('scenes.prompt.mismatch.hint', { id: boundPrompt }) : t('scenes.prompt.tag.hint'),
13490
- }, promptMissing
13491
- ? t('scenes.prompt.missing', { id: boundPrompt })
13492
- : promptLive ? t('scenes.prompt.live', { id: boundPrompt }) : t('scenes.prompt.bound', { id: boundPrompt })) : null,
13493
- promptMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('scenes.prompt.mismatch.hint', { id: boundPrompt }) }, t('agm.file.mismatch')) : null,
14060
+ // 左列:名字 + 描述(两行)。右侧控件是另一格,二者按卡片中线对齐。
14061
+ React.createElement('div', { className: 'dsm-scene-tile-main' },
14062
+ React.createElement('div', { className: 'dsm-scene-tile-head' },
14063
+ React.createElement('span', { className: 'dsm-scene-tile-name', title: label }, label),
14064
+ // 显示名与磁盘目录名不同时才标出真名,方便对文件核对。
14065
+ name === label ? null : React.createElement('span', { className: 'dsm-scene-tile-key' }, name),
14066
+ locked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.scene.shared')) : null,
14067
+ modeScene === name ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.mode.current')) : null,
14068
+ sceneLocked ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('scenes.lock.tag')) : null,
14069
+ !locked && scene.active === false ? React.createElement('span', { className: 'dsm-tag' }, t('memory.scene.off')) : null,
14070
+ // 「提示词生效中:X」「提示词:X」两枚标签已删(用户 2026-10-18):绑定关系在
14071
+ // 编辑器里一眼可改,挂在卡片上只是每行多一枚药丸。只留**出问题时**的两枚
14072
+ //(预设不见了 / 文件里不是它)—— 那才是需要人动手的信号。
14073
+ promptMissing ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('scenes.prompt.tag.hint') }, t('scenes.prompt.missing', { id: boundPrompt })) : null,
14074
+ promptMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('scenes.prompt.mismatch.hint', { id: boundPrompt }) }, t('agm.file.mismatch')) : null),
14075
+ // 描述固定第二行(用户 2026-10-18:「场景页的备注在设置页没显示,应该显示在第二行」):
14076
+ // 在编辑器里填的描述就是这一句,它不在卡头那一行跟名字抢宽度。没有描述也渲染,
14077
+ // 保持每张卡同样两行高,行与行之间对得齐。
14078
+ React.createElement('div', { className: 'dsm-scene-tile-desc', title: desc || '' }, desc || t('scenes.noDesc'))),
14079
+ // 右列:开关 + ⋯(用户 2026-10-18:「switch 按键和三个点按键没有上下居中」)。
14080
+ // 「锁定」已搬进当前模式横幅,卡片上不再出现 —— 锁的是"正在用的那个场景"。
14081
+ // 编辑 / 删除收进行尾一颗 ⋯:卡片底下那条只有两颗按钮的空 band 白占一截高度。
14082
+ // 锁定态「编辑」照旧置灰(改档案是写盘动作,与记忆 / 子智能体那两页的
14083
+ // "只读能看"不同,场景编辑器一打开就是那张要保存的表)。
14084
+ React.createElement('div', { className: 'dsm-scene-tile-side' },
13494
14085
  locked ? null : React.createElement('span', {
13495
14086
  className: 'dsm-scene-tile-switch',
13496
14087
  title: blockedByOther ? t('scenes.enable.blocked', { name: sceneLabel(data.scenes, data.activeScene) || data.activeScene || '' }) : (sceneLocked && scene.active === true ? t('scenes.lock.blockedExit') : ''),
13497
14088
  }, React.createElement(Switch, { on: scene.active === true, disabled: busy || blockedByOther || (sceneLocked && scene.active === true), label: t('memory.scene.enable') + ' ' + name, onClick: function () { toggleScene(scene) } })),
13498
- locked ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-lock', disabled: busy || scene.active !== true, title: scene.active !== true ? t('scenes.lock.notActive') : t('scenes.lock.hint'), onClick: function () { toggleSceneLock(scene) } }, sceneLocked ? t('scenes.lock.unlock') : t('scenes.lock.lock')),
13499
- // 编辑 / 删除收进卡头一颗 ⋯(用户 2026-10-16):卡片底下那条只有两颗按钮的
13500
- // 空 band 白占一截高度,档案没内容时整张卡看着像坏了。
13501
- // 锁定态「编辑」照旧置灰(改档案是写盘动作,与记忆 / 子智能体那两页的
13502
- // "只读能看"不同,场景编辑器一打开就是那张要保存的表)。
13503
14089
  React.createElement(RowMenu, {
13504
14090
  key: 'menu',
13505
14091
  label: t('row.menu', { name: label }),
@@ -13508,10 +14094,7 @@ function enabledMemoryCount(rules) {
13508
14094
  ].concat(locked ? [] : [
13509
14095
  { key: 'delete', label: t('scenes.row.delete'), danger: true, disabled: busy || sceneLocked, title: sceneLocked ? t('scenes.lock.blockedEdit') : undefined, onClick: function () { setModal({ type: 'scene-delete', name: name }) } },
13510
14096
  ]),
13511
- })),
13512
- // 描述行**只有描述**(数量都收进上面的模式条了);全文放 title,卡片本身永远一行。
13513
- // 它是卡片的最后一行,所以下边距由它承担(原来那条 foot 一并删掉,见上)。
13514
- React.createElement('p', { className: 'dsm-scene-tile-desc', title: desc || '' }, desc || t('scenes.noDesc')))
14097
+ })))
13515
14098
  })),
13516
14099
  modal && modal.type === 'scene-delete' ? React.createElement(Modal, { key: 'sdel', className: 'dsm-modal-sm', title: t('memory.deleteScene.title'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
13517
14100
  React.createElement('p', { className: 'dsm-help' }, t('memory.deleteScene.desc', { name: modal.name })),
@@ -14508,8 +15091,8 @@ function enabledMemoryCount(rules) {
14508
15091
  //
14509
15092
  // 目录注入深度:人设目录注入到哪些会话(默认 1 = 只在顶层)。
14510
15093
  // ⚠️ 它**不是**递归上限。2026-09-17 用户实测后改名(原名 maxDepth /「委派预算」):
14511
- // 官方 `dsh-tool-subagent` 默认 `maxDepth: 3`,子代理本来就能继续嵌套,本插件
14512
- // 也不再向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
15094
+ // 官方按自己的 `maxDepth` 配置决定能不能继续嵌套(0.2.1 默认 1),本插件也不再
15095
+ // 向官方传 maxDepth。这个字段只决定常驻目录出现在哪些深度的会话里。
14513
15096
  React.createElement('div', { className: 'dsm-field-row' },
14514
15097
  React.createElement('div', { className: 'dsm-field' },
14515
15098
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.catalogDepth')),
@@ -14598,6 +15181,7 @@ function enabledMemoryCount(rules) {
14598
15181
  * 提到组件外是为了能被 `useMemo` 包住:这段要遍历全部记忆并逐桶排序,而本页的
14599
15182
  * 编辑器弹窗每敲一个字都会重渲染整页(`editor` 是页内 state)。传进来的 `q` 已防抖。
14600
15183
  */
15184
+
14601
15185
  function memoryView(data, q, sceneFilter) {
14602
15186
  function matchRule(r) {
14603
15187
  if (sceneFilter !== '' && sceneOfGroup(r.group) !== sceneFilter) return false
@@ -15492,7 +16076,7 @@ function enabledMemoryCount(rules) {
15492
16076
  }
15493
16077
  // ── 斜杠命令「工具」段(对话输入框里直接管六个域)────────────────────────────
15494
16078
  //
15495
- // 要解决的问题:切个场景、临时停一个 MCP,都得离开当前对话去「设置 → 工具」。这一段把
16079
+ // 要解决的问题:切个场景、临时停一个 MCP,都得离开当前对话去「设置 → 工具管理」。这一段把
15496
16080
  // 这些**可逆**动作搬进宿主的 `/` 菜单:打一个 `/` → 出现「工具」段 → 点一行原地进入该域
15497
16081
  // → 点一条直接启停,**菜单停在原地不关**,接着点下一条也行。
15498
16082
  //
@@ -16091,8 +16675,11 @@ function enabledMemoryCount(rules) {
16091
16675
  // 中英随宿主语言。源名本身(tool-management)因此不会出现在界面上,只当唯一性用。
16092
16676
  showGroupTitle: false,
16093
16677
  async candidates(session, req) {
16094
- // 只在行首提供:`帮我看看 /tool` 这种半路打出来的 `/` 不该弹我们的东西。
16095
- if (req.position !== 'leading') return []
16678
+ // 行内也认(用户 2026-10-18:「输入框输入了文字,空格后输入斜杠命令没有工具和
16679
+ // 快捷提示词」):宿主自己的 `boundaryOk` 只把「紧跟在文字后面的 `/`」当成普通
16680
+ // 字符,前面是空格的 `/` 本来就是合法触发(`position` 报 `inline`)。我们此前一律
16681
+ // 只收 `leading`,于是「先打一段话、空格、再打 /」永远弹不出这两段。
16682
+ // 真正的"半路打出来的 /"(`https://`、`a/b`)宿主已经挡掉,这里不必再拦一遍。
16096
16683
  const domains = domainsOf()
16097
16684
  const query = req.query || ''
16098
16685
  const at = query.indexOf('/')
@@ -16222,7 +16809,7 @@ function enabledMemoryCount(rules) {
16222
16809
  order: 3,
16223
16810
  showGroupTitle: false,
16224
16811
  async candidates(session, req) {
16225
- if (req.position !== 'leading') return []
16812
+ // 同「工具」段:行内也认(前面是空格的 `/` 本来就是合法触发)。
16226
16813
  const query = String(req.query || '')
16227
16814
  // 只列一层:带 `/` 的草稿(`/tool-skill/…`)不是这一段的 token。
16228
16815
  if (query.indexOf('/') >= 0) return []
@@ -16289,6 +16876,95 @@ function enabledMemoryCount(rules) {
16289
16876
  })
16290
16877
  }
16291
16878
 
16879
+
16880
+ // ── 侧栏入口(2026-10-18)─────────────────────────────────────────────────
16881
+ //
16882
+ // 宿主的侧栏全局入口是**两半**:`main` 里那个键控面板(选中后占中栏)与
16883
+ // `sidebar.panellist` 里那一行(图标 + 名字)。两半必须同 id,点行才有地方去。
16884
+ //
16885
+ // 为什么是"主面板"而不是"打开设置":设置对话框的开合状态是宿主私有的,没有公开
16886
+ // 服务可调(同生态的 dshmarket 在 sidebar-entry.ts 里写明同一件事 —— 主面板是公开
16887
+ // 契约里唯一的一键目的地)。所以这一行打开的是本插件页面的**另一份挂载**,内容
16888
+ // 与设置面板里那一页同源(都渲染 ToolsSection),不是复制一套界面。
16889
+ //
16890
+ // 开关在兼容页页头,状态与侧车在 factory 作用域(见 20 片 sidebarEntryState):
16891
+ // 关掉时两半一起撤,撤销前先把中栏从这一页带出去 —— 否则选中项指向一个已注销的
16892
+ // key,中栏会空着。
16893
+ const SIDEBAR_ENTRY_ID = 'dsm-tools'
16894
+ // 排在官方那几行之后(插件 0 / 市场 1 / 自动化任务 10):本插件是"设置"这一类,
16895
+ // 摆在那三行下面比插在插件与市场之间更像话。
16896
+ const SIDEBAR_ENTRY_ORDER = 20
16897
+
16898
+ /** 侧栏入口打开的那一页:外层只负责滚动与留白,内容仍是 ToolsSection 本身。 */
16899
+ function SidebarToolsPage() {
16900
+ return React.createElement('div', { className: 'dsm-panel' }, React.createElement(ToolsSection))
16901
+ }
16902
+
16903
+ /**
16904
+ * 侧栏那一行的图标(props:size / active,宿主给)。
16905
+ *
16906
+ * 与设置侧栏那颗图形同一份(40b 的 mask 版给宿主的封闭名单用),这里直接画 ——
16907
+ * 用 currentColor,宿主切换选中 / 悬停 / 主题时自动跟着变。
16908
+ */
16909
+ function SidebarEntryIcon(props) {
16910
+ const size = (props && props.size) || 16
16911
+ return React.createElement('svg', { viewBox: '0 0 1024 1024', width: size, height: size, 'aria-hidden': 'true', focusable: 'false' },
16912
+ React.createElement('path', { fill: 'currentColor', d: 'M592 336h-416c-52.928 0-96 43.072-96 96v416c0 52.928 43.072 96 96 96h416c52.928 0 96-43.072 96-96v-416c0-52.928-43.072-96-96-96z m32 512a32 32 0 0 1-32 32h-416c-17.632 0-32-14.336-32-32v-416c0-17.632 14.368-32 32-32h416c17.664 0 32 14.368 32 32v416z' }),
16913
+ React.createElement('path', { fill: 'currentColor', d: 'M720 208h-416a32 32 0 0 0 0 64h416c17.664 0 32 14.368 32 32v416a32 32 0 1 0 64 0v-416c0-52.928-43.072-96-96-96zM528 752h-288a32 32 0 1 0 0 64h288a32 32 0 1 0 0-64z' }),
16914
+ React.createElement('path', { fill: 'currentColor', d: 'M848 80h-416a32 32 0 0 0 0 64h416c17.664 0 32 14.368 32 32v416a32 32 0 1 0 64 0v-416c0-52.928-43.072-96-96-96z' }))
16915
+ }
16916
+
16917
+ /**
16918
+ * 一块注册 + 一个 `apply`:开关真翻的那一刻就注册 / 注销,不必等下一次打开面板。
16919
+ *
16920
+ * @param leavePanel 面板那一半传 true:注销前先把中栏带出去(见文件内那段注释)。
16921
+ */
16922
+ const sidebarEntryGate = function (options, component, leavePanel) {
16923
+ let dispose = null
16924
+ const apply = function () {
16925
+ const want = sidebarEntrySnapshot().enabled !== false
16926
+ if (want && dispose === null) {
16927
+ const off = slots.register(options, component)
16928
+ dispose = typeof off === 'function' ? off : function () {}
16929
+ return
16930
+ }
16931
+ if (want || dispose === null) return
16932
+ const stop = dispose
16933
+ dispose = null
16934
+ if (leavePanel) {
16935
+ const layout = serviceOf('layout')
16936
+ try {
16937
+ if (layout && layout.panelInfo && layout.panelInfo.getSnapshot().activePanelId === SIDEBAR_ENTRY_ID) layout.selectPanel(null)
16938
+ } catch (e) { /* 布局服务不可用 → 交给宿主的 retainMainPanels */ }
16939
+ }
16940
+ stop()
16941
+ }
16942
+ apply()
16943
+ const unsubscribe = onSidebarEntryChange(apply)
16944
+ return function () {
16945
+ unsubscribe()
16946
+ if (dispose !== null) { const stop = dispose; dispose = null; stop() }
16947
+ }
16948
+ }
16949
+
16950
+ // 两个 slot 各自注入:宿主没声明侧栏(旧版本 / 精简组合)时这一整段等于不存在,
16951
+ // 设置面板里那一页照旧能用。
16952
+ slots.inject('main', function () {
16953
+ return sidebarEntryGate({ name: 'main', key: SIDEBAR_ENTRY_ID }, SidebarToolsPage, true)
16954
+ })
16955
+ slots.inject('sidebar.panellist', function () {
16956
+ return sidebarEntryGate({
16957
+ name: 'sidebar.panellist', id: SIDEBAR_ENTRY_ID, order: SIDEBAR_ENTRY_ORDER,
16958
+ label: function () { return t('nav.title') }, locale: NS,
16959
+ }, SidebarEntryIcon, false)
16960
+ })
16961
+ // 读一次存下来的开关(读侧免令牌,见 op-registry 的 sidebar-settings)。读不到就
16962
+ // 保持默认(开);这一次跑在注册之后,所以关着的人会先看到一行再消失 —— 与
16963
+ // dshmarket 同一条时序,换的是"不必等服务器往返就能先给界面"。
16964
+ apiCall('sidebar-settings', {}).then(function (r) {
16965
+ if (r && r.ok && r.settings) setSidebarEntryState({ enabled: r.settings.enabled !== false })
16966
+ }).catch(function () { /* 读不到 → 保持默认(开) */ })
16967
+
16292
16968
 
16293
16969
  // 页面组件填进 factory 作用域那个占位对象(导出对象求值时已指向它)。
16294
16970
  // 不要改回 `module.exports.X = ...`:apply 可能在本函数开头提前返回。