dsh-plugin-tool-management 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +91 -123
  3. package/README_EN.md +99 -126
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +28 -0
  21. package/lib/client.js +578 -124
  22. package/lib/compat/preset-reach.js +6 -3
  23. package/lib/context-inject.js +86 -32
  24. package/lib/index.js +251 -92
  25. package/lib/mcp/manager.js +3 -14
  26. package/lib/mcp/secret-guard.js +21 -0
  27. package/lib/mcp/state-section.js +13 -11
  28. package/lib/memories/constants.js +72 -8
  29. package/lib/memories/index-io.js +1 -1
  30. package/lib/memories/projection.js +67 -31
  31. package/lib/memories/service.js +17 -2
  32. package/lib/memories/snapshot.js +113 -22
  33. package/lib/op-registry.js +8 -2
  34. package/lib/ops/candidates.js +11 -0
  35. package/lib/ops/compat.js +10 -2
  36. package/lib/ops/sessions.js +1 -1
  37. package/lib/prompts/service.js +25 -2
  38. package/lib/scenes/candidates.js +56 -0
  39. package/lib/skills/core.js +93 -0
  40. package/lib/skills/service.js +34 -2
  41. package/lib/subagents/service.js +37 -4
  42. package/lib/subagents/tools.js +11 -4
  43. package/lib/tools/deps.js +15 -0
  44. package/lib/tools/mcp.js +291 -50
  45. package/lib/tools/memory.js +131 -80
  46. package/lib/tools/prompt.js +27 -13
  47. package/lib/tools/scene.js +351 -0
  48. package/lib/tools/skills.js +93 -17
  49. package/lib/tools/subagent.js +56 -45
  50. package/lib/tools/table.js +204 -15
  51. package/package.json +108 -108
package/lib/client.js CHANGED
@@ -163,7 +163,7 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
163
163
  .dsm-row>.dsm-tags:empty{display:none}
164
164
  .dsm-row>.dsm-tags:empty~.dsm-row-actions{grid-row:1 / span 2}}@media(max-width:760px){.dsm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.dsm-title-row{flex-wrap:wrap}}@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%}.dsm-summary{grid-template-columns:1fr}}
165
165
  .dsm-hist-row{display:flex;align-items:center;gap:11px;padding:10px 13px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dsm-hist-row:last-child{border-bottom:0}.dsm-hist-main{min-width:0;flex:1}.dsm-hist-title{overflow:hidden;font-size:13px;font-weight:580;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dsm-hist-cwd-missing{color:var(--dsw-alias-state-error-primary)}.dsm-hist-actions{display:flex;gap:6px;flex:none}.dsm-hist-check,.dsm-hist-group-check{flex:none;width:15px;height:15px;margin:0 4px 0 0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}.dsm-hist-group-head{gap:10px}.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;color:var(--dsw-alias-state-error-primary);font-size:11px;white-space:nowrap}.dsm-hist-register-path{margin-top:-4px;color:var(--dsw-alias-label-tertiary);font-family:var(--dsw-font-mono,monospace);font-size:11px;word-break:break-all}.dsm-hist-batch{display:flex;align-items:center;gap:8px;margin-left:2px;padding-left:10px;border-left:1px solid var(--dsw-alias-border-l2)}.dsm-hist-batch-count{color:var(--dsw-alias-label-secondary);font-size:12px;white-space:nowrap}.dsm-hist-batch-actions{display:flex;align-items:center;flex-wrap:wrap;gap:8px;flex:none}.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-l1);border-radius:8px;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}
166
- .dsm-rule-shadowed .dsm-name,.dsm-rule-shadowed .dsm-note{color:var(--dsw-alias-state-error-primary)}.dsm-rule-off .dsm-name,.dsm-rule-off .dsm-note{color:var(--dsw-alias-label-tertiary)}.dsm-rule-shadow-hint{margin-top:3px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-invalid{border-color:var(--dsw-alias-state-error-primary)!important}.dsm-rule-hint{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-budget{display:flex;flex-direction:column;gap:6px}.dsm-budget-meta{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsm-budget-meta strong{margin-right:0;font-size:13px;font-weight:680}.dsm-budget-over-text{color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-budget-bar{height:6px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover)}.dsm-budget-fill{height:100%;border-radius:99px;background:var(--dsw-alias-state-success-primary);transition:width 160ms ease}.dsm-budget-fill.dsm-budget-over{background:var(--dsw-alias-state-error-primary)}.dsm-char-count{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-char-over{color:var(--dsw-alias-state-error-primary)}
166
+ .dsm-rule-shadowed .dsm-name,.dsm-rule-shadowed .dsm-note{color:var(--dsw-alias-state-error-primary)}.dsm-rule-off .dsm-name,.dsm-rule-off .dsm-note{color:var(--dsw-alias-label-tertiary)}.dsm-rule-shadow-hint{margin-top:3px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-invalid{border-color:var(--dsw-alias-state-error-primary)!important}.dsm-rule-hint{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}.dsm-rule-budget{display:flex;flex-direction:column;gap:6px}.dsm-budget-meta{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.dsm-budget-meta strong{margin-right:0;font-size:13px;font-weight:680}.dsm-budget-over-text{color:var(--dsw-alias-state-error-primary);font-size:11px}.dsm-budget-bar{height:6px;overflow:hidden;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover)}.dsm-budget-fill{height:100%;border-radius:99px;background:var(--dsw-alias-state-success-primary);transition:width 160ms ease}.dsm-budget-fill.dsm-budget-over{background:var(--dsw-alias-state-error-primary)}.dsm-char-count{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-char-over{color:var(--dsw-alias-state-error-primary)}.dsm-field-row{display:flex;gap:12px;align-items:flex-start}.dsm-field-row>.dsm-field{flex:1;min-width:0}
167
167
  .dsm-combo-row{display:flex;gap:8px}.dsm-combo-row .dsm-control{flex:1}
168
168
  .dsm-attach-list{display:flex;flex-direction:column;gap:6px}.dsm-attach-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
169
169
  .dsm-toast{position:fixed;right:22px;bottom:22px;z-index:1250;box-sizing:border-box;max-width:min(420px,calc(100vw - 44px));padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;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 时记得同步加到这里,
@@ -243,7 +243,11 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
243
243
  .dsm-adv{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
244
244
  .dsm-adv-head{display:flex;width:100%;min-height:40px;align-items:center;gap:8px;padding:0 11px;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-align:left;cursor:pointer}
245
245
  .dsm-adv-head:hover{background:var(--dsw-alias-interactive-bg-hover)}
246
- .dsm-adv-caret{color:var(--dsw-alias-label-tertiary);font-size:10px}
246
+ .dsm-adv-caret{flex:none;color:var(--dsw-alias-label-tertiary);font-size:10px}
247
+ /* 「高级选项」永不折行:它在收起时与两行摘要并排,被挤成两字一行就不再像个标题了。 */
248
+ .dsm-adv-title{flex:none;white-space:nowrap}
249
+ /* 折叠摘要固定两行(模型/思考强度/目录注入 + 工具名单),靠 align-items:center 与标题垂直居中。 */
250
+ .dsm-adv-sum{display:flex;min-width:0;flex-direction:column;gap:1px}
247
251
  .dsm-adv-body{display:flex;flex-direction:column;gap:12px;padding:11px;border-top:1px solid var(--dsw-alias-border-l1)}
248
252
  .dsm-adv-note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
249
253
  /* ── 工具限制四行(人设表单:一行一个 Agent 预设,白/黑名单互斥、默认折叠)──────── */
@@ -344,11 +348,26 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
344
348
  .dsm-inject-domain input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
345
349
  /* 模型工具表:与「注入」同屏的第二种开关。**一行一个工具、四列固定宽度**(勾选框 / 名字 /
346
350
  ≈tok / 一句话介绍)—— 列宽不固定就竖着扫不出任何一列(用户 2026-09-23 指出挤成一排很难看)。
347
- 名字用等宽、数字用 tabular-nums 右对齐;介绍超长省略,完整那句在悬停里。 */
351
+ 名字用等宽、数字用 tabular-nums 右对齐;介绍超长省略,完整那句在悬停里。
352
+ 出厂默认关着哪几条**不在行上标**(用户 2026-09-23 要求去掉「默认关」小标):合计下面那句
353
+ "其中 ≈X 是出厂默认关掉的 N 条"是唯一的出处,加上悬停那一行会说"出厂默认就是关着的"。 */
348
354
  .dsm-tooltable{display:flex;flex-direction:column;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
349
355
  /* 保存往返期间:整块不吃点击、略灰 —— 这一块刻意没有乐观更新(见 saveToolTable 注释)。 */
350
356
  .dsm-tooltable-busy{pointer-events:none;opacity:.55}
351
- .dsm-tooltable-summary{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
357
+ .dsm-tooltable-summary{display:flex;align-items:center;gap:10px;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
358
+ /* 「保存方案 / 恢复方案」钉在合计那一行的最右(用户 2026-09-23 指定位置)。
359
+ 按钮压到与 18px 行高齐平:宿主默认的 .dsm-btn 比这行字高,直接放会把整行顶开。 */
360
+ .dsm-tooltable-preset-actions{display:flex;flex:none;align-items:center;gap:6px;margin-left:auto}
361
+ .dsm-tooltable-preset-actions .dsm-btn{padding:0 9px;font-size:11px;line-height:22px}
362
+ /* 方案清单(恢复弹窗):一行一份,名字吃剩余宽度,数字与两颗按钮靠右。 */
363
+ .dsm-preset-list{display:flex;flex-direction:column;gap:6px}
364
+ .dsm-preset-row{display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
365
+ .dsm-preset-name{min-width:0;overflow:hidden;flex:1;color:var(--dsw-alias-label-primary);font-size:12px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}
366
+ .dsm-preset-meta{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
367
+ .dsm-preset-row .dsm-btn{padding:0 9px;font-size:11px;line-height:22px}
368
+ .dsm-preset-tag{margin-left:6px;padding:0 5px;border:1px solid var(--dsw-alias-border-l2);border-radius:4px;color:var(--dsw-alias-label-tertiary);font-size:10px;font-weight:400}
369
+ /* 合计的第二句:把"省下的"拆出出厂默认那一截。比第一句弱一档,它是解释不是结论。 */
370
+ .dsm-tooltable-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
352
371
  .dsm-tooltable-group{display:flex;flex-direction:column;gap:3px}
353
372
  .dsm-tooltable-ghead{display:flex;align-items:center;gap:7px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;cursor:pointer}
354
373
  .dsm-tooltable-ghead input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
@@ -373,7 +392,7 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
373
392
  .dsm-inject-live-mark-blank{visibility:hidden}
374
393
  .dsm-inject-live-size{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}
375
394
  /* 实况面板的域名格里有多个行内元素(域名 / 体积 / 采纳统计),行内元素之间没有空白节点,
376
- 不设间距就会挤成「场景和记忆· 3.2 KB」。
395
+ 不设间距就会挤成「场景· 3.2 KB」。
377
396
  刻意**不复用** .dsm-compat-name:那一类在另外五个面板里只放一个文本子节点,改成 flex 会让
378
397
  长标签变成不可收缩的 flex 项 → 窄容器下溢出而不是换行。所以只在实况面板这一处加 gap。 */
379
398
  .dsm-inject-live-name{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px}
@@ -877,6 +896,19 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
877
896
  const [toolTable, setToolTable] = React.useState(null)
878
897
  // 保存往返期间把整块置灰(见 saveToolTable 的注释:不做乐观更新)。
879
898
  const [toolTableBusy, setToolTableBusy] = React.useState(false)
899
+ // 「方案」弹窗:null = 未打开。`mode:'save'` 给当前这套勾选起名存下来;
900
+ // `mode:'apply'` 列出已存的方案 + 那条不可删的出厂默认,点一行就应用。
901
+ const [toolPreset, setToolPreset] = React.useState(null)
902
+ const toolPresetDialogRef = React.useRef(null)
903
+ const toolPresetInputRef = React.useRef(null)
904
+ // 与备份弹窗同一条依赖口径:用"开 / 关"这个布尔,不是整个 state —— 后者每改一次
905
+ // (输入名字、busy)都会变,会把用户刚聚焦的输入框抢回弹窗根节点。
906
+ React.useEffect(function () {
907
+ if (!toolPreset || !toolPresetDialogRef.current) return
908
+ toolPresetDialogRef.current.focus()
909
+ // 存方案那条路用户是来打字的,光标直接放进输入框,省一次点击。
910
+ if (toolPreset.mode === 'save' && toolPresetInputRef.current) toolPresetInputRef.current.focus()
911
+ }, [!!toolPreset])
880
912
  // 注入实况(只读):最近活跃会话里模型**真正看到**的五域文本 + 那一段对话的投递统计。
881
913
  // 与「注入」设置是一体两面:设置说"要送什么",这里说"实际送到了什么"。
882
914
  const [live, setLive] = React.useState(null)
@@ -1120,7 +1152,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1120
1152
  // ≈token 合计 —— 界面不自己算,也不缓存第二份数字。
1121
1153
  const applyToolTable = function (r, alive) {
1122
1154
  if (alive === false) return
1123
- if (r && r.ok && r.report) setToolTable({ hidden: r.hidden || [], report: r.report })
1155
+ if (r && r.ok && r.report) setToolTable(toolTableFrom(r))
1156
+ }
1157
+ /** 服务端回包 → 界面那一份状态(三条路径共用:读、改勾选、存/删方案)。 */
1158
+ const toolTableFrom = function (r, hiddenFallback) {
1159
+ return {
1160
+ hidden: r.hidden || hiddenFallback || [], presets: r.presets || [],
1161
+ defaultHidden: r.defaultHidden || [], report: r.report,
1162
+ }
1124
1163
  }
1125
1164
  const loadToolTable = function (alive) {
1126
1165
  apiCall('tool-table', {})
@@ -1140,7 +1179,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1140
1179
  setToolTableBusy(true)
1141
1180
  apiCall('tool-table', { set: true, hidden: names.slice() })
1142
1181
  .then(function (r) {
1143
- if (r && r.ok && r.report) setToolTable({ hidden: r.hidden || names, report: r.report })
1182
+ if (r && r.ok && r.report) setToolTable(toolTableFrom(r, names))
1144
1183
  else loadToolTable(true)
1145
1184
  // 功能总览那一行写着"一轮发出去多少",它得跟着变。
1146
1185
  loadFeatures(true)
@@ -1163,6 +1202,69 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1163
1202
  if (!on) Array.prototype.push.apply(hid, names)
1164
1203
  saveToolTable(hid)
1165
1204
  }
1205
+ // ---------- 「方案」:把一组勾选存成名字,之后一键换回来 ----------
1206
+ // 为什么要它(用户 2026-09-23):这一块的开关是**成批**用的 —— "这次要让模型自己配
1207
+ // 服务器"与"平时只让它查"是两套选择,挨个点二十个框没人愿意重复第二遍。
1208
+ // 三条口径:① 方案存的是**关掉的名单**(与 `hidden` 同口径),不是"开着的",否则
1209
+ // 新注册一条工具会让所有旧方案静默地少管一条;② 应用 = 走 `saveToolTable` 那条既有
1210
+ // 落盘路径(可见性重排与两处目录刷新都在那边,不开第二条真相);③ 出厂默认**不在文件里**
1211
+ // (它是代码里的 `DEFAULT_HIDDEN_TOOLS`),所以删不掉,界面把它排在第一行。
1212
+ /** 工具名 → ≈tok:方案行上那句"开着 X 个 · ≈Y tok/轮"要用,数字仍全部来自服务端报告。 */
1213
+ const toolTokByName = function () {
1214
+ const map = {}
1215
+ const groups = (toolTable && toolTable.report && toolTable.report.groups) || []
1216
+ groups.forEach(function (g) {
1217
+ (g.tools || []).forEach(function (tool) { map[tool.name] = tool.tok })
1218
+ })
1219
+ return map
1220
+ }
1221
+ /** 弹窗里的错误行:服务端回 `code` 时按当前语言出话;回 `error`(落盘失败)只能原样。
1222
+ * 认不出的 code 退回通用那句 —— 拼出来的键名如果落空,`t` 会把键本身吐到界面上。 */
1223
+ const presetErrorText = function (r) {
1224
+ const code = r && r.code
1225
+ if (code === 'nameEmpty' || code === 'nameTaken' || code === 'limit' || code === 'notFound') {
1226
+ return t('compat.tools.preset.err.' + code, { max: r.limit || 0 })
1227
+ }
1228
+ return (r && r.error) || t('compat.tools.preset.err.net')
1229
+ }
1230
+ /** 名字归一化与服务端 `presetNameOf` 同形(折叠空白 + 去首尾),否则本地查重会漏。 */
1231
+ const presetNameNorm = function (value) { return String(value || '').replace(/\s+/g, ' ').trim() }
1232
+ /** 存方案:空名与撞名在本地先挡住(服务端也会挡,但那是白跑一趟)。 */
1233
+ const submitToolPreset = function () {
1234
+ if (!toolPreset || toolPreset.busy) return
1235
+ const name = presetNameNorm(toolPreset.name)
1236
+ if (!name) {
1237
+ setToolPreset(Object.assign({}, toolPreset, { error: t('compat.tools.preset.err.nameEmpty') }))
1238
+ return
1239
+ }
1240
+ if (((toolTable && toolTable.presets) || []).some(function (p) { return p.name === name })) {
1241
+ setToolPreset(Object.assign({}, toolPreset, { name: name, error: t('compat.tools.preset.err.nameTaken', { name: name }) }))
1242
+ return
1243
+ }
1244
+ setToolPreset(Object.assign({}, toolPreset, { busy: true, error: '' }))
1245
+ apiCall('tool-table', { presetSave: name })
1246
+ .then(function (r) {
1247
+ if (r && r.ok) { setToolTable(toolTableFrom(r)); setToolPreset(null) }
1248
+ else setToolPreset({ mode: 'save', name: name, error: presetErrorText(r) })
1249
+ })
1250
+ .catch(function () { setToolPreset({ mode: 'save', name: name, error: presetErrorText(null) }) })
1251
+ }
1252
+ /** 删方案:留在弹窗里(用户多半还要接着挑),只把清单换成服务端那份。 */
1253
+ const deleteToolPreset = function (name) {
1254
+ if (toolPreset && toolPreset.busy) return
1255
+ setToolPreset({ mode: 'apply', busy: true })
1256
+ apiCall('tool-table', { presetDelete: name })
1257
+ .then(function (r) {
1258
+ if (r && r.ok) { setToolTable(toolTableFrom(r)); setToolPreset({ mode: 'apply' }) }
1259
+ else setToolPreset({ mode: 'apply', error: presetErrorText(r) })
1260
+ })
1261
+ .catch(function () { setToolPreset({ mode: 'apply', error: presetErrorText(null) }) })
1262
+ }
1263
+ /** 应用一份方案 = 用它那组名单走既有的保存路径。 */
1264
+ const applyToolPreset = function (hidden) {
1265
+ setToolPreset(null)
1266
+ saveToolTable((hidden || []).slice())
1267
+ }
1166
1268
  const loadLive = function (alive) {
1167
1269
  apiCall('injection-live', {})
1168
1270
  .then(function (r) { if (alive !== false && r && r.ok) setLive(r) })
@@ -1545,7 +1647,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1545
1647
  React.createElement('span', null, t('compat.inject.domains')),
1546
1648
  React.createElement('span', { className: 'dsm-seg-group-line' })),
1547
1649
  React.createElement('div', { className: 'dsm-inject-domains' },
1548
- ['memory', 'mcp', 'skills', 'subagents', 'prompt'].map(function (key) {
1650
+ ['scene', 'memory', 'mcp', 'skills', 'subagents', 'prompt'].map(function (key) {
1549
1651
  // 技能与提示词两域在标准类预设下由宿主送、本插件让位 —— 关掉它们时本插件会连
1550
1652
  // 宿主那条一起拦下(见 context-inject.ts 的 officialKindsToSuppress)。这里给
1551
1653
  // 那两项一句 title,免得用户以为"关了没用"。
@@ -1595,7 +1697,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1595
1697
  return React.createElement('label', {
1596
1698
  className: 'dsm-tooltable-tool',
1597
1699
  key: tool.name,
1598
- title: (hover ? hover + '\n\n' : '') + (tool.hidden ? t('compat.tools.offTitle') : t('compat.tools.onTitle')),
1700
+ title: (hover ? hover + '\n\n' : '') + (tool.hidden
1701
+ ? (tool.defaultHidden ? t('compat.tools.defaultOffTitle') : t('compat.tools.offTitle'))
1702
+ : t('compat.tools.onTitle')),
1599
1703
  },
1600
1704
  React.createElement('input', { type: 'checkbox', checked: !tool.hidden, onChange: function () { toggleToolTableTool(tool.name, !!tool.hidden) } }),
1601
1705
  React.createElement('span', { className: 'dsm-tooltable-name' }, tool.name),
@@ -1603,11 +1707,30 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1603
1707
  React.createElement('span', { className: 'dsm-tooltable-about' }, about))
1604
1708
  }))))
1605
1709
  })
1710
+ const presetCount = (toolTable.presets || []).length
1606
1711
  push(section(t('compat.tools'), t('compat.tools.hint'),
1607
1712
  React.createElement('div', { className: 'dsm-tooltable' + (toolTableBusy ? ' dsm-tooltable-busy' : '') },
1608
- React.createElement('div', { className: 'dsm-tooltable-summary' }, hidden.length === 0
1609
- ? t('compat.tools.summaryAll', { total: report.totalCount, tok: report.totalTok })
1610
- : t('compat.tools.summary', { on: report.visibleCount, total: report.totalCount, tok: report.visibleTok, saved: report.hiddenTok })),
1713
+ React.createElement('div', { className: 'dsm-tooltable-summary' },
1714
+ React.createElement('span', null, hidden.length === 0
1715
+ ? t('compat.tools.summaryAll', { total: report.totalCount, tok: report.totalTok })
1716
+ : t('compat.tools.summary', { on: report.visibleCount, total: report.totalCount, tok: report.visibleTok, saved: report.hiddenTok })),
1717
+ // 两颗按钮钉在合计那一行的最右(用户 2026-09-23 指定位置):方案管的是**整块**
1718
+ // 勾选,不属于任何一个域,所以既不放分组头也不放表尾。
1719
+ React.createElement('span', { className: 'dsm-tooltable-preset-actions' },
1720
+ React.createElement('button', {
1721
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: toolTableBusy,
1722
+ title: t('compat.tools.preset.save.tip'),
1723
+ onClick: function () { setToolPreset({ mode: 'save', name: '', error: '' }) },
1724
+ }, t('compat.tools.preset.save')),
1725
+ React.createElement('button', {
1726
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: toolTableBusy,
1727
+ title: presetCount ? t('compat.tools.preset.apply.tip') : t('compat.tools.preset.apply.tipEmpty'),
1728
+ onClick: function () { setToolPreset({ mode: 'apply' }) },
1729
+ }, t('compat.tools.preset.apply')))),
1730
+ // 合计里拆一句"出厂关的":不然第一句那个"省下 ≈2,679"会被读成用户自己关出来的。
1731
+ report.defaultHiddenCount > 0
1732
+ ? React.createElement('div', { className: 'dsm-tooltable-sub' }, t('compat.tools.summaryDefault', { n: report.defaultHiddenCount, tok: report.defaultHiddenTok }))
1733
+ : null,
1611
1734
  rows)))
1612
1735
  }
1613
1736
  // 注入实况:紧挨「注入」设置块下方。数据来自 `injection-live`(宿主读最近活跃会话的
@@ -1928,9 +2051,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1928
2051
  React.createElement('span', { className: 'dsm-compat-name dsm-reach-name' },
1929
2052
  React.createElement('span', null, (row.name || row.presetId) + (row.isDefault ? ' · ' + t('compat.reach.default') : '')),
1930
2053
  tags.length ? React.createElement('span', { className: 'dsm-tags' }, tags) : null),
1931
- // 芯片顺序与「注入」块的勾选顺序一致(用户裁定 2026-09-16):
1932
- // 场景和记忆 → MCP → 技能 → 子智能体 → 提示词。
2054
+ // 芯片顺序与「注入」块的勾选顺序一致(用户裁定 2026-09-16;2026-09-23 起
2055
+ // 场景与记忆是两个域,各占一枚):场景 → 记忆 → MCP → 技能 → 子智能体 → 提示词。
1933
2056
  React.createElement('span', { className: 'dsm-compat-reach-marks' },
2057
+ chip(t('compat.reach.mark.scene'), row.scene),
1934
2058
  chip(t('compat.reach.mark.memory'), row.memory),
1935
2059
  chip(t('compat.reach.mark.mcp'), row.mcp),
1936
2060
  chip(t('compat.reach.mark.skill'), row.skillCatalog),
@@ -2163,6 +2287,107 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2163
2287
  React.createElement('div', { className: 'dsm-modal-body' }, rows)))
2164
2288
  }
2165
2289
 
2290
+ // 「方案」弹窗(保存与恢复共用一颗入口、两种 mode):结构与令牌 / 备份清理弹窗同款 ——
2291
+ // 遮罩点击关闭 + 头部标题与关闭键 + body + 底部动作行,Esc 与焦点陷阱复用模块作用域助手。
2292
+ // 为什么恢复要能删:方案是试出来的一旦攒多了就没人在清单里认得出哪个是哪个,
2293
+ // 不给出口就只能到磁盘上改文件(用户 2026-09-23 明确要"在恢复弹窗里也能删")。
2294
+ let toolPresetModal = null
2295
+ if (toolPreset) {
2296
+ const closePreset = function () { setToolPreset(null) }
2297
+ const presetBusy = !!toolPreset.busy || toolTableBusy
2298
+ const presetRows = []
2299
+ if (toolPreset.mode === 'save') {
2300
+ const report0 = (toolTable && toolTable.report) || {}
2301
+ const nameDraft = String(toolPreset.name || '')
2302
+ presetRows.push(React.createElement('p', { className: 'dsm-help', key: 'hint' },
2303
+ t('compat.tools.preset.saveHint', {
2304
+ on: report0.visibleCount || 0, total: report0.totalCount || 0, tok: report0.visibleTok || 0,
2305
+ })))
2306
+ presetRows.push(React.createElement('label', { className: 'dsm-field', key: 'name' },
2307
+ React.createElement('span', { className: 'dsm-label' }, t('compat.tools.preset.name')),
2308
+ React.createElement('input', {
2309
+ ref: toolPresetInputRef, className: 'dsm-control', type: 'text',
2310
+ maxLength: 40, value: nameDraft, placeholder: t('compat.tools.preset.namePlaceholder'),
2311
+ onChange: function (e) { setToolPreset(Object.assign({}, toolPreset, { name: e.target.value, error: '' })) },
2312
+ onKeyDown: function (e) { if (e.key === 'Enter') submitToolPreset() },
2313
+ })))
2314
+ if (toolPreset.error) {
2315
+ presetRows.push(React.createElement('p', { className: 'dsm-feedback dsm-warning', role: 'alert', key: 'err' }, toolPreset.error))
2316
+ }
2317
+ presetRows.push(React.createElement('div', { className: 'dsm-modal-actions', key: 'act' },
2318
+ React.createElement('button', {
2319
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: presetBusy, onClick: closePreset,
2320
+ }, t('btn.cancel')),
2321
+ React.createElement('button', {
2322
+ type: 'button', className: 'dsm-btn', disabled: presetBusy || !nameDraft.trim(), onClick: submitToolPreset,
2323
+ }, t('compat.tools.preset.saveYes'))))
2324
+ } else {
2325
+ const tokOf = toolTokByName()
2326
+ const rep = (toolTable && toolTable.report) || {}
2327
+ const total = rep.totalCount || 0
2328
+ const totalTok = rep.totalTok || 0
2329
+ const entries = [{
2330
+ key: '', name: t('compat.tools.preset.default'), builtin: true,
2331
+ hidden: (toolTable && toolTable.defaultHidden) || [],
2332
+ }]
2333
+ const storedPresets = (toolTable && toolTable.presets) || []
2334
+ storedPresets.forEach(function (p) {
2335
+ // `builtin` 必须显式给 false:数组类型由首元素(出厂默认,builtin: true)推断,
2336
+ // 漏了它 typecheck:client 会红;运行时 `e.builtin ? 删除键 : null` 依赖这个假值。
2337
+ entries.push({ key: p.name, name: p.name, builtin: false, hidden: p.hidden || [] })
2338
+ })
2339
+ presetRows.push(React.createElement('p', { className: 'dsm-help', key: 'hint' }, t('compat.tools.preset.applyHint')))
2340
+ presetRows.push(React.createElement('div', { className: 'dsm-preset-list', key: 'list' },
2341
+ entries.map(function (e) {
2342
+ // 只数**注册表里真有的**名字:方案可能存自更早的版本(那条工具已经不叫这个了),
2343
+ // 把陌生名字算进"关着的"会让这一行报出一个假的合计。应用时服务端本来也会把这些
2344
+ // 名字滤掉(`tool-table` 的 set 路径),所以这里显示的与真会生效的是同一份。
2345
+ const known = e.hidden.filter(function (n) { return tokOf[n] !== undefined })
2346
+ let offTok = 0
2347
+ known.forEach(function (n) { offTok += tokOf[n] })
2348
+ return React.createElement('div', { className: 'dsm-preset-row', key: e.key },
2349
+ React.createElement('span', { className: 'dsm-preset-name' },
2350
+ e.name,
2351
+ e.builtin ? React.createElement('span', { className: 'dsm-preset-tag' }, t('compat.tools.preset.builtin')) : null),
2352
+ React.createElement('span', { className: 'dsm-preset-meta' }, t('compat.tools.preset.count', {
2353
+ on: total - known.length, total: total, tok: totalTok - offTok,
2354
+ })),
2355
+ React.createElement('button', {
2356
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: presetBusy,
2357
+ onClick: function () { applyToolPreset(e.hidden) },
2358
+ }, t('compat.tools.preset.use')),
2359
+ // 出厂默认不在文件里(它是代码里那份名单),所以没有删除键 —— 删它没有意义,
2360
+ // 下次读盘它还会回来;能删的只有用户自己存的那几份。
2361
+ e.builtin ? null : React.createElement('button', {
2362
+ type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: presetBusy,
2363
+ title: t('compat.tools.preset.removeTitle', { name: e.name }),
2364
+ onClick: function () { deleteToolPreset(e.name) },
2365
+ }, t('btn.custom.remove')))
2366
+ })))
2367
+ if (toolPreset.error) {
2368
+ presetRows.push(React.createElement('p', { className: 'dsm-feedback dsm-warning', role: 'alert', key: 'err' }, toolPreset.error))
2369
+ }
2370
+ }
2371
+ toolPresetModal = React.createElement('div', {
2372
+ key: 'preset-modal',
2373
+ className: 'dsm-mask',
2374
+ onMouseDown: function (e) { if (e.target === e.currentTarget) closePreset() },
2375
+ }, React.createElement('div', {
2376
+ ref: toolPresetDialogRef, tabIndex: -1,
2377
+ className: 'dsm-modal dsm-modal-sm', role: 'dialog', 'aria-modal': 'true',
2378
+ 'aria-labelledby': 'dsm-preset-title',
2379
+ onKeyDown: function (e) { if (!handleModalEscape(e, closePreset)) trapModalFocus(e.currentTarget, e) },
2380
+ },
2381
+ React.createElement('div', { className: 'dsm-modal-head-wrap' },
2382
+ React.createElement('div', { className: 'dsm-modal-head' },
2383
+ React.createElement('h3', { className: 'dsm-modal-title', id: 'dsm-preset-title' },
2384
+ t(toolPreset.mode === 'save' ? 'compat.tools.preset.saveTitle' : 'compat.tools.preset.applyTitle')),
2385
+ React.createElement('button', {
2386
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: closePreset,
2387
+ }, t('btn.close')))),
2388
+ React.createElement('div', { className: 'dsm-modal-body' }, presetRows)))
2389
+ }
2390
+
2166
2391
  return React.createElement('div', { className: 'dsm-compat' },
2167
2392
  React.createElement('div', { className: 'dsm-head' },
2168
2393
  React.createElement('div', { className: 'dsm-title-block' },
@@ -2184,7 +2409,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2184
2409
  }, backups && backups.total ? t('compat.backups.btnCount', { count: backups.total }) : t('compat.backups.btn'))),
2185
2410
  body,
2186
2411
  backupModal,
2187
- tokenModalEl)
2412
+ tokenModalEl,
2413
+ toolPresetModal)
2188
2414
  }
2189
2415
 
2190
2416
  // 曾经把导出写成 apply 方法体的最后两条语句(`module.exports.DICT = ...` /
@@ -2224,7 +2450,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2224
2450
  "empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
2225
2451
  "source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
2226
2452
  "detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
2227
- "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 skill_manager_create 建的是用户级技能",
2453
+ "create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 skill_manager_save 建的是用户级技能",
2228
2454
  "import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名", "upload.requirement.memory.3": "带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.4": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "项目目录可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
2229
2455
  // F-029:服务端错误码补词条(zh 逐字复现服务端文案,命中词条前后中文显示不变;EN 不再回退中文原文)。
2230
2456
  "error.custom.absolute": "目录路径必须是绝对路径: {path}", "error.custom.notDirectory": "不是目录: {path}", "error.custom.unreadable": "无法读取目录: {path}", "error.custom.overlap": "目录与已有技能来源重叠: {path} ↔ {other}", "error.custom.invalidKey": "非法的自定义来源 key: {key}", "error.custom.notFound": "自定义来源不存在: {key}",
@@ -2384,7 +2610,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2384
2610
  "compat.token.mismatch": "与宿主配置的令牌不一致:请填入配置里的那个值。",
2385
2611
  "compat.token.failed": "读取令牌状态失败,请刷新后重试。",
2386
2612
  "compat.inject.domains": "注入内容",
2387
- "compat.inject.domain.memory": "场景和记忆", "compat.inject.domain.mcp": "MCP",
2613
+ "compat.inject.domain.scene": "场景", "compat.inject.domain.memory": "记忆", "compat.inject.domain.mcp": "MCP",
2388
2614
  "compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "提示词",
2389
2615
  "compat.inject.domain.skills": "技能",
2390
2616
  "compat.inject.domain.carrier.title": "关掉后,系统自带的那份也会一并停掉",
@@ -2395,47 +2621,76 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2395
2621
  "compat.tools.other": "其它",
2396
2622
  "compat.tools.onTitle": "已下发:模型能看到并调用它",
2397
2623
  "compat.tools.offTitle": "已关掉:整份不进请求,模型看不到也调不到(面板不受影响)",
2398
- // 每个工具两档文案(行内一句 / 悬停稍详细)。格式统一:行内一律动词开头、不带句号;
2624
+ "compat.tools.summaryDefault": "其中 ≈{tok} 是出厂默认关掉的 {n} 条(不需要模型做这些事就不用管;要用在这里勾上,只改本机)",
2625
+ "compat.tools.defaultOffTitle": "出厂默认就是关着的:模型看不到也调不到,面板不受影响。要模型做这件事,在这里勾上即可(只改本机)。",
2626
+ // 「方案」:把一整组勾选存成名字,之后一键换回来(合计行右侧那两颗按钮)。
2627
+ "compat.tools.preset.save": "保存方案",
2628
+ "compat.tools.preset.apply": "恢复方案",
2629
+ "compat.tools.preset.save.tip": "把现在这套勾选存成一个名字,以后一键换回来",
2630
+ "compat.tools.preset.apply.tip": "换到已存的方案(含那条不可删的出厂默认)",
2631
+ "compat.tools.preset.apply.tipEmpty": "还没有存过方案;这里可以先看出厂默认那一套",
2632
+ "compat.tools.preset.saveTitle": "保存当前方案",
2633
+ "compat.tools.preset.applyTitle": "恢复方案",
2634
+ "compat.tools.preset.saveHint": "现在这套是:开着 {on}/{total} 个 · ≈{tok} tok/轮。",
2635
+ "compat.tools.preset.name": "方案名字",
2636
+ "compat.tools.preset.namePlaceholder": "例如:日常最小 / 让它自己配服务器",
2637
+ "compat.tools.preset.saveYes": "存下来",
2638
+ "compat.tools.preset.applyHint": "选一份应用:立刻按它重排发给模型的工具表,面板不受影响。",
2639
+ "compat.tools.preset.default": "出厂默认",
2640
+ "compat.tools.preset.builtin": "不可删",
2641
+ "compat.tools.preset.count": "开着 {on}/{total} · ≈{tok} tok/轮",
2642
+ "compat.tools.preset.use": "应用这一份",
2643
+ "compat.tools.preset.removeTitle": "删掉方案「{name}」(只删这条记录,不改当前的勾选)",
2644
+ "compat.tools.preset.err.nameEmpty": "名字不能是空的",
2645
+ "compat.tools.preset.err.nameTaken": "已经有叫「{name}」的方案了 —— 保存不会覆盖它,换个名字(或先删掉那份)",
2646
+ "compat.tools.preset.err.limit": "方案最多存 {max} 份,先删一份再存",
2647
+ "compat.tools.preset.err.notFound": "那份方案已经不在了(可能在别处删掉了)",
2648
+ "compat.tools.preset.err.net": "没存上:与本机服务没接上",
2649
+ // 每个工具两档文案(行内一句 / 悬停稍详细)。格式统一:行内一律动词开头、不带句号,
2650
+ // 而且是**这个工具真能做的事**的缩写;一个工具能做几件事时用 `/` 分隔,**不用「或」**
2651
+ // —— 「或」读起来像"二选一",而这些工具是真能都做的(`save` 既是新建也是修改,
2652
+ // `switch` 既能开也能关)。0.14.x 之前写成「保存一条记忆」这类带量词的说法,读起来
2653
+ // 只像"新建",与 upsert 的实际行为对不上(用户 2026-09-23 指出)。
2399
2654
  // 悬停补"什么时候用、会改到什么、边界在哪"。键按工具名拼(`compat.tools.about.<工具名>`),
2400
2655
  // 工具改名 = 换身份,这两组键要跟着改(取不到时界面留空、不显示键名)。
2401
- "compat.tools.about.memory_manager_list": "列出记忆(默认只列会注入的)",
2402
- "compat.tools.about.memory_manager_read": "按 id 读一条记忆的正文",
2403
- "compat.tools.about.memory_manager_set_enabled": "启用或停用一条记忆",
2404
- "compat.tools.about.memory_manager_write": "新建一条记忆",
2405
- "compat.tools.about.memory_manager_update": "改记忆的正文、描述或名字",
2406
- "compat.tools.about.mcp_manager_list": "列出服务器、状态与工具",
2407
- "compat.tools.about.mcp_manager_set_enabled": "启用或停用服务器 / 单个工具",
2408
- "compat.tools.about.mcp_manager_restart": "重启一台服务器",
2409
- "compat.tools.about.mcp_manager_add": "新增一台 MCP 服务器",
2656
+ "compat.tools.about.memory_manager_list": "列出记忆与启停",
2657
+ "compat.tools.about.memory_manager_read": "按 id 读记忆正文",
2658
+ "compat.tools.about.memory_manager_switch": "启用/停用记忆",
2659
+ "compat.tools.about.memory_manager_save": "新建/修改记忆",
2660
+ "compat.tools.about.scene_manager_list": "列出场景、档案与启停",
2661
+ "compat.tools.about.scene_manager_switch": "进入/退出场景",
2662
+ "compat.tools.about.scene_manager_save": "新建/修改场景、写档案、绑提示词",
2663
+ "compat.tools.about.mcp_manager_list": "列出服务器与工具",
2664
+ "compat.tools.about.mcp_manager_switch": "启停/重启服务器",
2665
+ "compat.tools.about.mcp_manager_save": "新增/修改服务器",
2410
2666
  "compat.tools.about.skill_manager_list": "列出技能、启停与路径",
2411
2667
  "compat.tools.about.skill_manager_read": "按名字读技能正文",
2412
- "compat.tools.about.skill_manager_set_enabled": "启用或停用技能 / 技能目录",
2413
- "compat.tools.about.skill_manager_create": "新建一个技能",
2668
+ "compat.tools.about.skill_manager_switch": "启用/停用技能或技能目录",
2669
+ "compat.tools.about.skill_manager_save": "新建/修改技能",
2414
2670
  "compat.tools.about.subagent_manager_list": "列出人设与描述",
2415
- "compat.tools.about.subagent_manager_run": "按人设跑一个子智能体",
2416
- "compat.tools.about.subagent_manager_set_enabled": "启用或停用人设",
2417
- "compat.tools.about.subagent_manager_create": "新建一个人设",
2418
- "compat.tools.about.subagent_manager_update": "改人设(含改名)",
2671
+ "compat.tools.about.subagent_manager_run": "按人设派子智能体",
2672
+ "compat.tools.about.subagent_manager_switch": "启用/停用人设",
2673
+ "compat.tools.about.subagent_manager_save": "新建/修改人设",
2419
2674
  "compat.tools.about.prompt_manager_list": "列出提示词预设",
2420
2675
  "compat.tools.about.prompt_manager_apply": "应用一份提示词预设",
2421
- "compat.tools.hover.memory_manager_list": "列出记忆的 id、所属场景、启停与一句话描述。默认只列这次真的会注入的;all=true 会把停用、场景未启用、被同名覆盖的一起列出来。只看不改。",
2676
+ "compat.tools.hover.memory_manager_list": "列出记忆的 id、所属场景、启停与一句话描述,并标明注入按哪个场景走。默认只列这次真的会注入的记忆;all=true 会把停用、场景未启用、被同名覆盖的一起列出来。场景本身(档案、绑定的预设、锁没锁)在 scene_manager_list。只看不改。",
2422
2677
  "compat.tools.hover.memory_manager_read": "按 id(<场景>/<名字>)读一条记忆的完整正文。清单里只有一句话描述,要看正文就用它 —— 尤其是被停用、场景没启用、或被体积预算挤掉的记忆。",
2423
- "compat.tools.hover.memory_manager_set_enabled": "只拨索引里的开关,.md 文件一个字不动,随时能拨回来。停用的记忆不再进上下文,等于把注入预算让给别人 —— 想省 token 优先用它,而不是删。",
2424
- "compat.tools.hover.memory_manager_write": "在某个场景下建一条记忆(场景要先存在;global 是常驻那个)。记忆会进之后每一轮请求,所以只记从代码、配置、git 推不出来的事;建之前先看清单里有没有已经讲了同一件事的。",
2425
- "compat.tools.hover.memory_manager_update": "改一条现有记忆:正文、描述或改名(旧文件会留在回收站)。没传的字段保持原样;改之前先读一遍、只改要改的那部分,不要把整份重述一遍。",
2426
- "compat.tools.hover.mcp_manager_list": "列出配好的服务器、连没连上、各自有哪些工具,以及你写给这台服务器的备注。曾经连上过、现在连不上的会标出来 —— 那说的是「这台有问题」,不是「没配」。",
2427
- "compat.tools.hover.mcp_manager_set_enabled": "停用整台服务器,或只停它其中一个工具。停用的工具会从模型可见的工具表里消失,执行侧也拦住;重启 DSH 后仍然生效。",
2428
- "compat.tools.hover.mcp_manager_restart": "把服务器停用、等它断开、再按原来的状态启用 —— 改了配置或服务器卡住时用它。过程中这台服务器的工具会短暂消失。",
2429
- "compat.tools.hover.mcp_manager_add": "新增一台服务器(streamable-http 或 stdio),可带 URL、命令、参数、请求头、环境变量与备注,写到项目层或全局层。只在用户明确要求时用。",
2678
+ "compat.tools.hover.memory_manager_switch": "只拨索引里的开关,.md 文件一个字不动,随时能拨回来。停用的记忆不再进上下文,等于把注入预算让给别人 —— 想省 token 优先用它,而不是删。",
2679
+ "compat.tools.hover.memory_manager_save": "新建一条记忆,或改掉已经存在的那条同名记忆 —— 回执会说清走了哪条。记忆会进之后每一轮请求,所以只记从代码、配置、git 推不出来的事;建之前先看清单里有没有已经讲了同一件事的。没传的字段保持原样;改名会把旧文件留在回收站。",
2680
+ "compat.tools.hover.scene_manager_list": "列出每个场景:场景名(界面显示名不同时会一并标出 —— 工具要的是前者)、启用 / 已进入 / 已锁定、场景说明、绑定的预设、记忆条数,以及档案:进入它时会开哪些 MCP / 技能 / 人设(没勾的一律关掉)。要改某个场景的档案之前先看它 —— 保存是整段替换,看不到现有勾选就会把原来勾的挤掉;要进入哪个场景也在这一份里挑。只看不改。",
2681
+ "compat.tools.hover.scene_manager_switch": "进入一个场景:按它的档案把 MCP / 技能 / 人设整套拨到位(没勾的一律关掉),注入范围收窄成它的记忆,并把场景绑定的预设正文写进全局 AGENTS.md。退出则按进场景前的快照精确还原,基线也恢复。切之前先用 scene_manager_list 看一眼那个场景的档案。改的是整个运行时环境,所以默认要用户确认。",
2682
+ "compat.tools.hover.scene_manager_save": "建一个场景,或改掉已有的那个 —— 回执会说清走了哪条。场景是「进入时按它切换」的一套:档案里勾了什么,进入后就开什么,其余全部关掉。只写档案不会让场景生效 —— 要生效得进入它(scene_manager_switch,或你在「场景」页点)。没传的段保持原样;传空段 = 全不勾(不是「不限制」)。改一个段是整段替换,先用 scene_manager_list 看现有勾选,回执会列出被挤掉的项。",
2683
+ "compat.tools.hover.mcp_manager_list": "列出配好的服务器、连没连上、各自有哪些工具,以及你写给这台服务器的备注。曾经连上过、现在连不上的会标出来 —— 那说的是「这台有问题」,不是「没配」。tools=true 会把每台的工具名也列出来,被单独关掉的标 (off);整台停用的服务器只有上次运行记下的名单。",
2684
+ "compat.tools.hover.mcp_manager_switch": "action=on/off 启用或停用整台服务器;给了 tool 就只动那一个工具(停用的工具会从模型可见的工具表里消失,执行侧也拦住,重启 DSH 后仍生效);action=restart 把服务器停用、等它断开、再按原状态启用 —— 改了配置或服务器卡住时用它。同名服务器在项目层与全局层各有一台时,要传 level 指定动哪台。",
2685
+ "compat.tools.hover.mcp_manager_save": "新增一台服务器(streamable-http 或 stdio),或改掉同名的那台 —— 回执会说清走了哪条。改的时候**没传的字段保持原样**,所以改一个 URL 不必重述整条配置(密钥也不会因此丢);stdio 的 command+args 就是宿主真正会 spawn 的命令,改它会弹确认卡。只在用户明确要求时用。",
2430
2686
  "compat.tools.hover.skill_manager_list": "列出技能的启停、生效还是被同名覆盖、源文件路径。常驻目录只带名字和摘要,所以「我怎么关了它」「同名哪份生效」要看这个;all=true 连停用与影子副本一起列。",
2431
2687
  "compat.tools.hover.skill_manager_read": "按名字读某个技能的 SKILL.md 正文(目录里只有名字和摘要)。同名多份时取真正生效的那一份;要特意看某个被覆盖的副本就传 root。会话里已有官方 skill 工具时这条不下发。",
2432
- "compat.tools.hover.skill_manager_set_enabled": "按技能名或按来源目录启停。停用的技能不进目录也不能被加载;同名冲突时只有生效的那份有用 —— 启错了副本等于没启。",
2433
- "compat.tools.hover.skill_manager_create": "在指定来源目录下新建技能(名字、描述、SKILL.md 正文)。技能目录会进每一轮上下文,描述写清「什么时候用它」,模型才找得到。",
2688
+ "compat.tools.hover.skill_manager_switch": "按技能名或按来源目录启停。停用的技能不进目录也不能被加载;同名冲突时只有生效的那份有用 —— 启错了副本等于没启。",
2689
+ "compat.tools.hover.skill_manager_save": "新建一个技能,或改掉本插件自己写的那份同名技能 —— 回执会说清走了哪条。落点是 hub 的 tool-management/skills/(与界面「创建技能」一致),新建的默认停用、要用得先启用;官方根里的技能不改。技能目录会进每一轮上下文,描述要写清「什么时候用它」。",
2434
2690
  "compat.tools.hover.subagent_manager_list": "列出可用人设(子智能体的角色定义)与各自的描述。常驻目录给的是同一份清单;这个工具拿到的永远是当前完整版,包括目录没注入到的会话。",
2435
2691
  "compat.tools.hover.subagent_manager_run": "把任务交给某个具名人设:它拿到人设当自己的系统提示词、在自己的上下文里做完、只回最终结果。开 inherit 它会看到本次会话已完成的对话。花真 token,默认要用户确认。",
2436
- "compat.tools.hover.subagent_manager_set_enabled": "停用的人设不进目录也不能被委派;被场景绑定的人设,在进入该场景时会自动启用。",
2437
- "compat.tools.hover.subagent_manager_create": "新建人设:名字、描述、正文(会作为子代理的系统提示词),可带 provider、模型与工具限制。描述决定模型什么时候会选它。",
2438
- "compat.tools.hover.subagent_manager_update": "改一个人设:正文、描述、provider、模型、工具限制,或改名(场景与档案里的绑定会跟着改)。没传的字段保持原样。",
2692
+ "compat.tools.hover.subagent_manager_switch": "停用的人设不进目录也不能被委派;被场景绑定的人设,在进入该场景时会自动启用。",
2693
+ "compat.tools.hover.subagent_manager_save": "新建人设,或改掉已存在的那份 —— 回执会说清走了哪条。可改名字、描述、正文(正文会作为子代理的系统提示词)与输出要求,没传的字段保持原样;改名会连带改场景与档案里的绑定。provider / 模型 / 工具限制只留给面板(模型看不到自己改对了没有,所以不给)。描述决定模型什么时候会选它。",
2439
2694
  "compat.tools.hover.prompt_manager_list": "列出提示词预设与摘要,标明当前生效的是哪一份 —— 同时只有一份生效。要换用哪份之前先用它确认名字。",
2440
2695
  "compat.tools.hover.prompt_manager_apply": "把某份预设应用到用户级 AGENTS.md,之后的对话立刻按它走。一次只有一份在生效,换一份就是替换上一份。",
2441
2696
  "compat.live.title": "注入实况", "compat.live.hint": "模型实际看到的内容(只读)",
@@ -2454,12 +2709,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2454
2709
  "compat.live.state.official": "官方注入", "compat.live.state.official.title": "提示词与技能目录在标准类预设下由官方通道送达,本插件不重复注入(极简这类官方没挂的预设才由本插件兜底)",
2455
2710
  "compat.live.state.empty": "无内容", "compat.live.est": "≈{count} token",
2456
2711
  "compat.live.state.absent.title": "本插件负责这个域,但当前不在上下文里(可能刚被压缩,下一步会自动补发)",
2457
- "compat.reach.mark.memory": "场景和记忆", "compat.reach.mark.agentsMd": "提示词", "compat.reach.mark.skill": "技能", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "子智能体",
2712
+ "compat.reach.mark.scene": "场景", "compat.reach.mark.memory": "记忆", "compat.reach.mark.agentsMd": "提示词", "compat.reach.mark.skill": "技能", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "子智能体",
2458
2713
  "compat.reach.default": "默认",
2459
2714
  "compat.reach.whyUnknown": "判断不了", "compat.reach.broken": "预设损坏",
2460
2715
  "compat.reach.tools.title": "工具能用 ≠ 模型知道",
2461
2716
  "compat.reach.tools.1": "工具由本插件注册,任何预设下都能调用;怎么送到模型由预设决定。",
2462
- "compat.reach.tools.2": "场景和记忆 / MCP / 技能 / 子智能体 / 提示词 —— 由本插件按上面的「注入」块补齐。",
2717
+ "compat.reach.tools.2": "场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词 —— 由本插件按上面的「注入」块补齐。",
2463
2718
  "compat.reach.tools.3": "提示词与技能官方自己也会送;官方有的不重复送,官方缺的(如极简)由本插件补上。",
2464
2719
  "compat.hint.doctor": "命令行体检:",
2465
2720
  "scenes.title": "场景", "scenes.desc": "管理场景:预设 MCP、技能、子智能体与记忆的组合,一键切换。",
@@ -2557,6 +2812,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2557
2812
  "subagents.field.description": "描述", "subagents.field.description.placeholder": "例如 擅长 Java 后端实现与重构",
2558
2813
  "subagents.field.model": "模型", "subagents.field.model.placeholder": "自定义模型 id", "subagents.field.model.hint": "留空继承主会话", "subagents.field.provider": "模型来源", "subagents.field.provider.placeholder": "deepseek", "subagents.field.provider.hint": "「模型来源」与「模型」是一对:只填模型会落在主会话的来源上,跨来源会解析失败。",
2559
2814
  "subagents.field.catalogDepth": "目录注入", "subagents.field.catalogDepth.onlyTop": "只在顶层", "subagents.field.catalogDepth.toChild": "顶层和子会话", "subagents.field.catalogDepth.toGrand": "顶层和两层子会话", "subagents.field.catalogDepth.unlimited": "不限制嵌套", "subagents.field.catalogDepth.hint": "决定人设目录注入到哪几层会话。\n只影响目录注入,不影响委派 —— 子代理始终可以继续委派。\n子会话看不到目录时,可用 subagent_manager_list 查询。",
2815
+ // 思考强度(0.14.0)。界面上只留"这一格现在能选什么",不解释机制 ——
2816
+ // 官方语义(改 provider/model 会丢掉继承值、不支持的档位直接拒)记在 CHANGELOG / update.md,
2817
+ // 不搬进表单。
2818
+ "subagents.field.effort": "思考强度", "subagents.effort.default": "默认(模型的默认强度)", "subagents.effort.none": "默认", "subagents.effort.needModel": "当前默认主会话思考强度", "subagents.effort.loading": "正在问该模型支持哪些档位…", "subagents.effort.unavailable": "该模型没有暴露可选档位,默认思考强度", "subagents.effort.hint": "当前可选择多种思考强度", "subagents.effort.dropped": "换模型后「{effort}」不在新模型的档位里,已清空。",
2560
2819
  "subagents.model.inherit": "继承主会话(不指定)", "subagents.model.customOption": "自定义 / 目录里没有…",
2561
2820
  "subagents.field.modes": "工具限制(按 Agent 预设)",
2562
2821
  "subagents.field.modes.hint": "只有当前模式那一行生效,其他模式用该模式的全部工具。",
@@ -2573,7 +2832,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2573
2832
  "subagents.legacy.convert": "转换到按模式设置",
2574
2833
  "subagents.legacy.hint": "转换会把旧名单搬进你选的模式;不点它,文件里的旧键原样保留(运行时照旧生效)。",
2575
2834
  "subagents.tools.filter": "筛选工具名", "subagents.tools.remove": "移除", "subagents.toolFailed": "子代理工具没注册上:{names}({reason})。这几个工具在子智能体流程里用不了;重启 DSH 后仍如此请核对宿主版本。",
2576
- "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型 {model} · 目录注入 {depth} · 模式限制 {modes} 项 · 旧格式 白名单 {allow} / 黑名单 {deny}", "subagents.adv.note": "白名单 = 子代理只能用勾选的工具。\n黑名单 = 除勾选的以外都能用。\nMCP 工具不在候选里:子代理始终能用当前在跑的 MCP。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
2835
+ "subagents.adv.title": "高级选项", "subagents.adv.inherit": "继承主会话", "subagents.adv.summary": "模型:{model};思考强度:{effort};目录注入:{depth}", "subagents.adv.summary2": "模式限制:{modes} 项;旧格式:白名单 {allow} / 黑名单 {deny}", "subagents.adv.note": "白名单 = 子代理只能用勾选的工具。\n黑名单 = 除勾选的以外都能用。\nMCP 工具不在候选里:子代理始终能用当前在跑的 MCP。", "subagents.adv.loadFailed": "读取候选数据失败(模型目录 / 工具清单);仍可手动填写。",
2577
2836
  "preset.name.standard": "标准模式", "preset.name.ptc": "PTC 模式", "preset.name.minimal": "极简模式", "preset.name.cordis": "创造模式",
2578
2837
  "preset.short.standard": "标准", "preset.short.ptc": "PTC", "preset.short.minimal": "极简", "preset.short.cordis": "创造",
2579
2838
  "subagents.field.body": "人设提示词", "subagents.field.body.placeholder": "写下这个人设的身份、职责与工作方式…",
@@ -2591,7 +2850,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2591
2850
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
2592
2851
  "error.import.noFiles": "没有选择要导入的文件",
2593
2852
  "nav.title": "工具",
2594
- "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.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "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 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "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": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用场景即以它为全局基线。\n启用期间在提示词页「应用」别的预设 = 改绑到那一份(同步写进档案)。", "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.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{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.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "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.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "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.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "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.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
2853
+ "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.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "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 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "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": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用场景即以它为全局基线。\n启用期间在提示词页「应用」别的预设 = 改绑到那一份(同步写进档案)。", "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.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{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.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "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.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "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.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "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.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
2595
2854
  },
2596
2855
  en: {
2597
2856
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -2629,7 +2888,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2629
2888
  "empty.search": "No matching skills", "empty.source": "This source is missing or has no skills", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
2630
2889
  "source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; a default source, always read — it cannot be removed or toggled off",
2631
2890
  "detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
2632
- "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "skill_manager_create in chat creates user-level skills",
2891
+ "create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "skill_manager_save in chat creates user-level skills",
2633
2892
  "import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The project directory may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
2634
2893
  "error.custom.absolute": "The folder path must be absolute: {path}", "error.custom.notDirectory": "Not a folder: {path}", "error.custom.unreadable": "Could not read the folder: {path}", "error.custom.overlap": "The folder overlaps an existing skill source: {path} ↔ {other}", "error.custom.invalidKey": "Invalid custom source key: {key}", "error.custom.notFound": "Custom source not found: {key}",
2635
2894
  // 逐字对应宿主 http-fence.ts 的 TOKEN_MSG_*(见中文侧注释)。
@@ -2782,7 +3041,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2782
3041
  "compat.token.failed": "Could not read the token status; refresh and retry.",
2783
3042
  "compat.inject.force": "Also inject in Minimal mode", "compat.inject.force.desc": "Off by default (follow the preset)",
2784
3043
  "compat.inject.domains": "What to inject",
2785
- "compat.inject.domain.memory": "Scene + memory", "compat.inject.domain.mcp": "MCP",
3044
+ "compat.inject.domain.scene": "Scene", "compat.inject.domain.memory": "Memory", "compat.inject.domain.mcp": "MCP",
2786
3045
  "compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Prompt",
2787
3046
  "compat.inject.domain.skills": "Skills",
2788
3047
  "compat.inject.domain.carrier.title": "Turning this off also stops the copy the system sends by itself",
@@ -2793,47 +3052,74 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2793
3052
  "compat.tools.other": "Other",
2794
3053
  "compat.tools.onTitle": "Sent: the model can see and call it",
2795
3054
  "compat.tools.offTitle": "Switched off: not sent at all, so the model cannot see or call it (the panel is unaffected)",
3055
+ "compat.tools.summaryDefault": "of which ≈{tok} is saved by the {n} tools that ship switched off (leave them alone if the model should not do those things; ticking one is local to this machine)",
3056
+ "compat.tools.defaultOffTitle": "Switched off in the shipped default: the model cannot see or call it, the panel is unaffected. Tick it here if you want the model to do this (this machine only).",
3057
+ // "Sets": store a whole tick pattern under a name and switch back to it in one click.
3058
+ "compat.tools.preset.save": "Save set",
3059
+ "compat.tools.preset.apply": "Restore set",
3060
+ "compat.tools.preset.save.tip": "Store this tick pattern under a name so you can switch back to it in one click",
3061
+ "compat.tools.preset.apply.tip": "Switch to a stored set (including the undeletable shipped default)",
3062
+ "compat.tools.preset.apply.tipEmpty": "No sets stored yet; you can still look at the shipped default here",
3063
+ "compat.tools.preset.saveTitle": "Save the current set",
3064
+ "compat.tools.preset.applyTitle": "Restore a set",
3065
+ "compat.tools.preset.saveHint": "Right now: {on}/{total} sent · ≈{tok} tok per request.",
3066
+ "compat.tools.preset.name": "Set name",
3067
+ "compat.tools.preset.namePlaceholder": "e.g. minimal / let it manage servers",
3068
+ "compat.tools.preset.saveYes": "Save it",
3069
+ "compat.tools.preset.applyHint": "Pick one: the tool table the model receives is rebuilt immediately, and the panel is unaffected.",
3070
+ "compat.tools.preset.default": "Shipped default",
3071
+ "compat.tools.preset.builtin": "cannot delete",
3072
+ "compat.tools.preset.count": "{on}/{total} sent · ≈{tok} tok per request",
3073
+ "compat.tools.preset.use": "Use this",
3074
+ "compat.tools.preset.removeTitle": "Delete the set “{name}” (only that record; your current ticks stay)",
3075
+ "compat.tools.preset.err.nameEmpty": "The name cannot be empty",
3076
+ "compat.tools.preset.err.nameTaken": "There is already a set called “{name}” — saving never overwrites it. Choose another name (or delete that one first).",
3077
+ "compat.tools.preset.err.limit": "At most {max} sets — delete one before saving another",
3078
+ "compat.tools.preset.err.notFound": "That set is gone (someone else deleted it)",
3079
+ "compat.tools.preset.err.net": "Not saved: no reply from the local service",
2796
3080
  // Two tiers per tool (inline one-liner / slightly longer hover). Same shape everywhere:
2797
- // the inline line starts with a verb and has no full stop; the hover adds when to use it,
2798
- // what it changes and where the limit is. Keys are built from the tool name.
2799
- "compat.tools.about.memory_manager_list": "List memories (only the injected ones by default)",
2800
- "compat.tools.about.memory_manager_read": "Read one memory's full body by id",
2801
- "compat.tools.about.memory_manager_set_enabled": "Enable or disable one memory",
2802
- "compat.tools.about.memory_manager_write": "Create a memory",
2803
- "compat.tools.about.memory_manager_update": "Update a memory (body, description, name)",
2804
- "compat.tools.about.mcp_manager_list": "List servers, state and tools",
2805
- "compat.tools.about.mcp_manager_set_enabled": "Enable or disable a server or one tool",
2806
- "compat.tools.about.mcp_manager_restart": "Restart a server",
2807
- "compat.tools.about.mcp_manager_add": "Add an MCP server",
3081
+ // the inline line starts with a verb and has no full stop, and it is an abbreviation of
3082
+ // what the tool can actually do — several behaviours are joined with "/", never "or"
3083
+ // (these tools really do both: `save` creates *and* updates, `switch` enables *and*
3084
+ // disables). Keys are built from the tool name.
3085
+ "compat.tools.about.memory_manager_list": "List memories and their state",
3086
+ "compat.tools.about.memory_manager_read": "Read a memory body by id",
3087
+ "compat.tools.about.memory_manager_switch": "Enable/disable a memory",
3088
+ "compat.tools.about.memory_manager_save": "Create/update a memory",
3089
+ "compat.tools.about.scene_manager_list": "List scenes and their archives",
3090
+ "compat.tools.about.scene_manager_switch": "Enter/leave a scene",
3091
+ "compat.tools.about.scene_manager_save": "Create/update a scene, write its archive, bind a prompt",
3092
+ "compat.tools.about.mcp_manager_list": "List servers and tools",
3093
+ "compat.tools.about.mcp_manager_switch": "Enable/disable/restart a server",
3094
+ "compat.tools.about.mcp_manager_save": "Add/update a server",
2808
3095
  "compat.tools.about.skill_manager_list": "List skills with state and path",
2809
3096
  "compat.tools.about.skill_manager_read": "Read a skill body by name",
2810
- "compat.tools.about.skill_manager_set_enabled": "Enable or disable a skill or source",
2811
- "compat.tools.about.skill_manager_create": "Create a skill",
3097
+ "compat.tools.about.skill_manager_switch": "Enable/disable a skill or source",
3098
+ "compat.tools.about.skill_manager_save": "Create/update a skill",
2812
3099
  "compat.tools.about.subagent_manager_list": "List personas and descriptions",
2813
3100
  "compat.tools.about.subagent_manager_run": "Run a persona as a subagent",
2814
- "compat.tools.about.subagent_manager_set_enabled": "Enable or disable a persona",
2815
- "compat.tools.about.subagent_manager_create": "Create a persona",
2816
- "compat.tools.about.subagent_manager_update": "Update a persona (can rename)",
3101
+ "compat.tools.about.subagent_manager_switch": "Enable/disable a persona",
3102
+ "compat.tools.about.subagent_manager_save": "Create/update a persona",
2817
3103
  "compat.tools.about.prompt_manager_list": "List prompt presets",
2818
3104
  "compat.tools.about.prompt_manager_apply": "Apply a prompt preset",
2819
- "compat.tools.hover.memory_manager_list": "Lists each memory's id, scene, on/off state and one-line description. Only the ones that would actually be injected by default; all=true adds disabled, inactive-scene and shadowed entries. Read-only.",
3105
+ "compat.tools.hover.memory_manager_list": "Lists each memory's id, scene, on/off state and one-line description, and says which scene injection follows. Only the memories that would actually be injected by default; all=true adds disabled, inactive-scene and shadowed entries. The scenes themselves (archives, bound presets, locks) are in scene_manager_list. Read-only.",
2820
3106
  "compat.tools.hover.memory_manager_read": "Reads the full body of one memory by id (<scene>/<name>). The list carries a one-line description only — use this for the body, especially for memories that are off, in an inactive scene, or dropped by the byte budget.",
2821
- "compat.tools.hover.memory_manager_set_enabled": "Flips the index switch only — the .md file is untouched and it is reversible. A disabled memory stops being injected, which frees byte budget for the others; prefer this over deleting when you want to spend fewer tokens.",
2822
- "compat.tools.hover.memory_manager_write": "Creates a memory in a scene (the scene must exist; global is the always-on one). Memories enter every later request, so record only what cannot be re-derived from code, config or git — and check the list first for an entry that already says this.",
2823
- "compat.tools.hover.memory_manager_update": "Updates an existing memory: body, description, or rename (the old file stays in the trash). Omitted fields are kept as they are; read it first and change only the part that needs changing.",
2824
- "compat.tools.hover.mcp_manager_list": "Lists the configured servers, whether each is connected, which tools it exposes, and the note you wrote for it. Servers that connected before but not now are marked — that means this one is broken, not unconfigured.",
2825
- "compat.tools.hover.mcp_manager_set_enabled": "Disables a whole server, or a single tool on it. A disabled tool disappears from the model-visible tool table and is blocked at execution; this survives a DSH restart.",
2826
- "compat.tools.hover.mcp_manager_restart": "Disables the server, waits for it to drop, then enables it back the way it was — use it after a config change or when a server is stuck. Its tools disappear briefly during the restart.",
2827
- "compat.tools.hover.mcp_manager_add": "Adds a server (streamable-http or stdio) with URL, command, args, headers, env and a note, at project or global level. Only on the user's explicit request.",
3107
+ "compat.tools.hover.memory_manager_switch": "Flips the index switch only — the .md file is untouched and it is reversible. A disabled memory stops being injected, which frees byte budget for the others; prefer this over deleting when you want to spend fewer tokens.",
3108
+ "compat.tools.hover.memory_manager_save": "Creates a memory, or updates the existing one with that id — the receipt says which. Memories enter every later request, so record only what cannot be re-derived from code, config or git — and check the list first for an entry that already says this. Omitted fields are kept; a rename leaves the old file in the trash.",
3109
+ "compat.tools.hover.scene_manager_list": "Lists every scene: its name (with the display label when it differs — tools need the name), enabled / entered / locked state, its description, bound preset, memory count, and its archive: which MCP servers, skills and personas entering it switches on, everything else going off. Look here before changing a scene's archive — saving replaces a whole section, so an unseen selection gets dropped — and use it to pick which scene to enter. Read-only.",
3110
+ "compat.tools.hover.scene_manager_switch": "Enters a scene: its archive is applied across MCP, skills and personas (anything it does not list goes off), injection narrows to its memories, and its bound preset is written into the global AGENTS.md. Leaving restores the runtime snapshot taken on entry, including the previous baseline. Check the scene's archive in scene_manager_list first. It rewrites the whole runtime environment, so it asks for confirmation by default.",
3111
+ "compat.tools.hover.scene_manager_save": "Saves a scene — a named bundle the user enters: whatever its archive lists gets switched on, everything else off. Writing the archive does not turn the scene on — enter it with scene_manager_switch (or the Scenes page) for that. Sections you omit keep their current value; an empty section means \"switch nothing on\", not \"no restriction\". A section you do pass is replaced whole, so read the current one in scene_manager_list first; the receipt lists what got dropped.",
3112
+ "compat.tools.hover.mcp_manager_list": "Lists the configured servers, whether each is connected, which tools it exposes, and the note you wrote for it. Servers that connected before but not now are marked — that means this one is broken, not unconfigured. tools=true also lists each server's tool names, marking the switched-off ones (off); a server that is off only has the names recorded from its last run.",
3113
+ "compat.tools.hover.mcp_manager_switch": "action=on/off enables or disables a whole server; pass `tool` to switch just that one tool (a disabled tool disappears from the model-visible tool table and is blocked at execution, and it survives a DSH restart); action=restart disables the server, waits for it to drop, then enables it back the way it was — use it after a config change or when a server is stuck. Pass level when the same server name exists at both project and global level.",
3114
+ "compat.tools.hover.mcp_manager_save": "Adds a server (streamable-http or stdio) or updates the one with that name — the receipt says which. When updating, every field you omit keeps its current value, so changing one URL does not mean restating the whole config (and the secrets survive); for stdio, command+args is the command the host actually spawns, and changing it raises a confirmation card. Only on the user's explicit request.",
2828
3115
  "compat.tools.hover.skill_manager_list": "Lists each skill's on/off state, whether it wins or is shadowed, and its source path. The always-on catalog carries names and summaries only — use this to see what you turned off and which copy of a name wins; all=true includes disabled and shadowed copies.",
2829
3116
  "compat.tools.hover.skill_manager_read": "Reads one skill's SKILL.md body by name (the catalog has names and summaries only). Name collisions resolve to the copy actually in effect; pass root to inspect a shadowed copy instead. Not sent in conversations where the official skill tool is mounted.",
2830
- "compat.tools.hover.skill_manager_set_enabled": "Toggles by skill name or by source directory. Disabled skills leave the catalog and cannot be loaded; when a name exists in several roots only the winning copy counts, so enabling the wrong one changes nothing.",
2831
- "compat.tools.hover.skill_manager_create": "Creates a skill in the given source (name, description, SKILL.md body). The catalog is injected into every request, so make the description say when to use it — that is how the model finds it.",
3117
+ "compat.tools.hover.skill_manager_switch": "Toggles by skill name or by source directory. Disabled skills leave the catalog and cannot be loaded; when a name exists in several roots only the winning copy counts, so enabling the wrong one changes nothing.",
3118
+ "compat.tools.hover.skill_manager_save": "Creates a skill, or updates the copy this plugin wrote itself — the receipt says which. It lands in hub (tool-management/skills/), matching the panel's \"Create skill\"; a new skill starts disabled, so enable it before use, and skills living in the official roots are never touched. The catalog is injected into every request, so make the description say when to use it.",
2832
3119
  "compat.tools.hover.subagent_manager_list": "Lists the available personas (pre-configured subagent roles) with their descriptions. The always-on catalog carries the same list; this tool always returns the current, complete version — including conversations the catalog does not reach.",
2833
3120
  "compat.tools.hover.subagent_manager_run": "Hands a task to a named persona: it gets the persona as its own system prompt, works in its own context and returns only the final result. With inherit it also sees this conversation's finished turns. It spends real tokens and asks the user by default.",
2834
- "compat.tools.hover.subagent_manager_set_enabled": "A disabled persona leaves the catalog and cannot be delegated to; personas bound to a scene are enabled automatically when that scene is entered.",
2835
- "compat.tools.hover.subagent_manager_create": "Creates a persona: name, description and body (the body becomes the subagent's system prompt), plus optional provider, model and tool limits. The description decides when the model picks it.",
2836
- "compat.tools.hover.subagent_manager_update": "Updates a persona: body, description, provider, model, tool limits, or a rename (scene and archive bindings follow the rename). Omitted fields are kept as they are.",
3121
+ "compat.tools.hover.subagent_manager_switch": "A disabled persona leaves the catalog and cannot be delegated to; personas bound to a scene are enabled automatically when that scene is entered.",
3122
+ "compat.tools.hover.subagent_manager_save": "Creates a persona, or updates the existing one — the receipt says which. You can set the name, description, body (the body becomes the subagent's system prompt) and output contract; omitted fields keep their current values, and a rename makes scene and archive bindings follow. Provider, model and tool limits stay in the panel (the model cannot tell whether it got them right, so it is not offered them). The description decides when the model picks it.",
2837
3123
  "compat.tools.hover.prompt_manager_list": "Lists the prompt presets with their summaries and marks the one currently in effect — only one is active at a time. Use it to confirm the name before switching.",
2838
3124
  "compat.tools.hover.prompt_manager_apply": "Applies a preset to the user-level AGENTS.md; later conversations follow it immediately. Only one preset is in effect, so applying another replaces it.",
2839
3125
  "compat.live.title": "Live injection", "compat.live.hint": "What the model actually sees (read-only)",
@@ -2852,12 +3138,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2852
3138
  "compat.live.state.official": "Official", "compat.live.state.official.title": "The official carrier delivers these under standard presets; the plugin does not duplicate (it only fills in for Minimal-like presets)",
2853
3139
  "compat.live.state.empty": "Nothing to send", "compat.live.est": "≈{count} tokens",
2854
3140
  "compat.live.state.absent.title": "This plugin owns this domain but it is not in context right now (it may have just been compacted; the next step re-sends it)",
2855
- "compat.reach.mark.memory": "Scene + memory", "compat.reach.mark.agentsMd": "Prompt", "compat.reach.mark.skill": "Skills", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "Subagents",
3141
+ "compat.reach.mark.scene": "Scene", "compat.reach.mark.memory": "Memory", "compat.reach.mark.agentsMd": "Prompt", "compat.reach.mark.skill": "Skills", "compat.reach.mark.mcp": "MCP", "compat.reach.mark.subagent": "Subagents",
2856
3142
  "compat.reach.default": "default",
2857
3143
  "compat.reach.whyUnknown": "cannot tell", "compat.reach.broken": "preset broken",
2858
3144
  "compat.reach.tools.title": "Tools work ≠ the model knows",
2859
3145
  "compat.reach.tools.1": "The tools come from this plugin and are callable under any preset; how they reach the model is up to the preset.",
2860
- "compat.reach.tools.2": "Scene + memory / MCP / skills / subagents / prompt — filled in by this plugin according to the Injection block above.",
3146
+ "compat.reach.tools.2": "Scene / memory / MCP / skills / subagents / prompt — filled in by this plugin according to the Injection block above.",
2861
3147
  "compat.reach.tools.3": "The prompt and the skill catalog also come from the official rows; this plugin only fills in what they miss (e.g. under Minimal).",
2862
3148
  "compat.hint.doctor": "Command-line check:",
2863
3149
  "scenes.title": "Scenes", "scenes.desc": "Manage scenes: preset combinations of MCP, skills, subagents and memories, switch with one click.",
@@ -2954,6 +3240,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2954
3240
  "subagents.field.description": "Description", "subagents.field.description.placeholder": "e.g. Senior Java engineer",
2955
3241
  "subagents.field.model": "Model", "subagents.field.model.placeholder": "Custom model id", "subagents.field.model.hint": "Blank inherits the main session", "subagents.field.provider": "Model provider", "subagents.field.provider.placeholder": "deepseek", "subagents.field.provider.hint": "Provider and model are a pair: a model alone resolves against the main session's provider and fails across providers.",
2956
3242
  "subagents.field.catalogDepth": "Catalog injection", "subagents.field.catalogDepth.onlyTop": "Top level only", "subagents.field.catalogDepth.toChild": "Top level and subagents", "subagents.field.catalogDepth.toGrand": "Top level and two levels down", "subagents.field.catalogDepth.unlimited": "No nesting limit", "subagents.field.catalogDepth.hint": "Which levels of sessions get the persona catalog.\nIt affects the catalog only — subagents can always delegate further.\nWhen the catalog is not injected, subagent_manager_list still works.",
3243
+ // Reasoning effort (0.14.0). The form only says what this field offers right now; the
3244
+ // official semantics (the host drops the inherited level when provider/model change, and an
3245
+ // unsupported level is rejected rather than clamped) live in CHANGELOG / update.md, not here.
3246
+ "subagents.field.effort": "Reasoning effort", "subagents.effort.default": "Default (the model's default)", "subagents.effort.none": "default", "subagents.effort.needModel": "Currently the main session's default reasoning effort.", "subagents.effort.loading": "Asking the model which levels it supports…", "subagents.effort.unavailable": "This model exposes no selectable levels — default reasoning effort.", "subagents.effort.hint": "Several reasoning-effort levels are available.", "subagents.effort.dropped": "“{effort}” is not among the new model's levels, so it was cleared.",
2957
3247
  "subagents.model.inherit": "Inherit the main session (unspecified)", "subagents.model.customOption": "Custom / not in the catalogue…",
2958
3248
  "subagents.field.modes": "Tool limits (per agent preset)",
2959
3249
  "subagents.field.modes.hint": "Only the row for the current preset applies; other presets keep their full tool set.",
@@ -2970,7 +3260,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2970
3260
  "subagents.legacy.convert": "Convert into a preset row",
2971
3261
  "subagents.legacy.hint": "Converting moves the old list into the preset you pick; until then the old keys stay untouched in the file (and keep working at run time).",
2972
3262
  "subagents.tools.filter": "Filter tool names", "subagents.tools.remove": "Remove", "subagents.toolFailed": "Subagent tools failed to register: {names} ({reason}). They cannot be used in the subagent flow; if this persists after a DSH restart, check the host version.",
2973
- "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model {model} · catalog {depth} · preset limits {modes} · legacy allow {allow} / deny {deny}", "subagents.adv.note": "Allowlist = the subagent may only use the picked tools.\nDenylist = everything except the picked ones.\nMCP tools are not listed: a subagent always keeps the MCP servers currently running.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
3263
+ "subagents.adv.title": "Advanced options", "subagents.adv.inherit": "inherit", "subagents.adv.summary": "model: {model}; effort: {effort}; catalog: {depth}", "subagents.adv.summary2": "preset limits: {modes}; legacy allow: {allow} / deny: {deny}", "subagents.adv.note": "Allowlist = the subagent may only use the picked tools.\nDenylist = everything except the picked ones.\nMCP tools are not listed: a subagent always keeps the MCP servers currently running.", "subagents.adv.loadFailed": "Could not read the candidate data (model catalogue / tool list); you can still fill the fields by hand.",
2974
3264
  "preset.name.standard": "Standard mode", "preset.name.ptc": "PTC mode", "preset.name.minimal": "Minimal mode", "preset.name.cordis": "Creator mode",
2975
3265
  "preset.short.standard": "Standard", "preset.short.ptc": "PTC", "preset.short.minimal": "Minimal", "preset.short.cordis": "Creator",
2976
3266
  "subagents.field.body": "Persona prompt", "subagents.field.body.placeholder": "Describe the persona's role, responsibilities, and working style…",
@@ -2988,7 +3278,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2988
3278
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
2989
3279
  "error.import.noFiles": "No files selected",
2990
3280
  "nav.title": "Tools",
2991
- "agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” 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}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "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.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.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.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "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 prompt", "agm.btn.import": "Import prompts", "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": "id (directory 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; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (for humans only)", "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)", "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.err.archive": "Archive 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": "Directory 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": "Prompt preset", "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.field.prompt.hint": "While the scene is on, this preset is the global baseline.\nApplying another preset on the Prompts page rebinds the scene to it (written into the profile too).", "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.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {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.row.notes": "Overrides the notes of {count} server(s) with scene notes", "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, so deleting it would lose that fallback", "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); click “Apply again” 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.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "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": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "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.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.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
3281
+ "agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” 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}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "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.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.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.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "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 prompt", "agm.btn.import": "Import prompts", "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": "id (directory 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; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "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)", "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.err.archive": "Archive 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": "Directory 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": "Prompt preset", "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.field.prompt.hint": "While the scene is on, this preset is the global baseline.\nApplying another preset on the Prompts page rebinds the scene to it (written into the profile too).", "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.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {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.row.notes": "Overrides the notes of {count} server(s) with scene notes", "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, so deleting it would lose that fallback", "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); click “Apply again” 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.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "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": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "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.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.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
2992
3282
  }
2993
3283
  };
2994
3284
 
@@ -4211,7 +4501,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4211
4501
  id: id,
4212
4502
  ...(content ? { content: content } : {}),
4213
4503
  ...(from ? { from: from } : {}),
4214
- // 描述只给使用者看(存 meta.json,不进 AGENTS.md)。
4504
+ // 描述存 meta.json,**不写进 AGENTS.md**(那个文件的正文会被原样注入)。
4505
+ // 0.14.0 起模型也在 `prompt_manager_list` 的清单里看得到它,所以有了字数上限
4506
+ // (输入框那枚 maxLength / 计数就是它)。
4215
4507
  description: String(description == null ? '' : description),
4216
4508
  }).then(function (res) {
4217
4509
  setBusy(null)
@@ -4368,8 +4660,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4368
4660
  React.createElement('input', { className: 'dsm-control', value: editModal.nextId || '', onChange: function (e) { setEditModal(Object.assign({}, editModal, { nextId: e.target.value, error: null })) } }),
4369
4661
  React.createElement('p', { className: 'dsm-help' }, t('agm.field.id.hint'))),
4370
4662
  React.createElement('div', { className: 'dsm-field' },
4371
- React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
4372
- React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.desc.placeholder'), value: editModal.description || '', onChange: function (e) { setEditModal(Object.assign({}, editModal, { description: e.target.value })) } })),
4663
+ // 上限与宿主侧 `src/prompts/service.ts` 的 `DEFAULT_PRESET_DESC_MAX_LENGTH`
4664
+ // 同数(客户端镜像 `PRESET_DESC_MAX`):描述从 0.14.0 起会进模型清单
4665
+ // (`prompt_manager_list` 要打出来,模型才能在预设之间做选择),所以它有了
4666
+ // 预算 —— 写多少模型就能读到多少,与 MCP 备注那条同一条口径。
4667
+ React.createElement('div', { className: 'dsm-budget-meta' },
4668
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
4669
+ React.createElement('span', { className: 'dsm-char-count' + (String(editModal.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(editModal.description || '').length + ' / ' + PRESET_DESC_MAX)),
4670
+ React.createElement('input', { className: 'dsm-control', maxLength: PRESET_DESC_MAX, placeholder: t('agm.field.desc.placeholder'), value: editModal.description || '', onChange: function (e) { setEditModal(Object.assign({}, editModal, { description: e.target.value })) } })),
4373
4671
  React.createElement('div', { className: 'dsm-field' },
4374
4672
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.content')),
4375
4673
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', value: editModal.content, onChange: function (e) { setEditModal(Object.assign({}, editModal, { content: e.target.value })) } })),
@@ -4384,8 +4682,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4384
4682
  React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.id.placeholder'), value: createModal.id, onChange: function (e) { setCreateModal(Object.assign({}, createModal, { id: e.target.value, error: null })) } }),
4385
4683
  React.createElement('p', { className: 'dsm-help' }, t('agm.field.id.hint'))),
4386
4684
  React.createElement('div', { className: 'dsm-field' },
4387
- React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
4388
- React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.desc.placeholder'), value: createModal.description || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { description: e.target.value })) } })),
4685
+ React.createElement('div', { className: 'dsm-budget-meta' },
4686
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
4687
+ React.createElement('span', { className: 'dsm-char-count' + (String(createModal.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(createModal.description || '').length + ' / ' + PRESET_DESC_MAX)),
4688
+ React.createElement('input', { className: 'dsm-control', maxLength: PRESET_DESC_MAX, placeholder: t('agm.field.desc.placeholder'), value: createModal.description || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { description: e.target.value })) } })),
4389
4689
  (state.presets || []).length ? React.createElement('div', { className: 'dsm-field' },
4390
4690
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.copyFrom')),
4391
4691
  React.createElement(SourceSelect, {
@@ -4792,14 +5092,16 @@ function enabledMemoryCount(rules) {
4792
5092
  function removedSkillRoots(roots) { return (roots || []).filter(function (root) { return root.removed === true; }); }
4793
5093
  var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
4794
5094
  /**
4795
- * 注入段里"模型实际能看到多长"的两个截断长度,同时当输入框的 `maxLength` 与字数计数的分母
5095
+ * 注入段里"模型实际能看到多长"的截断长度,同时当输入框的 `maxLength` 与字数计数的分母
4796
5096
  * —— 写多少就能被看到多少,不会出现"界面让你写 500 字、模型只读到前 300"。
4797
5097
  *
4798
5098
  * ⚠️ 宿主侧各有一份真相:`src/mcp/state-section.ts` 的 `DEFAULT_MCP_NOTE_MAX_LENGTH`、
4799
- * `src/skills/catalog.ts` 与 `src/subagents/catalog.ts` 的 `..._DESCRIPTION_MAX_LENGTH`。
5099
+ * `src/skills/catalog.ts` 与 `src/subagents/catalog.ts` 的 `..._DESCRIPTION_MAX_LENGTH`、
5100
+ * `src/prompts/service.ts` 的 `DEFAULT_PRESET_DESC_MAX_LENGTH`(提示词预设描述 —— 0.14.0
5101
+ * 起它会进模型清单,所以有了预算)。
4800
5102
  * 两边没有编译期约束(计划文档 A3 说的"同一事实两处写"),改一处必须改另一处。
4801
5103
  */
4802
- var MCP_NOTE_MAX = 300, CATALOG_DESC_MAX = 500;
5104
+ var MCP_NOTE_MAX = 300, CATALOG_DESC_MAX = 500, PRESET_DESC_MAX = 300;
4803
5105
  function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
4804
5106
  function inspectUploadSelection(files) {
4805
5107
  var list = Array.prototype.slice.call(files || []); if (!list.length) return null;
@@ -7592,6 +7894,22 @@ function enabledMemoryCount(rules) {
7592
7894
  )
7593
7895
  }
7594
7896
  // ---------- 子智能体页:人设文件(~/.dsh/subagents/*.md)管理 ----------
7897
+ /**
7898
+ * 「思考强度」档位的按 (provider, model) 缓存。
7899
+ *
7900
+ * 为什么放在**组件外**:放进组件里每次渲染都会重建,等于没有缓存,而档位清单要问
7901
+ * adapter(`llm.resolveModelInfo`,异步、可能联网)—— 编辑人设时来回切模型不该每次都问。
7902
+ * TTL 5 分钟:档位是模型能力声明,短时间内不会变;换模型时才重新问。
7903
+ */
7904
+ var EFFORT_CACHE_TTL_MS = 5 * 60 * 1000
7905
+ var effortCacheByRoute = {}
7906
+ function readEffortCache(key) {
7907
+ var hit = effortCacheByRoute[key]
7908
+ if (!hit) return null
7909
+ if (Date.now() - hit.at > EFFORT_CACHE_TTL_MS) { delete effortCacheByRoute[key]; return null }
7910
+ return hit.value
7911
+ }
7912
+ function writeEffortCache(key, value) { effortCacheByRoute[key] = { at: Date.now(), value: value } }
7595
7913
  function SubagentsPage() {
7596
7914
  var state = React.useState({ loading: true, error: null, subagents: [] })
7597
7915
  var data = state[0], setData = state[1]
@@ -7601,6 +7919,16 @@ function enabledMemoryCount(rules) {
7601
7919
  var result = rs[0], setResult = rs[1]
7602
7920
  var ms = React.useState(null)
7603
7921
  var modal = ms[0], setModal = ms[1]
7922
+ /**
7923
+ * 「思考强度」的加载状态。key = 已拉取/正在拉取的 `provider\0model`(空串 = 还没选模型)。
7924
+ * 与 `cands` 分开两个 state:档位是**按模型**懒加载的,而候选目录是一次性拉的,
7925
+ * 混在一起会让"切换模型"误触发整份候选重拉。
7926
+ */
7927
+ var es = React.useState({ key: '', loading: false, error: null, efforts: [], defaultEffort: null })
7928
+ var eff = es[0], setEff = es[1]
7929
+ /** 换模型导致已存档位被清掉时的一次性提示(下次换模型或关弹窗即消失)。 */
7930
+ var ens = React.useState(null)
7931
+ var effNotice = ens[0], setEffNotice = ens[1]
7604
7932
  // 搜索词独立一处:`data` 在 refresh 时被整对象替换,寄在它里面会被刷新清空。
7605
7933
  // 过滤用防抖值,输入框仍绑原值(与提示词页同一写法)。
7606
7934
  var qs = React.useState('')
@@ -7694,6 +8022,76 @@ function enabledMemoryCount(rules) {
7694
8022
  setCand({ loaded: true, loading: false, error: errMsg(e), models: [], tools: [], presets: [] })
7695
8023
  })
7696
8024
  }
8025
+ /**
8026
+ * 拉取某个 (provider, model) 的思考强度档位。**只有真正展开了高级选项、且选了模型**才会
8027
+ * 被调到(三个调用点:展开高级选项、模型下拉变更、自定义模型输入失焦)。
8028
+ *
8029
+ * 两个语义要点(都来自官方源码,见 src/subagents/service.ts 的 `reasoningEffort` 注释):
8030
+ * · 档位跟模型走 —— 换模型必须重拉,旧清单对新模型无效;
8031
+ * · **拉取失败一律不动已存的值**:只有"成功且当前值不在清单里"才清档。否则一次网络
8032
+ * 抖动就把用户设置抹了,而失败本身已经有提示。
8033
+ */
8034
+ function loadEfforts(provider, model) {
8035
+ setEffNotice(null)
8036
+ var pv = String(provider || ''), md = String(model || '')
8037
+ if (!pv || !md) { setEff({ key: '', loading: false, error: null, efforts: [], defaultEffort: null }); return }
8038
+ var key = pv + '\u0000' + md
8039
+ var cached = readEffortCache(key)
8040
+ if (cached) {
8041
+ setEff({ key: key, loading: false, error: cached.ok ? null : cached.error, efforts: cached.efforts || [], defaultEffort: cached.defaultEffort || null })
8042
+ if (cached.ok) dropEffortIfGone(cached.efforts || [])
8043
+ return
8044
+ }
8045
+ setEff({ key: key, loading: true, error: null, efforts: [], defaultEffort: null })
8046
+ apiCall('model-reasoning', { provider: pv, model: md }).then(function (res) {
8047
+ var value = res && res.ok !== false
8048
+ ? { ok: true, efforts: res.efforts || [], defaultEffort: res.defaultEffort || null }
8049
+ : { ok: false, error: (res && res.error) || t('subagents.effort.unavailable'), efforts: [], defaultEffort: null }
8050
+ writeEffortCache(key, value)
8051
+ setEff({ key: key, loading: false, error: value.ok ? null : value.error, efforts: value.efforts, defaultEffort: value.defaultEffort })
8052
+ if (value.ok) dropEffortIfGone(value.efforts)
8053
+ }).catch(function (e) {
8054
+ var value = { ok: false, error: errMsg(e), efforts: [], defaultEffort: null }
8055
+ writeEffortCache(key, value)
8056
+ setEff({ key: key, loading: false, error: value.error, efforts: [], defaultEffort: null })
8057
+ })
8058
+ }
8059
+ /**
8060
+ * 自动清档:清单里没有当前存的那一档 → 清掉并给一次性提示。
8061
+ *
8062
+ * 为什么必须清:官方对**不支持**的显式档位是在 provider I/O 之前直接拒(不夹紧、不别名),
8063
+ * 留着它 = 下一次委派直接起不来,而错误要到那时才看得见。
8064
+ * 为什么只在"拉取成功"时判断:失败时清单是空的,拿空清单当"没有这一档"会把设置误清。
8065
+ */
8066
+ function dropEffortIfGone(list) {
8067
+ if (!modal || modal.type !== 'editor') return
8068
+ var cur = String(modal.form.reasoningEffort || '')
8069
+ if (!cur) return
8070
+ var has = (list || []).some(function (e) { return String(e && e.id) === cur })
8071
+ if (has) return
8072
+ setEffNotice(t('subagents.effort.dropped', { effort: cur }))
8073
+ setForm({ reasoningEffort: '' })
8074
+ }
8075
+ /** 当前表单的 (provider, model) 路由键 —— 用来与 `eff.key` 比,确认清单属于当前模型。 */
8076
+ function currentRouteKey() {
8077
+ return String((modal && modal.form.provider) || '') + '\u0000' + String((modal && modal.form.model) || '')
8078
+ }
8079
+ /**
8080
+ * 当前模型**已就绪**的档位。未就绪一律返回空数组:切换模型后 `eff` 里还留着上一个模型的
8081
+ * 清单(新请求还没回来),照它渲染会让用户选中一个不属于这个模型的档位 —— 而官方对不支持的
8082
+ * 档位是直接拒,等于埋一次"委派起不来"。
8083
+ */
8084
+ function currentEfforts() {
8085
+ if (!modal || !modal.form.model || eff.loading || eff.error) return []
8086
+ return eff.key === currentRouteKey() ? (eff.efforts || []) : []
8087
+ }
8088
+ /** 空档(不指定)那一行的悬停说明:把 adapter 给的默认档位名字显示出来(拿不到就不显示)。 */
8089
+ function defaultEffortTitle() {
8090
+ var id = eff.defaultEffort
8091
+ if (!id || eff.key !== currentRouteKey()) return undefined
8092
+ var hit = (eff.efforts || []).filter(function (e) { return String(e && e.id) === String(id) })[0]
8093
+ return hit ? String(hit.name || hit.id) : String(id)
8094
+ }
7697
8095
  /** 高级选项:默认收起;已经在用模型/工具限制的人设自动展开(否则用户看不见自己配了什么)。 */
7698
8096
  /** 「不限制嵌套」的取值(与 src/subagents/service.ts 的 UNLIMITED_PERSONA_CATALOG_DEPTH 同值)。 */
7699
8097
  var CATALOG_DEPTH_UNLIMITED = 99
@@ -7709,7 +8107,9 @@ function enabledMemoryCount(rules) {
7709
8107
  // 目录注入深度不是默认值(1)时也算"配过":它决定常驻目录出现在哪些会话,
7710
8108
  // 藏起来会让"为什么子会话看不到目录"变得无从查起。
7711
8109
  var budget = !!(p && typeof p.catalogDepth === 'number' && p.catalogDepth !== 1)
7712
- return !!(budget || (p && (p.model || p.provider)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
8110
+ // 思考强度同理:配过就得让人看得见 —— 它是"下一次委派会怎么跑"的一部分,
8111
+ // 藏在收起的高级选项里等于用户改过又忘了自己改过。
8112
+ return !!(budget || (p && (p.model || p.provider || p.reasoningEffort)) || (p && ((p.tools || []).length || (p.toolsDeny || []).length || modes)))
7713
8113
  }
7714
8114
  /** 编辑态里按模式分组的名单(深拷贝:取消编辑不留痕)。 */
7715
8115
  function cloneRules(source) {
@@ -7722,7 +8122,7 @@ function enabledMemoryCount(rules) {
7722
8122
  }
7723
8123
  function openEditor(name) {
7724
8124
  if (!name) {
7725
- setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', catalogDepth: 1, tools: [], toolsDeny: [], toolsByPreset: {}, body: '', output: '', error: null } })
8125
+ setModal({ type: 'editor', mode: 'create', advanced: false, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: { name: '', description: '', provider: '', model: '', reasoningEffort: '', catalogDepth: 1, tools: [], toolsDeny: [], toolsByPreset: {}, body: '', output: '', error: null } })
7726
8126
  return
7727
8127
  }
7728
8128
  setBusy(true)
@@ -7733,6 +8133,8 @@ function enabledMemoryCount(rules) {
7733
8133
  var advanced = initialAdvanced(p)
7734
8134
  setModal({ type: 'editor', mode: 'edit', originalName: String(p.name || name), advanced: advanced, openMode: null, stoppedRules: {}, modeQuery: '', legacyTarget: '', form: {
7735
8135
  name: p.name || name, description: p.description || '', provider: p.provider || '', model: p.model || '',
8136
+ // 思考强度:服务端回空串 = 这份人设没指定(不是"没有档位")。空值不落盘,见 serializePersona。
8137
+ reasoningEffort: p.reasoningEffort || '',
7736
8138
  // 服务端回的是**生效值**(没写就是默认 1),所以这里不必再兜默认。
7737
8139
  catalogDepth: typeof p.catalogDepth === 'number' ? p.catalogDepth : 1,
7738
8140
  tools: (p.tools || []).slice(), toolsDeny: (p.toolsDeny || []).slice(),
@@ -7740,7 +8142,8 @@ function enabledMemoryCount(rules) {
7740
8142
  } })
7741
8143
  // 已配过限制的人设**一打开就是展开的**(initialAdvanced)→ 候选数据必须在这里也拉,
7742
8144
  // 否则四行模式先亮"宿主没有回传预设名单",非得点两次「高级选项」才补上(用户实测)。
7743
- if (advanced) loadCandidates()
8145
+ // 思考强度同理:展开着就要有档位清单(它按模型懒加载,所以这里显式带上 model)。
8146
+ if (advanced) { loadCandidates(); loadEfforts(p.provider, p.model) }
7744
8147
  } else setResult({ ok: false, text: translateError(t, res) })
7745
8148
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
7746
8149
  }
@@ -7979,7 +8382,12 @@ function enabledMemoryCount(rules) {
7979
8382
  if (!modal || modal.type !== 'editor') return
7980
8383
  var next = !modal.advanced
7981
8384
  setModal(Object.assign({}, modal, { advanced: next }))
7982
- if (next) loadCandidates()
8385
+ if (next) {
8386
+ loadCandidates()
8387
+ // 档位清单与候选目录**分开拉**:候选是本地目录(一次性),档位要问 adapter
8388
+ // (按模型、可能联网),所以这里用表单里当前的模型去拉;没选模型就什么都不发。
8389
+ loadEfforts(modal.form.provider, modal.form.model)
8390
+ }
7983
8391
  }
7984
8392
  function submitEditor() {
7985
8393
  if (!modal || modal.type !== 'editor') return
@@ -8128,43 +8536,89 @@ function enabledMemoryCount(rules) {
8128
8536
  React.createElement('div', { className: 'dsm-adv' },
8129
8537
  React.createElement('button', { type: 'button', className: 'dsm-adv-head', 'aria-expanded': modal.advanced === true, onClick: toggleAdvanced },
8130
8538
  React.createElement('span', { className: 'dsm-adv-caret' }, modal.advanced ? '▼' : '▶'),
8131
- React.createElement('span', null, t('subagents.adv.title')),
8132
- React.createElement('span', { className: 'dsm-adv-note' },
8133
- modal.advanced ? '' : t('subagents.adv.summary', {
8539
+ React.createElement('span', { className: 'dsm-adv-title' }, t('subagents.adv.title')),
8540
+ modal.advanced ? null : React.createElement('span', { className: 'dsm-adv-note dsm-adv-sum' },
8541
+ // 摘要固定两行:六项挤在一行会整句折行,把「高级选项」顶得七零八落。
8542
+ // 第一行是"下一次委派会怎么跑",第二行是工具名单的存量。
8543
+ React.createElement('span', null, t('subagents.adv.summary', {
8134
8544
  model: modal.form.model ? (modal.form.provider ? modal.form.provider + '/' + modal.form.model : modal.form.model) : t('subagents.adv.inherit'),
8135
8545
  depth: catalogDepthLabel(typeof modal.form.catalogDepth === 'number' ? modal.form.catalogDepth : 1),
8546
+ // 思考强度收起来也要看得见:它是"下一次委派会怎么跑"的一部分。
8547
+ // 没配时报「默认」而**不是**「继承」—— 官方语义里换模型会把继承来的那一档
8548
+ // 删掉(`dsh-subagent/lib/index.js:482`),报「继承」是句假话。
8549
+ effort: modal.form.reasoningEffort ? String(modal.form.reasoningEffort) : t('subagents.effort.none'),
8550
+ })),
8551
+ React.createElement('span', null, t('subagents.adv.summary2', {
8136
8552
  modes: Object.keys(modal.form.toolsByPreset || {}).length,
8137
8553
  allow: (modal.form.tools || []).length,
8138
8554
  deny: (modal.form.toolsDeny || []).length,
8139
- }))),
8555
+ })))),
8140
8556
  modal.advanced ? React.createElement('div', { className: 'dsm-adv-body' },
8141
- // 模型:宿主 LLM 目录里的 (provider, model) 对 + 自定义兜底。
8142
- React.createElement('div', { className: 'dsm-field' },
8143
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
8144
- React.createElement('div', { className: 'dsm-combo-row' },
8557
+ // 模型 + 思考强度并排(`.dsm-field-row`):它们是同一件事的两半 —— 档位清单是
8558
+ // **按模型**给的(adapter 的能力声明),换模型必须同时看这一格。
8559
+ // 不复用 `.dsm-combo-row`:那个的语义是"一个控件 + 它的自定义输入",
8560
+ // 它的 `.dsm-control{flex:1}` 会把两个控件都拉满。
8561
+ React.createElement('div', { className: 'dsm-field-row' },
8562
+ // 模型:宿主 LLM 目录里的 (provider, model) 对 + 自定义兜底。
8563
+ React.createElement('div', { className: 'dsm-field' },
8564
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.model')),
8565
+ React.createElement('div', { className: 'dsm-combo-row' },
8566
+ React.createElement('div', { className: 'dsm-select' },
8567
+ React.createElement('select', {
8568
+ className: 'dsm-control',
8569
+ value: modelSelectValue(),
8570
+ disabled: busy || cand.loading,
8571
+ onChange: function (e) {
8572
+ var v = e.target.value
8573
+ if (v === '') { setForm({ provider: '', model: '' }); loadEfforts('', '') }
8574
+ else if (v !== '__custom__') {
8575
+ var parts = v.split('\u0000')
8576
+ setForm({ provider: parts[0], model: parts[1] })
8577
+ // 档位跟模型走:换模型必须重拉(缓存命中时是同步的,不会闪)。
8578
+ loadEfforts(parts[0], parts[1])
8579
+ } else { setForm({ model: modal.form.model || '' }); loadEfforts(modal.form.provider || '', modal.form.model || '') }
8580
+ },
8581
+ },
8582
+ React.createElement('option', { value: '' }, t('subagents.model.inherit')),
8583
+ (cand.models || []).map(function (m) {
8584
+ return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, m.provider + ' · ' + m.name)
8585
+ }),
8586
+ React.createElement('option', { value: '__custom__' }, t('subagents.model.customOption')))),
8587
+ modelSelectValue() === '__custom__'
8588
+ ? React.createElement('input', {
8589
+ className: 'dsm-control',
8590
+ value: modal.form.model || '',
8591
+ placeholder: t('subagents.field.model.placeholder'),
8592
+ onChange: function (e) { setForm({ model: e.target.value }) },
8593
+ // 手填模型 id 时**不按键触发**拉取(一次一个字符 = 一串请求),
8594
+ // 失焦时再问一次;没变的话缓存会直接命中。
8595
+ onBlur: function () { loadEfforts(modal.form.provider || '', modal.form.model || '') },
8596
+ })
8597
+ : null),
8598
+ React.createElement('p', { className: 'dsm-help' }, cand.loading ? t('memory.loading') : t('subagents.field.model.hint'))),
8599
+ // 思考强度:值就是 adapter 给的档位 id(不校验合法性,官方在 provider I/O 前会拒)。
8600
+ React.createElement('div', { className: 'dsm-field' },
8601
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.effort')),
8145
8602
  React.createElement('div', { className: 'dsm-select' },
8146
8603
  React.createElement('select', {
8147
8604
  className: 'dsm-control',
8148
- value: modelSelectValue(),
8149
- disabled: busy || cand.loading,
8150
- onChange: function (e) {
8151
- var v = e.target.value
8152
- if (v === '') setForm({ provider: '', model: '' })
8153
- else if (v !== '__custom__') {
8154
- var parts = v.split('\u0000')
8155
- setForm({ provider: parts[0], model: parts[1] })
8156
- } else setForm({ model: modal.form.model || '' })
8157
- },
8605
+ value: String(modal.form.reasoningEffort || ''),
8606
+ disabled: busy || eff.loading || !modal.form.model,
8607
+ onChange: function (e) { setEffNotice(null); setForm({ reasoningEffort: e.target.value }) },
8158
8608
  },
8159
- React.createElement('option', { value: '' }, t('subagents.model.inherit')),
8160
- (cand.models || []).map(function (m) {
8161
- return React.createElement('option', { key: m.provider + '/' + m.id, value: m.provider + '\u0000' + m.id }, m.provider + ' · ' + m.name)
8162
- }),
8163
- React.createElement('option', { value: '__custom__' }, t('subagents.model.customOption')))),
8164
- modelSelectValue() === '__custom__'
8165
- ? React.createElement('input', { className: 'dsm-control', value: modal.form.model || '', placeholder: t('subagents.field.model.placeholder'), onChange: function (e) { setForm({ model: e.target.value }) } })
8166
- : null),
8167
- React.createElement('p', { className: 'dsm-help' }, cand.loading ? t('memory.loading') : t('subagents.field.model.hint'))),
8609
+ React.createElement('option', { value: '', title: defaultEffortTitle() }, t('subagents.effort.default')),
8610
+ currentEfforts().map(function (x) {
8611
+ return React.createElement('option', { key: x.id, value: x.id, title: x.description || undefined }, x.name)
8612
+ }))),
8613
+ React.createElement('p', { className: 'dsm-help' },
8614
+ !modal.form.model
8615
+ ? t('subagents.effort.needModel')
8616
+ : eff.loading
8617
+ ? t('subagents.effort.loading')
8618
+ : eff.error
8619
+ ? String(eff.error)
8620
+ : currentEfforts().length ? t('subagents.effort.hint') : t('subagents.effort.unavailable')),
8621
+ effNotice ? React.createElement('p', { className: 'dsm-feedback dsm-warning' }, effNotice) : null)),
8168
8622
  // provider 独立成一项:跨来源模型(如 sensenova)需要 provider+model 两个键同时给。
8169
8623
  React.createElement('label', { className: 'dsm-field' },
8170
8624
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.provider')),