dsh-plugin-tool-management 0.16.6 → 0.18.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 (96) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +20 -4
  3. package/README_EN.md +20 -4
  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 +59 -0
  21. package/lib/audit-log.js +6 -2
  22. package/lib/client.js +1615 -148
  23. package/lib/compat/capability-specs.js +321 -0
  24. package/lib/compat/host-probe.js +261 -0
  25. package/lib/compat/host-roots.js +168 -0
  26. package/lib/compat/probe-types.js +6 -0
  27. package/lib/compat/probe.js +54 -714
  28. package/lib/context-inject-contract.js +114 -0
  29. package/lib/context-inject-frame.js +147 -0
  30. package/lib/context-inject-select.js +127 -0
  31. package/lib/context-inject-surface.js +97 -0
  32. package/lib/context-inject.js +57 -459
  33. package/lib/host-names.js +150 -0
  34. package/lib/http-api.js +229 -0
  35. package/lib/hub.js +84 -11
  36. package/lib/imports/parsers.js +40 -15
  37. package/lib/index.js +235 -843
  38. package/lib/mcp/inspect-probe.js +199 -0
  39. package/lib/mcp/loader-token.js +26 -0
  40. package/lib/mcp/manager.js +267 -558
  41. package/lib/mcp/patch-yaml.js +207 -40
  42. package/lib/mcp/secret-guard.js +122 -16
  43. package/lib/mcp/tool-visibility.js +388 -0
  44. package/lib/memories/archive-engine.js +163 -16
  45. package/lib/memories/archive.js +10 -1
  46. package/lib/memories/discovery.js +261 -0
  47. package/lib/memories/export-plan.js +42 -0
  48. package/lib/memories/index-io.js +125 -12
  49. package/lib/memories/memories-types.js +1 -0
  50. package/lib/memories/service.js +167 -78
  51. package/lib/memories/snapshot.js +41 -260
  52. package/lib/op-registry.js +72 -8
  53. package/lib/ops/compat.js +17 -2
  54. package/lib/ops/prompts.js +18 -5
  55. package/lib/ops/quick-prompts.js +26 -0
  56. package/lib/ops/scene-records.js +157 -10
  57. package/lib/ops/scene-sync.js +71 -1
  58. package/lib/ops/sessions.js +63 -13
  59. package/lib/ops/snapshot.js +123 -24
  60. package/lib/ops/trash.js +45 -9
  61. package/lib/prompts/quick-service.js +211 -0
  62. package/lib/prompts/service.js +7 -2
  63. package/lib/request-gate.js +115 -56
  64. package/lib/scene-linkage.js +184 -0
  65. package/lib/scene-prompt-sync.js +46 -7
  66. package/lib/scenes/candidates.js +21 -3
  67. package/lib/sessions/archive-schemas.js +121 -0
  68. package/lib/sessions/path-key.js +46 -0
  69. package/lib/sessions/workspace.js +45 -168
  70. package/lib/settings-sidecars.js +143 -0
  71. package/lib/skills/core-types.js +8 -0
  72. package/lib/skills/core-util.js +81 -0
  73. package/lib/skills/core.js +97 -2297
  74. package/lib/skills/dir-watch.js +71 -0
  75. package/lib/skills/frontmatter.js +96 -0
  76. package/lib/skills/homes.js +14 -0
  77. package/lib/skills/import-pipeline.js +231 -0
  78. package/lib/skills/manager-state.js +800 -0
  79. package/lib/skills/official.js +269 -0
  80. package/lib/skills/provider-registration.js +89 -0
  81. package/lib/skills/service.js +4 -234
  82. package/lib/skills/skill-roots.js +287 -0
  83. package/lib/skills/skill-trash.js +265 -0
  84. package/lib/skills/system-trash.js +97 -0
  85. package/lib/skills/upload-pipeline.js +374 -0
  86. package/lib/slash-settings.js +25 -0
  87. package/lib/subagents/persona-parse.js +312 -0
  88. package/lib/subagents/persona-types.js +5 -0
  89. package/lib/subagents/persona-write.js +93 -0
  90. package/lib/subagents/run-provider.js +159 -0
  91. package/lib/subagents/service.js +20 -639
  92. package/lib/subagents/tool-filter.js +98 -0
  93. package/lib/token-config.js +240 -0
  94. package/lib/tool-table-switch.js +163 -0
  95. package/lib/tools/scene.js +40 -9
  96. package/package.json +3 -2
package/lib/client.js CHANGED
@@ -119,6 +119,12 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
119
119
  改成纵向两行:**名字独占一行、仍单行省略**(全名靠 title),描述另起一行、单行省略。
120
120
  只作用于本页:.dsm-source-head-main 还被技能 / 记忆 / MCP / 会话四页的分组头共用,
121
121
  那些行没有描述,不动。同权重靠书写顺序取胜,所以必须排在上面那条 .dsm-source 规则之后。 */
122
+ /* 提示词页的两张分组卡片:条目本身仍是 .dsm-source 卡,直接套进组卡的卡片体里就成了
123
+ "卡中卡"(一层边框套一层边框)。这里把它们压平成带分隔线的行 —— 与技能 / MCP 那两页
124
+ 的卡片体同一个观感(用户 2026-10-01 裁定:做成技能那样可以折叠的样式)。 */
125
+ .dsm-source-body .dsm-sources{gap:0}
126
+ .dsm-source-body .dsm-source{border:0;border-radius:0;border-bottom:1px solid var(--dsw-alias-border-l1)}
127
+ .dsm-source-body .dsm-source:last-child{border-bottom:0}
122
128
  .dsm-persona-main{box-sizing:border-box;min-height:48px;padding:6px 0;flex-direction:column;align-items:flex-start;justify-content:center;gap:0}
123
129
  .dsm-persona-name-row{display:flex;width:100%;min-width:0;align-items:center;gap:7px}
124
130
  .dsm-persona-name-row .dsm-tag{flex:none}
@@ -211,6 +217,12 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
211
217
  .dsm-seg-body{display:flex;height:216px;padding:6px;overflow:auto;flex-direction:column}
212
218
  /* 滚动容器里的子项不许被压缩:否则内容一多,筛选框会被压到比 input 还矮、与下面的条目叠在一起。 */
213
219
  .dsm-seg-body>*{flex:none}
220
+ /* 条目本来就少的那一段(场景表单里的快捷提示词):跟着内容长,超过 6 行才内滚。
221
+ 固定 216px 的段体放在一张按内容高的表单弹窗里,就是"一个勾 + 一片空白"(用户实测)。 */
222
+ .dsm-seg-fit{height:auto;max-height:172px}
223
+ /* 表单分段标题:把一长串字段切成「基本信息 / 场景绑定」这类模块,右侧一道发丝线跟着撑满。 */
224
+ .dsm-form-section{display:flex;align-items:center;gap:10px;margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:620;letter-spacing:.02em}
225
+ .dsm-form-section::after{content:"";height:1px;flex:1;background:var(--dsw-alias-border-l1)}
214
226
  .dsm-seg-foot{display:flex;align-items:center;gap:8px;padding:7px 12px;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
215
227
  .dsm-pick{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px}.dsm-mcp-edit-tools-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsm-mcp-edit-tools-actions{display:flex;align-items:center;gap:8px}.dsm-seg-empty-actions{display:flex;justify-content:center;padding:4px 0 12px}.dsm-seg-empty-error{padding:0 10px 10px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;text-align:center}
216
228
  .dsm-pick:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -447,6 +459,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
447
459
  /* 动作可用性表:表头列宽与 dsm-compat-mod-row 一致;窄容器下隐藏表头。 */
448
460
  .dsm-compat-table-head{display:grid;grid-template-columns:minmax(0,1fr) max-content;gap:10px;padding:6px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}
449
461
  .dsm-compat-cards .dsm-compat-table-head{display:none}
462
+ /* 新增提示词弹窗定高(用户 2026-09-30):切「全局 / 快捷」时「从现有预设复制」那一行会出现/
463
+ 消失,让整颗框跟着长缩。高的一屏为准,多出来的高度给正文文本框吃掉;窗口太矮时弹窗自己滚。 */
464
+ .dsm-modal-fixed{height:min(760px,calc(100vh - 48px));max-height:none}
465
+ .dsm-modal-fixed .dsm-field-grow{min-height:0;flex:1}
466
+ .dsm-modal-fixed .dsm-field-grow textarea{flex:1}
450
467
  /* 导出弹窗(D14):宽高都固定;只有「条目列表」这一层滚动,动作条常驻底部。 */
451
468
  .dsm-export-modal .dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:14px;overflow:hidden}
452
469
  .dsm-export-modal .dsm-export-form{display:flex;min-height:0;flex:1;flex-direction:column;gap:10px}
@@ -497,6 +514,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
497
514
  .dsm-token-banner-close{pointer-events:auto;flex:none;width:20px;height:20px;padding:0;border:0;border-radius:5px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;opacity:.7}
498
515
  .dsm-token-banner-close:hover{opacity:1}
499
516
 
517
+ /* 全局轻提示(斜杠命令的执行反馈)。位置在右下角:对话页的输入框居底居中,顶部那条又是
518
+ 令牌横幅,两处都会被挡。z-index 比横幅低一档 —— 令牌没过时那句解释比"改成功了"更要紧。 */
519
+ .dsm-toast{position:fixed;right:18px;bottom:18px;z-index:998;max-width:min(420px,80vw);padding:9px 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-text-primary);font-size:12.5px;line-height:1.55;box-shadow:0 6px 22px rgba(0,0,0,.16);pointer-events:none;backdrop-filter:blur(6px)}
520
+ .dsm-toast-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
521
+ .dsm-toast-warn{border-color:var(--dsm-warn);color:var(--dsm-warn)}
522
+ .dsm-toast-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
523
+
500
524
  /* MCP JSON 导入预览(0.15.0 A1):勾选 | 主体 | 状态三列。非法行只把「问题」那一行标红,
501
525
  名字净化 / 字段忽略这类是配置事实,走中性灰 —— 黄色在这里留给不了任何东西(琥珀语义)。 */
502
526
  .dsm-imp-head,.dsm-imp-row{display:grid;grid-template-columns:15px minmax(0,1fr) auto;align-items:start;gap:10px}
@@ -547,9 +571,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
547
571
  .dsm-snap{display:flex;flex-direction:column;gap:8px}
548
572
  .dsm-snap-row{display:flex;gap:8px}
549
573
  .dsm-snap-row .dsm-control{flex:1;min-width:0}
550
- .dsm-snap-picks{display:flex;flex-wrap:wrap;gap:6px 14px}
551
- .dsm-snap-pick{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
552
- .dsm-snap-pick input{width:14px;height:14px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
574
+ .dsm-snap-picks,.dsm-kind-picks{display:flex;flex-wrap:wrap;gap:6px 14px}
575
+ .dsm-snap-pick,.dsm-kind-pick{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
576
+ .dsm-snap-pick input,.dsm-kind-pick input{width:14px;height:14px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
553
577
  .dsm-snap-preview{display:flex;flex-direction:column;gap:6px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
554
578
  /* 兼容页三块弹窗:**开合不改尺寸**(列表长短、预览有没有读回来都不该让框跳)。
555
579
  「最近改动」(唯一 fixed 弹窗)的滚动层**只给列表**:正文是纯 flex 容器不滚动,
@@ -944,6 +968,60 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
944
968
  return pending
945
969
  }
946
970
 
971
+ /**
972
+ * 斜杠命令两段的挂载实况(由 48-slash.js 在注册时写入,兼容页那两枚标签读它)。
973
+ * **按段记**:两段各自是一个源,各自挂没挂上可以不同(只注册了一个、或第二个源重名)。
974
+ *
975
+ * 为什么声明放在这里而不是 48 片:消费它的 CompatPage 在 20 片,比 48 片**早** —— 同一个
976
+ * factory 作用域里函数声明提升没问题,但 `checkJs` 对这条跨片的反向引用会报 TS2304
977
+ * (`npm run typecheck:client` 实测)。共享状态放在两个使用方的**共同上游**这一档。
978
+ *
979
+ * 服务端**不知道**挂没挂上(那是浏览器那半的事),所以这一句只能由客户端说;feature-overview
980
+ * 那一行报的是开关状态,两者不是一回事,别拿一个冒充另一个(见 ops/compat.ts 同一处注释)。
981
+ */
982
+ let slashMountStates = { tools: 'pending', quickPrompts: 'pending' }
983
+ function slashMountStateOf(key) { return slashMountStates[key] || 'pending' }
984
+ /** 传 key 只改那一段;不传是"两段一起"(都关掉了、或整批注册失败)。 */
985
+ function setSlashMount(state, key) {
986
+ if (key) slashMountStates[key] = state
987
+ else slashMountStates = { tools: state, quickPrompts: state }
988
+ }
989
+
990
+ /**
991
+ * 一条全局轻提示(body 级,与页面/弹窗无关)。
992
+ *
993
+ * 为什么不是 `Notice`:`Notice` 长在页签里,而斜杠命令是在**对话页**按下的 —— 那时设置页
994
+ * 根本没挂载,面板内的提示条无处可渲染。它与「令牌没填」那条横幅同源同位(40-apply-head.js
995
+ * 的 body 级 append),差别只是这条会自动收。
996
+ *
997
+ * 同时只留一条:后发的直接覆写前一条(连点两下开关不该叠出两层提示)。
998
+ */
999
+ let toastEl = null
1000
+ let toastText = null
1001
+ let toastTimer = null
1002
+ function showPluginToast(text, kind) {
1003
+ try {
1004
+ if (typeof document === 'undefined' || !document.body) return
1005
+ const tone = kind === 'err' ? 'err' : kind === 'warn' ? 'warn' : 'ok'
1006
+ if (!toastEl) {
1007
+ toastEl = document.createElement('div')
1008
+ toastEl.setAttribute('role', 'status')
1009
+ toastText = document.createElement('span')
1010
+ toastEl.appendChild(toastText)
1011
+ document.body.appendChild(toastEl)
1012
+ }
1013
+ toastText.textContent = String(text == null ? '' : text)
1014
+ toastEl.className = 'dsm-toast dsm-toast-' + tone
1015
+ toastEl.style.display = ''
1016
+ if (toastTimer) clearTimeout(toastTimer)
1017
+ // 报错那条留久一点:它常常是"得去设置页填令牌"这类要照着做的话。
1018
+ toastTimer = setTimeout(function () {
1019
+ if (toastEl) toastEl.style.display = 'none'
1020
+ toastTimer = null
1021
+ }, tone === 'err' ? 9000 : 4000)
1022
+ } catch (e) { /* DOM 不可用 → 只是少一条反馈,不影响已经生效的改动 */ }
1023
+ }
1024
+
947
1025
  // Small version badge shown next to each settings-page title; reads the
948
1026
  // package version from the host (plugin-version op) so it always matches
949
1027
  // the installed release.
@@ -1046,6 +1124,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1046
1124
  'agentsmd-apply': 'compat.audit.op.presetApply',
1047
1125
  'agentsmd-trash-restore': 'compat.audit.op.trashRestore',
1048
1126
  'agentsmd-trash-delete': 'compat.audit.op.trashDelete',
1127
+ // 快捷提示词:与预设分开两套文案(「保存提示词预设」在流水里等于"改了 AGENTS.md 的来源",
1128
+ // 而快捷词一个字节都不碰宿主状态,混着记会让流水读起来不像事实)。
1129
+ 'quickprompt-create': 'compat.audit.op.quickSave',
1130
+ 'quickprompt-update': 'compat.audit.op.quickSave',
1131
+ 'quickprompt-toggle': 'compat.audit.op.quickSwitch',
1132
+ 'quickprompt-remove': 'compat.audit.op.quickDelete',
1133
+ 'quickprompt-trash-restore': 'compat.audit.op.trashRestore',
1134
+ 'quickprompt-trash-delete': 'compat.audit.op.trashDelete',
1049
1135
  'tool-table': 'compat.audit.op.toolTable',
1050
1136
  'inject-settings': 'compat.audit.op.injectSettings',
1051
1137
  'scene-settings': 'compat.audit.op.sceneSettings',
@@ -1090,6 +1176,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1090
1176
  // 注入设置(本插件各注入域的开关,清单见 context-inject 的 INJECT_DOMAIN_KEYS)。它和可达性矩阵是一体两面:
1091
1177
  // 矩阵说"到不到得了",这里决定"要不要"。读不到时这一节不显示。
1092
1178
  const [inject, setInject] = React.useState(null)
1179
+ // 斜杠命令入口(`/` 菜单里那个「工具」段)的开关。与 inject 同一类:读不到就不渲染这一节。
1180
+ const [slash, setSlash] = React.useState(null)
1093
1181
  // 模型工具表(哪些工具整份不发给模型;见 src/tools/table.ts)。与「注入」是两种省法:
1094
1182
  // 注入关掉省的是正文,这里关掉省的是工具 schema(工具表按每个请求付钱)。
1095
1183
  const [toolTable, setToolTable] = React.useState(null)
@@ -1450,6 +1538,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1450
1538
  .then(function (r) { if (alive !== false && r && r.ok && r.settings) setInject(r.settings) })
1451
1539
  .catch(function () { /* 读不到 → 不渲染这一节 */ })
1452
1540
  }
1541
+ const loadSlash = function (alive) {
1542
+ apiCall('slash-settings', {})
1543
+ .then(function (r) { if (alive !== false && r && r.ok && r.settings) setSlash(r.settings) })
1544
+ .catch(function () { /* 读不到 → 不渲染这一节 */ })
1545
+ }
1453
1546
  // 工具表实况(注册时量到的体积 + 关掉了哪些)。服务端返回的 `report` 自带分组与
1454
1547
  // ≈token 合计 —— 界面不自己算,也不缓存第二份数字。
1455
1548
  const applyToolTable = function (r, alive) {
@@ -1587,6 +1680,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1587
1680
  .catch(function (e) { if (alive) { setError(errMsg(e)); setBusy(false) } })
1588
1681
  loadReach(alive)
1589
1682
  loadInject(alive)
1683
+ loadSlash(alive)
1590
1684
  loadToolTable(alive)
1591
1685
  loadLive(alive)
1592
1686
  loadFeatures(alive)
@@ -1601,6 +1695,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1601
1695
  .catch(function (e) { setError(errMsg(e)); setBusy(false) })
1602
1696
  loadReach(true)
1603
1697
  loadInject(true)
1698
+ loadSlash(true)
1604
1699
  loadToolTable(true)
1605
1700
  loadLive(true)
1606
1701
  loadFeatures(true)
@@ -1620,6 +1715,21 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1620
1715
  })
1621
1716
  .catch(function () { loadInject(true); loadReach(true) })
1622
1717
  }
1718
+ /**
1719
+ * 保存斜杠命令入口的两颗开关(「工具」段 / 「快捷提示词」段)。与 saveInject 同一条口径:
1720
+ * 先乐观更新(点一下就该有反馈),服务端返回值到达后以其为准,失败重拉。
1721
+ *
1722
+ * **本次启动不会立刻挂上 / 撤下** —— 源注册是启动期一次性的事,所以说明里写清"重启或
1723
+ * 重开页面后生效",别让"关掉了但菜单还在"变成一次报障。
1724
+ */
1725
+ const saveSlash = function (patch) {
1726
+ if (!slash) return
1727
+ const next = Object.assign({}, slash, patch)
1728
+ setSlash(next)
1729
+ apiCall('slash-settings', Object.assign({ set: true }, next))
1730
+ .then(function (r) { if (r && r.ok && r.settings) setSlash(r.settings); loadFeatures(true) })
1731
+ .catch(function () { loadSlash(true) })
1732
+ }
1623
1733
 
1624
1734
  const findings = (data && data.findings) || []
1625
1735
  const degraded = (data && data.degraded) || []
@@ -1697,6 +1807,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1697
1807
  case 'scene-lock': return t('compat.ov.sceneLock')
1698
1808
  case 'mcp-tools': return t('compat.ov.mcpTools')
1699
1809
  case 'tool-table': return t('compat.ov.toolTable')
1810
+ case 'slash-commands': return t('compat.ov.slashCommands')
1700
1811
  case 'native-delete': return t('compat.ov.nativeDelete')
1701
1812
  case 'host-identity': return t('compat.ov.hostIdentity')
1702
1813
  case 'memory': return t('compat.ov.memory')
@@ -1714,6 +1825,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1714
1825
  switch (row.detailKey) {
1715
1826
  case 'skills-provider': return t('compat.note.skillsProvider', params)
1716
1827
  case 'context-injection': return t('compat.note.contextInjection', params)
1828
+ case 'context-injection-runtime.domain': return t('compat.note.contextInjectionRuntime.domain', params)
1829
+ case 'context-injection-runtime.throw': return t('compat.note.contextInjectionRuntime.throw')
1717
1830
  case 'cordis-original-symbol': return t('compat.note.cordisOriginalSymbol')
1718
1831
  case 'patch-write-guard.no-dep': return t('compat.note.patchWriteGuard.noDep', params)
1719
1832
  case 'patch-write-guard.stale': return t('compat.note.patchWriteGuard.stale', params)
@@ -1724,6 +1837,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1724
1837
  case 'mcp-tool-visibility.no-restrict': return t('compat.note.mcpToolVisibility.noRestrict')
1725
1838
  case 'mcp-tool-visibility.partial': return t('compat.note.mcpToolVisibility.partial', params)
1726
1839
  case 'workspace.delete-native': return t('compat.note.workspaceDeleteNative', params)
1840
+ case 'mcp-server-name-ambiguous.separator': return t('compat.note.mcpServerNameAmbiguous.separator', params)
1841
+ case 'mcp-server-name-ambiguous.prefix': return t('compat.note.mcpServerNameAmbiguous.prefix', params)
1727
1842
  }
1728
1843
  }
1729
1844
  if (row.key === 'mount' && row.state === 'ok') return t('compat.ov.mount.ok', params)
@@ -1770,6 +1885,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1770
1885
  : t('compat.ov.toolTable.mixed', { hidden: hidden, total: row.total, defaults: row.defaults })
1771
1886
  }
1772
1887
  if (row.key === 'native-delete' && row.state === 'ok') return t('compat.ov.nativeDelete.ok')
1888
+ if (row.key === 'slash-commands') {
1889
+ // 两段各自一颗开关:只开一段时不能沿用"两段都在"那句(服务端把两个布尔一起带回来了)。
1890
+ if (row.state === 'disabled') return t('compat.ov.slashCommands.off')
1891
+ if (row.slashTools && row.slashQuickPrompts) return t('compat.ov.slashCommands.on')
1892
+ return t(row.slashTools ? 'compat.ov.slashCommands.onTools' : 'compat.ov.slashCommands.onQuick')
1893
+ }
1773
1894
  if (row.key === 'host-identity') {
1774
1895
  if (row.state === 'unknown') return t('compat.ov.route.unknown')
1775
1896
  if (row.state === 'degraded') return t('compat.ov.hostIdentity.degraded', { list: list(row.blockers) })
@@ -2107,24 +2228,67 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2107
2228
  }, t('compat.token.edit.cancel'))]))))
2108
2229
  }
2109
2230
 
2110
- // 注入设置块:本插件注入给模型哪些内容的开关。总开关只管
2111
- // "极简这类预设下要不要破例",各域勾选在任何预设下都生效。
2231
+ // 注入设置块:本插件注入给模型哪些内容的开关 + 对话输入框的斜杠命令入口(`/` 菜单里的
2232
+ // 「工具」与「快捷提示词」两段)。总开关只管"极简这类预设下要不要破例",各域勾选在任何
2233
+ // 预设下都生效。
2112
2234
  //
2113
2235
  // 排版口径(用户裁定 2026-09-16):勾选**横排**(短标签一行放得下,不再一人一行竖着排),
2114
2236
  // 文案只说到"有这个内容"为止 —— 用户是普通使用者,不解释 persona/载体/路径这些内部机制。
2237
+ //
2238
+ // 斜杠命令入口并进这一块(用户 2026-09-30 裁定):它原先自己成一节,那一节的说明又长又和
2239
+ // 勾选框自己的标签重复。现在只剩一条分组标题 + 两段各自一颗勾选框(「工具」/「快捷提示词」,
2240
+ // 各自就是一个源,所以能各挂各的)+ 一句"重启才生效"。
2241
+ const slashParts = []
2242
+ if (slash) {
2243
+ // 行末那枚标签是"本次启动真挂上了没":开关值说的是"要不要",挂没挂上只有浏览器知道
2244
+ // (服务端看不到源注册的结果),两件事不能合成一句 —— 合了必有一句是假的。标签只说
2245
+ // 已生效 / 未生效,**为什么**没生效挂在悬停那句里(重名抢占 / 宿主没这个服务 / 还没就绪,
2246
+ // 三种的处置完全不同)—— 见 48-slash.js 的 setSlashMount。
2247
+ const liveTag = function (key) {
2248
+ const state = slashMountStateOf(key)
2249
+ const on = state === 'registered'
2250
+ return React.createElement('span', { className: 'dsm-pick-meta' },
2251
+ React.createElement('span', {
2252
+ className: 'dsm-tag' + (on ? ' dsm-tag-on' : ''), title: t('compat.slash.mount.' + state),
2253
+ }, t(on ? 'compat.slash.live.on' : 'compat.slash.live.off')))
2254
+ }
2255
+ slashParts.push(
2256
+ React.createElement('div', { key: 'slash-head', className: 'dsm-seg-group' },
2257
+ React.createElement('span', null, t('compat.slash.title')),
2258
+ React.createElement('span', { className: 'dsm-seg-group-line' })),
2259
+ React.createElement('label', { key: 'slash-tools', className: 'dsm-pick' },
2260
+ React.createElement('input', {
2261
+ type: 'checkbox', checked: slash.tools !== false,
2262
+ onChange: function (e) { saveSlash({ tools: e.target.checked === true }) },
2263
+ }),
2264
+ React.createElement('span', { className: 'dsm-pick-main' },
2265
+ React.createElement('span', { className: 'dsm-pick-name' }, t('compat.slash.tools')),
2266
+ React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.slash.tools.desc'))),
2267
+ liveTag('tools')),
2268
+ React.createElement('label', { key: 'slash-quick', className: 'dsm-pick' },
2269
+ React.createElement('input', {
2270
+ type: 'checkbox', checked: slash.quickPrompts !== false,
2271
+ onChange: function (e) { saveSlash({ quickPrompts: e.target.checked === true }) },
2272
+ }),
2273
+ React.createElement('span', { className: 'dsm-pick-main' },
2274
+ React.createElement('span', { className: 'dsm-pick-name' }, t('compat.slash.quick')),
2275
+ React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.slash.quick.desc'))),
2276
+ liveTag('quickPrompts')),
2277
+ React.createElement('p', { key: 'slash-restart', className: 'dsm-help' }, t('compat.slash.restartHint')))
2278
+ }
2279
+ const injectParts = []
2115
2280
  if (inject) {
2116
2281
  const domains = (inject && inject.domains) || {}
2117
- push(section(t('compat.inject'), t('compat.inject.hint'),
2118
- React.createElement('div', { className: 'dsm-inject-settings' },
2119
- React.createElement('label', { className: 'dsm-pick' },
2282
+ injectParts.push(
2283
+ React.createElement('label', { key: 'force', className: 'dsm-pick' },
2120
2284
  React.createElement('input', { type: 'checkbox', checked: inject.underSuppressingPresets === true, onChange: function () { saveInject({ underSuppressingPresets: inject.underSuppressingPresets !== true }) } }),
2121
2285
  React.createElement('span', { className: 'dsm-pick-main' },
2122
2286
  React.createElement('span', { className: 'dsm-pick-name' }, t('compat.inject.force')),
2123
2287
  React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.inject.force.desc')))),
2124
- React.createElement('div', { className: 'dsm-seg-group' },
2288
+ React.createElement('div', { key: 'domains-head', className: 'dsm-seg-group' },
2125
2289
  React.createElement('span', null, t('compat.inject.domains')),
2126
2290
  React.createElement('span', { className: 'dsm-seg-group-line' })),
2127
- React.createElement('div', { className: 'dsm-inject-domains' },
2291
+ React.createElement('div', { key: 'domains', className: 'dsm-inject-domains' },
2128
2292
  ['scene', 'memory', 'mcp', 'skills', 'subagents', 'prompt'].map(function (key) {
2129
2293
  // 技能与提示词两域在标准类预设下由宿主送、本插件让位 —— 关掉它们时本插件会连
2130
2294
  // 宿主那条一起拦下(见 context-inject.ts 的 officialKindsToSuppress)。这里给
@@ -2137,7 +2301,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2137
2301
  saveInject({ domains: nextDomains })
2138
2302
  } }),
2139
2303
  React.createElement('span', null, t('compat.inject.domain.' + key)))
2140
- })))))
2304
+ })))
2305
+ }
2306
+ // 两块数据各自独立加载:inject 读不到时斜杠入口照样要出现,反之也一样,所以整块的条件是
2307
+ // 「任一有值」—— 缺的那半只是不出现,不把另一半一起吞掉。
2308
+ if (inject || slash) {
2309
+ push(section(inject ? t('compat.inject') : t('compat.slash.title'), inject ? t('compat.inject.hint') : null,
2310
+ React.createElement('div', { className: 'dsm-inject-settings' }, injectParts.concat(slashParts))))
2141
2311
  }
2142
2312
  // 模型工具表:与「注入」并列的第二种省法 —— 那边省正文,这边省工具 schema。
2143
2313
  // 工具表**每个请求都发一遍**(哪怕这一轮用不上),所以关掉的工具是整份不进请求,
@@ -2242,6 +2412,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2242
2412
  if (row.state === 'cleared') return { cls: '', text: t('compat.live.state.cleared'), title: '' }
2243
2413
  // 子会话不适用:与「已关闭 / 无内容」同档的灰 —— 它是设计选择,不是故障。
2244
2414
  if (row.state === 'child') return { cls: '', text: t('compat.live.state.child'), title: t('compat.live.state.child.title') }
2415
+ // 取数抛异常:这是**故障**(该域内容还在,但这一步取不到),所以给琥珀 ——
2416
+ // 与「未投递」(压缩后待补发)不同档,混在一起会把故障读成延迟(2026-09-30 P1-8)。
2417
+ if (row.state === 'error') return { cls: ' dsm-compat-pill-warn', text: t('compat.live.state.error'), title: t('compat.live.state.error.title') }
2245
2418
  return { cls: '', text: t('compat.live.state.unknown'), title: '' }
2246
2419
  }
2247
2420
  // 采纳一行:注入 N 次 / 调用 M 次 / 其中"调用时正文就在眼前"K 次。
@@ -2770,9 +2943,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2770
2943
  snapIn.result ? React.createElement('div', { className: 'dsm-feedback' + (snapIn.result.ok ? '' : ' dsm-error'), role: snapIn.result.ok ? 'status' : 'alert' },
2771
2944
  React.createElement('div', null, snapIn.result.text),
2772
2945
  (snapIn.result.report || []).map(function (row, i) {
2773
- return React.createElement('div', { key: i, className: 'dsm-help' }, row.error
2946
+ var warn = (row.warnings || []).length ? ' ' + t('snapshot.import.reportWarning', { warnings: row.warnings.join(';') }) : ''
2947
+ return React.createElement('div', { key: i, className: 'dsm-help' + (warn ? ' dsm-warning' : '') }, (row.error
2774
2948
  ? t('snapshot.import.reportFailed', { domain: t('snapshot.domain.' + row.domain), error: row.error })
2775
- : t('snapshot.import.report', { domain: t('snapshot.domain.' + row.domain), imported: row.imported, skipped: row.skipped }))
2949
+ : t('snapshot.import.report', { domain: t('snapshot.domain.' + row.domain), imported: row.imported, skipped: row.skipped })) + warn)
2776
2950
  })) : null)
2777
2951
 
2778
2952
  // ── 清理旧备份(兼容页,紧挨「刷新」)──────────────────────────────────────
@@ -3178,7 +3352,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3178
3352
  "confirm.custom.remove.title": "移除自定义目录",
3179
3353
  "confirm.custom.remove.desc": "移除后该目录的技能不再列出(文件不受影响):{name}",
3180
3354
  "result.custom.added": "已添加自定义目录:{path}", "result.custom.removed": "已移除自定义目录:{name}",
3181
- "status.enabled": "已启用", "status.disabled": "未启用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.shadowed.hint": "同名技能「{name}」正在生效;点「启用这个」可改用当前来源的版本", "status.preferred": "同名首选", "status.readonly": "源文件只读", "status.notDeletable": "不可删除", "status.notDeletable.hint": "删除只对插件可管理的来源开放(DSH 技能 / 导入技能 / 项目级 .dsh/skills);外部 Agent 与自定义目录只读,里面的技能不能删", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
3355
+ "status.enabled": "已启用", "status.disabled": "未启用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.shadowed.hint": "同名技能「{name}」正在生效;点「启用这个」可改用当前来源的版本", "status.shadowed.inert": "同名技能「{name}」已停用,被它挡住的本来源版本同样不会生效 —— 两份都不生效;启用其中一份即可", "status.preferred": "同名首选", "status.readonly": "源文件只读", "status.notDeletable": "不可删除", "status.notDeletable.hint": "删除只对插件可管理的来源开放(DSH 技能 / 导入技能 / 项目级 .dsh/skills);外部 Agent 与自定义目录只读,里面的技能不能删", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
3182
3356
  "summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述",
3183
3357
  "filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
3184
3358
  "empty.search": "没有匹配的技能", "empty.source": "该来源不存在或暂无技能", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
@@ -3204,7 +3378,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3204
3378
  "error.rules.reservedScene": "「{name}」是保留场景,不可{action}{reason}",
3205
3379
  "error.rules.singleSceneOnly": "除「全局」外同时只能启用一个场景{detail}",
3206
3380
  "error.agentsMd.sceneRebindFailed": "无法把场景「{scene}」的提示词绑定改成「{target}」:{reason}",
3207
- "error.agentsMd.referenced": "「{id}」仍被引用,不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。",
3381
+ "error.agentsMd.referenced": "「{id}」仍被引用,不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "error.agentsMd.refProbeFailed": "无法确认「{id}」是否被引用,已拒绝删除:引用探测失败(场景绑定 / AGENTS.md 当前内容 / 进场景前的基线,这三类里至少有一类没读到)。请稍后重试。",
3208
3382
  "error.source.reserved": "{origin}:不能停用或移除(里面的技能可以删除)",
3209
3383
  "action.rename": "改名", "action.lock": "锁定",
3210
3384
  "trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
@@ -3322,14 +3496,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3322
3496
  "snapshot.export.dir": "导出目录", "snapshot.export.btn": "导出", "snapshot.export.busy": "导出中…", "snapshot.export.done": "快照已写到:{dir}", "snapshot.export.failed": "导出失败",
3323
3497
  "snapshot.export.secrets": "完整导出(含明文凭据)", "snapshot.export.secrets.warn": "会把密钥原文一起写进去。这份目录要拷去别的机器或网盘,想清楚再勾。",
3324
3498
  "snapshot.counts": "{items}", "snapshot.notes": "说明:{items}",
3325
- "snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "记忆", "snapshot.domain.skills": "技能", "snapshot.domain.subagents": "子智能体", "snapshot.domain.prompts": "提示词", "snapshot.domain.settings": "侧车配置", "snapshot.domain.scenes": "场景档案",
3499
+ "snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "记忆", "snapshot.domain.skills": "技能", "snapshot.domain.subagents": "子智能体", "snapshot.domain.prompts": "全局提示词", "snapshot.domain.quick-prompts": "快捷提示词", "snapshot.domain.settings": "侧车配置", "snapshot.domain.scenes": "场景档案",
3326
3500
  "snapshot.import.dir": "快照位置", "snapshot.import.dir.placeholder": "例如 D:\\backup\\dsh-tool-management-snapshot-…",
3327
3501
  "snapshot.import.preview": "导入", "snapshot.import.busy": "处理中…",
3328
3502
  "snapshot.import.row": "{domain} {count} 项 · 重名 {clashes}",
3329
3503
  "snapshot.import.conflict.skip": "跳过重名", "snapshot.import.conflict.overwrite": "覆盖重名",
3330
3504
  "snapshot.import.run": "执行导入", "snapshot.import.done": "导入完成", "snapshot.import.failed": "导入失败",
3331
3505
  "snapshot.import.none": "没有勾选任何域", "snapshot.import.notFound": "这个快照里没有可导入的部分",
3332
- "snapshot.import.report": "{domain}:成功 {imported} · 跳过 {skipped}", "snapshot.import.reportFailed": "{domain}:失败({error})",
3506
+ "snapshot.import.report": "{domain}:成功 {imported} · 跳过 {skipped}", "snapshot.import.reportFailed": "{domain}:失败({error})", "snapshot.import.reportWarning": "⚠ 需要注意:{warnings}",
3333
3507
  "snapshot.import.locked": "{names} 已锁定:先解锁再导入。",
3334
3508
  "snapshot.import.versionNote": "来自插件 {version}",
3335
3509
  "snapshot.import.masked": "密钥已打码",
@@ -3347,7 +3521,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3347
3521
  "compat.audit.op.ruleSave": "保存记忆", "compat.audit.op.ruleDelete": "删除记忆", "compat.audit.op.ruleSwitch": "启停记忆", "compat.audit.op.ruleImport": "导入记忆", "compat.audit.op.ruleAttach": "挂到场景 / 摘下", "compat.audit.op.ruleIndex": "重建记忆索引", "compat.audit.op.memorySwitch": "切换生效记忆集",
3348
3522
  "compat.audit.op.sceneSave": "保存场景档案", "compat.audit.op.sceneDelete": "删除场景", "compat.audit.op.sceneRebind": "改绑提示词", "compat.audit.op.sceneLock": "场景锁定开关", "compat.audit.op.sceneEnter": "进入 / 退出场景", "compat.audit.op.engineApply": "场景引擎应用档案", "compat.audit.op.engineRestore": "场景引擎还原档案",
3349
3523
  "compat.audit.op.personaSave": "保存人设", "compat.audit.op.personaDelete": "删除人设", "compat.audit.op.personaImport": "导入人设", "compat.audit.op.personaSwitch": "启停人设",
3350
- "compat.audit.op.presetSave": "保存提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
3524
+ "compat.audit.op.presetSave": "保存提示词", "compat.audit.op.quickSave": "保存快捷提示词", "compat.audit.op.quickSwitch": "启停快捷提示词", "compat.audit.op.quickDelete": "删除快捷提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
3351
3525
  "compat.audit.op.trashRestore": "从回收站恢复", "compat.audit.op.trashDelete": "永久删除",
3352
3526
  "compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.backupsClean": "清理旧备份",
3353
3527
  "compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
@@ -3405,9 +3579,68 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3405
3579
  "compat.ov.hostIdentity": "宿主身份与模块", "compat.ov.hostIdentity.ok": "模块与宿主同源(宿主 DSH {version})", "compat.ov.hostIdentity.asar": "官方包与宿主同版本(DSH {version}),但宿主跑在 {name} 里、插件在归档外", "compat.ov.hostIdentity.degraded": "存在阻塞项:{list}",
3406
3580
  "compat.ov.memory": "记忆", "compat.ov.prompts": "提示词", "compat.ov.subagents": "子智能体", "compat.ov.scenes": "场景档案",
3407
3581
  "compat.ov.plain.ok": "无降级上报(装配失败会出现在这里)",
3582
+ // ── 斜杠命令「工具」段(对话输入框里直接管六个域;实现见 48-slash.js)──────
3583
+ // 段标题与六个域名出现在宿主 `/` 菜单里;域名要短 —— 菜单行右侧还要留位置给摘要与 › 箭头。
3584
+ "slash.section.tools": "工具",
3585
+ "slash.domain.scene": "场景", "slash.domain.skill": "技能", "slash.domain.mcp": "MCP 服务器",
3586
+ "slash.domain.agent": "子智能体", "slash.domain.preset": "提示词", "slash.domain.memory": "记忆",
3587
+ "slash.sum.current": "当前:{name}", "slash.sum.count": "{n} 项",
3588
+ "slash.sum.onoff": "{on} 启用 / {off} 停用", "slash.sum.sceneGlobal": "场景「{scene}」+ 全局 · {n} 条",
3589
+ "slash.sum.quick": "快捷 {on}/{total}",
3590
+ "slash.group.noScene": "未归场景",
3591
+ "slash.state.on": "已启用", "slash.state.off": "未启用",
3592
+ "slash.skill.sourceRow": "目录",
3593
+ "slash.did.on": "已启用 {name}", "slash.did.off": "已停用 {name}",
3594
+ "slash.did.scene": "已进入场景「{name}」", "slash.did.sceneOff": "已退出场景「{name}」(只注入全局与公共基线)",
3595
+ "slash.did.preset": "已应用基线「{name}」",
3596
+ "slash.did.quickOn": "已启用快捷提示词「{name}」(在 / 的「快捷提示词」段里可见)",
3597
+ "slash.did.quickOff": "已停用快捷提示词「{name}」(不再出现在 / 的「快捷提示词」段,正文留着)",
3598
+ "slash.did.sourceOn": "已启用目录「{name}」,其中原本启用的技能随之生效",
3599
+ "slash.did.sourceOff": "已停用目录「{name}」(整个文件夹不再读取)",
3600
+ "slash.did.already": "当前就是「{name}」",
3601
+ "slash.err.load": "读不到{domain}列表:{error}", "slash.err.gone": "这一项已经不在了,请重新打开列表",
3602
+ "slash.err.failed": "改动没有生效", "slash.err.pick": "请用 ↑↓ 选中一行后按 Enter(这半截命令没有发出去)",
3603
+ // ── 快捷提示词(提示词页的第二段;对话框 `/` 菜单里另起一段)────────────────
3604
+ // 两类提示词的差别全在这几句里说清:一份会写进 AGENTS.md 常驻上下文,一份只是贴进
3605
+ // 输入框的文字。这句话必须出现在**选它的那一步**(弹窗顶部的类型行),不然用户要到
3606
+ // 点完"应用"才知道自己存的是一段会被原样注入的字。
3607
+ "prompts.kind.label": "提示词类型",
3608
+ "prompts.kind.global": "全局提示词", "prompts.kind.quick": "快捷提示词",
3609
+ "prompts.kind.global.hint": "应用后写入 AGENTS.md,下一轮对话生效;可被场景绑定。",
3610
+ "prompts.kind.quick.hint": "对话框使用:将提示词输入对话框。",
3611
+ "prompts.group.global": "全局提示词", "prompts.group.quick": "快捷提示词", "prompts.bulk.quick": "把这一组里当前可见的快捷提示词全部打开 / 全部关闭", "prompts.bulk.failed": "{done} 条已改,{failed} 条没改成",
3612
+ "quickprompt.stat": "条快捷提示词",
3613
+ "quickprompt.toggle": "在 / 里显示",
3614
+ "quickprompt.toggle.hint": "关掉后这条不再出现在对话框 / 的「快捷提示词」段里;正文与备注都留着,随时可以再打开(「工具 › 提示词」那一域里也列着,在那里同样拨得动)",
3615
+ "quickprompt.empty": "还没有快捷提示词。点「新增提示词」,类型选「快捷提示词」。",
3616
+ "quickprompt.edit.title": "编辑快捷提示词",
3617
+ "quickprompt.field.id.hint": "也是 / 菜单里那一行的名字,可以中文。",
3618
+ "quickprompt.field.desc.placeholder": "一句话记住它是干什么的(只给你自己看)",
3619
+ "quickprompt.field.content": "正文",
3620
+ "quickprompt.field.content.placeholder": "这段文字会在你点的时候原样贴进对话框",
3621
+ "quickprompt.remove.title": "删除快捷提示词",
3622
+ "quickprompt.remove.hint": "删除后进入回收站,可以从本页的「回收站」恢复。",
3623
+ "trash.group.quick": "快捷提示词",
3624
+ "trash.section.quick.sub": "快捷提示词目录已移走:恢复能还原,永久删除会删掉文件",
3625
+ "slash.section.quick": "快捷提示词",
3626
+ "slash.quick.empty": "这条快捷提示词没有正文,先到「设置 → 工具 → 提示词」补上",
3627
+ "compat.ov.slashCommands": "斜杠命令入口", "compat.ov.slashCommands.on": "两段都开(打 / 唤出「工具」与「快捷提示词」两段)",
3628
+ "compat.ov.slashCommands.onTools": "只开「工具」段", "compat.ov.slashCommands.onQuick": "只开「快捷提示词」段",
3629
+ "compat.ov.slashCommands.off": "两段都已关闭(本页可重新打开)",
3630
+ "compat.slash.title": "斜杠命令入口",
3631
+ "compat.slash.tools": "「工具」", "compat.slash.tools.desc": "进场景、启停技能 / MCP / 子智能体 / 提示词(全局与快捷两类)/ 记忆",
3632
+ "compat.slash.quick": "「快捷提示词」", "compat.slash.quick.desc": "取用常用话术、一键贴进输入框(开关在「工具 › 提示词」里)",
3633
+ "compat.slash.restartHint": "斜杠命令修改后重启 DSH 即可生效",
3634
+ "compat.slash.live.on": "已生效", "compat.slash.live.off": "未生效",
3635
+ "compat.slash.mount.registered": "本次启动已挂上", "compat.slash.mount.pending": "还没挂上(宿主服务未就绪)", "compat.slash.mount.unregistered": "本次启动已撤下",
3636
+ "compat.slash.mount.disabled": "这一段的开关关着",
3637
+ "compat.slash.mount.missing": "宿主没有 inputTriggers 服务,这个入口不会出现",
3638
+ "compat.slash.mount.conflict": "源名与另一个插件重名,本次没挂上",
3408
3639
  // 降级 note 的模板(detailKey 见 src/compat/runtime-notes.ts;占位符与英文版一一对应)。
3409
3640
  "compat.note.skillsProvider": "技能 provider 装配失败({reason}):技能启用/停用与 agent 技能目录这一轮不生效。",
3410
3641
  "compat.note.contextInjection": "注入通道装配失败({reason}):全部注入域的内容这一轮不会被送进模型。",
3642
+ "compat.note.contextInjectionRuntime.domain": "注入域「{domain}」这一轮取数失败:该域本轮没有发送任何内容,也不会误发一条「已清空」(保留上一轮内容不动),下一步会自动重试。",
3643
+ "compat.note.contextInjectionRuntime.throw": "本步注入抛异常,这一轮全部注入域的内容都没有送进模型。通道仍在,下一步会自动重试。",
3411
3644
  "compat.note.cordisOriginalSymbol": "cordis 导出的 symbols.original 与 Symbol.for(\"cordis.original\") 不是同一个符号:能力探测可能把代理当原始对象,身份判定会失真。",
3412
3645
  "compat.note.patchWriteGuard.noDep": "校验依赖不可用:{detail}(写入照常进行,仅少一道「把启动配置写坏」的拦截)",
3413
3646
  "compat.note.patchWriteGuard.stale": "复刻可能已过期:{detail}(写入照常进行,仅少一道「把启动配置写坏」的拦截)",
@@ -3418,6 +3651,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3418
3651
  "compat.note.mcpToolVisibility.noRestrict": "agent scope 上没有可用的 tools.restrict(官方接口变了或该 scope 未暴露 tools):停用的 MCP 工具与兼容页关掉的本插件工具仍会出现在模型可见的工具表里,执行侧拦截仍然生效。",
3419
3652
  "compat.note.mcpToolVisibility.partial": "有工具没能从模型可见的工具表里摘掉({reason}):执行侧拦截仍然生效,模型仍能看到该工具的名字。",
3420
3653
  "compat.note.workspaceDeleteNative": "宿主已提供原生 {entry}:本插件仍走自有完整序列(级联子会话 + spill + 记账 + 缓存行),尚未切换到原生入口。",
3654
+ "compat.note.mcpServerNameAmbiguous.separator": "这些 MCP 服务器名里含 __({names}):工具全名 mcp__<server>__<tool> 因此切不唯一,界面上的勾选可能落不到停用表里那条键上(工具本身照常可用)。改名可消除;不改也能用。",
3655
+ "compat.note.mcpServerNameAmbiguous.prefix": "这些 MCP 服务器名互为 __ 前缀关系({pairs}):例如 mcp__a__b__c 既可以读成 a 的 b__c,也可以读成 a__b 的 c,界面上的勾选可能落不到停用表里那条键上(工具本身照常可用)。改名可消除;不改也能用。",
3421
3656
  // 路由缺失能力(refusal)的标签:id 对齐 src/compat/probe.ts 的 spec id 与 operation 兜底。
3422
3657
  "compat.cap.workspaceReadState": "读取工作区状态", "compat.cap.workspaceReadTable": "读取工作区表", "compat.cap.workspaceIndexShape": "工作区索引结构",
3423
3658
  "compat.cap.workspaceReadHeader": "读取会话头部", "compat.cap.workspaceEnqueue": "串行写事务", "compat.cap.workspaceSetState": "写工作区状态",
@@ -3473,7 +3708,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3473
3708
  "compat.token.failed": "读取令牌状态失败,请刷新后重试。",
3474
3709
  "compat.inject.domains": "注入内容",
3475
3710
  "compat.inject.domain.scene": "场景", "compat.inject.domain.memory": "记忆", "compat.inject.domain.mcp": "MCP",
3476
- "compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "提示词",
3711
+ "compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "全局提示词",
3477
3712
  "compat.inject.domain.skills": "技能",
3478
3713
  "compat.inject.domain.carrier.title": "关掉后,系统自带的那份也会一并停掉",
3479
3714
  "compat.tools": "模型工具表", "compat.tools.hint": "哪些工具发给模型:工具表每一轮都随请求发一遍,关掉的整份不发(≈ 是估算)",
@@ -3533,8 +3768,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3533
3768
  "compat.tools.about.subagent_manager_run": "按人设派子智能体",
3534
3769
  "compat.tools.about.subagent_manager_switch": "启用/停用人设",
3535
3770
  "compat.tools.about.subagent_manager_save": "新建/修改人设",
3536
- "compat.tools.about.prompt_manager_list": "列出提示词预设",
3537
- "compat.tools.about.prompt_manager_apply": "应用一份提示词预设",
3771
+ "compat.tools.about.prompt_manager_list": "列出全局提示词预设",
3772
+ "compat.tools.about.prompt_manager_apply": "应用一份全局提示词预设",
3538
3773
  "compat.tools.hover.memory_manager_list": "列出记忆的 id、所属场景、启停与一句话描述,并标明注入按哪个场景走。默认只列这次真的会注入的记忆;all=true 会把停用、场景未启用、被同名覆盖的一起列出来。场景本身(档案、绑定的预设、锁没锁)在 scene_manager_list。只看不改。",
3539
3774
  "compat.tools.hover.memory_manager_read": "按 id(<场景>/<名字>)读一条记忆的完整正文。清单里只有一句话描述,要看正文就用它 —— 尤其是被停用、场景没启用、或被体积预算挤掉的记忆。",
3540
3775
  "compat.tools.hover.memory_manager_switch": "只拨索引里的开关,.md 文件一个字不动,随时能拨回来。停用的记忆不再进上下文,等于把注入预算让给别人 —— 想省 token 优先用它,而不是删。",
@@ -3553,7 +3788,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3553
3788
  "compat.tools.hover.subagent_manager_run": "把任务交给某个具名人设:它拿到人设当自己的系统提示词、在自己的上下文里做完、只回最终结果。开 inherit 它会看到本次会话已完成的对话。花真 token,默认要用户确认。",
3554
3789
  "compat.tools.hover.subagent_manager_switch": "停用的人设不进目录也不能被委派;被场景绑定的人设,在进入该场景时会自动启用。",
3555
3790
  "compat.tools.hover.subagent_manager_save": "新建人设,或改掉已存在的那份 —— 回执会说清走了哪条。可改名字、描述、正文(正文会作为子代理的系统提示词)与输出要求,没传的字段保持原样;改名会连带改场景与档案里的绑定。provider / 模型 / 工具限制只留给面板(模型看不到自己改对了没有,所以不给)。描述决定模型什么时候会选它。",
3556
- "compat.tools.hover.prompt_manager_list": "列出提示词预设与摘要,标明当前生效的是哪一份 —— 同时只有一份生效。要换用哪份之前先用它确认名字。",
3791
+ "compat.tools.hover.prompt_manager_list": "列出全局提示词预设(写进 AGENTS.md 的那一类)与摘要,标明当前生效的是哪一份 —— 同时只有一份生效。要换用哪份之前先用它确认名字。快捷提示词不在这个域里:那是用户自己在输入框里点出来的一段话,不进上下文,模型这边没有可做的动作。",
3557
3792
  "compat.tools.hover.prompt_manager_apply": "把某份预设应用到用户级 AGENTS.md,之后的对话立刻按它走。一次只有一份在生效,换一份就是替换上一份。",
3558
3793
  "compat.live.title": "注入实况", "compat.live.hint": "模型实际看到的内容(只读)",
3559
3794
  "compat.live.never": "这段对话还没投过", "compat.live.delivered": "本段对话已投递 {count} 条 · 最近 {time}",
@@ -3561,6 +3796,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3561
3796
  "compat.live.state.inContext": "已注入", "compat.live.state.absent": "未投递", "compat.live.state.cleared": "已清空",
3562
3797
  "compat.live.state.off": "已关闭", "compat.live.state.unknown": "没有会话",
3563
3798
  "compat.live.state.child": "不在本会话注入",
3799
+ "compat.live.state.error": "取数失败",
3800
+ "compat.live.state.error.title": "这个域这一轮取数抛了异常(内容还在,是这一步没取到)—— 兼容页「上下文注入通道」有一行降级写着原因。它不会误发「已清空」,下一步会自动重试。",
3564
3801
  "compat.live.state.child.title": "本会话的深度超过了人设的目录注入深度(catalogDepth),所以常驻目录不在这里注入。要让目录出现在子会话,把对应人设的「目录注入」调大(或选「不限制嵌套」)。注意:这不影响委派 —— 子代理始终可以继续委派。",
3565
3802
  "compat.live.adopt": "注入 {injected} 次 · 调用 {used} 次 · 采纳 {adopted} 次",
3566
3803
  "compat.live.adopt.none": "注入 {injected} 次 · 从未调用",
@@ -3586,7 +3823,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3586
3823
  "scenes.mode.active": "当前模式:{name}", "scenes.mode.profile": "档案:{parts}", "scenes.mode.noProfile": "这个场景还没有档案",
3587
3824
  "scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
3588
3825
  "scenes.search.placeholder": "搜索场景名或描述", "scenes.empty.search": "没有匹配的场景",
3589
- "scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
3826
+ "scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.quickPrompts": "快捷提示词 {count} 条", "scenes.profile.memories": "记忆 {count} 条",
3590
3827
  "scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。没勾的、以及整段没建的,进场景时一律停用(含停掉正在运行的进程)。",
3591
3828
  "scenes.seg.checked": "{checked}/{total} 已勾选",
3592
3829
  "bulk.selectAll": "全选", "bulk.unselectAll": "取消全选",
@@ -3665,9 +3902,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3665
3902
  "memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}", "memory.result.sceneRemoved.withMemories": "已删除场景:{name}(连同 {count} 项记忆,可在回收站整条恢复)",
3666
3903
  "memory.table.name": "记忆名称与描述", "memory.table.tags": "标记",
3667
3904
  "error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.exists": "同名记忆已存在,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.outsideRoot": "拒绝操作:解析后的落点不在记忆根目录内(可能存在指向根外的目录链接)", "error.rules.nameTaken": "目标名称已被占用", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
3668
- "memory.archive.emptySection": "没有勾选任何条目 = 全部停用(整段没建也一样)", "memory.archive.emptySubagents": "没有勾选任何条目 = 本场景不启用任何人设(整段没建也一样)",
3669
3905
  "subagents.title": "子智能体", "subagents.stat.total": "个子智能体", "subagents.stat.limited": "个有工具限制", "subagents.desc": "管理子智能体:新建、导入人设,正文即子代理的系统提示词。",
3670
- "prompts.desc": "管理提示词:预设全局指令基线,应用后写入 AGENTS.md,下一轮对话生效。",
3906
+ "prompts.desc": "管理提示词:全局那份写入 AGENTS.md,快捷那份只在对话框里取用。",
3671
3907
  "sessions.desc": "管理已归档会话:可恢复或永久删除,超保留期自动清理。",
3672
3908
  "subagents.new": "新增子智能体", "subagents.empty": "还没有子智能体——点「新增子智能体」创建第一个。", "subagents.empty.search": "没有匹配的子智能体", "subagents.search.placeholder": "搜索人设名或描述",
3673
3909
  "subagents.create": "新建人设", "subagents.edit": "编辑人设",
@@ -3713,7 +3949,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3713
3949
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
3714
3950
  "error.import.noFiles": "没有选择要导入的文件",
3715
3951
  "nav.title": "工具",
3716
- "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": "为当前场景设置全局提示词", "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.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "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": "未知错误",
3952
+ "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.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.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.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.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "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.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "按场景勾选", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "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.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "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": "未知错误",
3717
3953
  },
3718
3954
  en: {
3719
3955
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -3745,7 +3981,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3745
3981
  "result.custom.added": "Custom folder added: {path}",
3746
3982
  "result.custom.removed": "Custom folder removed: {name}",
3747
3983
  "result.opened": "Opened with the system default app: {path}",
3748
- "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.shadowed.hint": "The copy “{name}” is the active one; click “Use this one” to switch to this source instead", "status.preferred": "Manually chosen", "status.readonly": "Source read-only", "status.notDeletable": "Cannot delete", "status.notDeletable.hint": "Deleting is available for plugin-managed sources (DSH skills / Imported skills / project .dsh/skills); external agent and custom directories are read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
3984
+ "status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.shadowed.hint": "The copy “{name}” is the active one; click “Use this one” to switch to this source instead", "status.shadowed.inert": "The copy “{name}” is disabled, so this source's version is blocked without taking effect either — nothing is active; enable one of the two to fix it", "status.preferred": "Manually chosen", "status.readonly": "Source read-only", "status.notDeletable": "Cannot delete", "status.notDeletable.hint": "Deleting is available for plugin-managed sources (DSH skills / Imported skills / project .dsh/skills); external agent and custom directories are read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
3749
3985
  "summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description",
3750
3986
  "filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
3751
3987
  "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",
@@ -3766,7 +4002,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3766
4002
  "error.rules.reservedScene": "「{name}」 is a reserved scene: {action} is not allowed{reason}",
3767
4003
  "error.rules.singleSceneOnly": "Only one scene besides 「global」 may be enabled at a time{detail}",
3768
4004
  "error.agentsMd.sceneRebindFailed": "Cannot change the prompt binding of scene 「{scene}」 to 「{target}」: {reason}",
3769
- "error.agentsMd.referenced": "「{id}」 is still referenced and cannot be deleted: {refs}. Remove those references first (rebind the prompt / leave the scene / apply another preset).",
4005
+ "error.agentsMd.referenced": "「{id}」 is still referenced and cannot be deleted: {refs}. Remove those references first (rebind the prompt / leave the scene / apply another preset).", "error.agentsMd.refProbeFailed": "Cannot tell whether “{id}” is still referenced, so the delete was refused: the reference probe failed (at least one of scene bindings / the current AGENTS.md content / the pre-scene baseline could not be read). Please try again later.",
3770
4006
  "error.source.reserved": "{origin}: cannot be disabled or removed (the skills inside can still be deleted)",
3771
4007
  "action.rename": "rename", "action.lock": "locking",
3772
4008
  "trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
@@ -3884,14 +4120,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3884
4120
  "snapshot.export.dir": "Export directory", "snapshot.export.btn": "Export", "snapshot.export.busy": "Exporting…", "snapshot.export.done": "Snapshot written to: {dir}", "snapshot.export.failed": "Export failed",
3885
4121
  "snapshot.export.secrets": "Full export (plaintext credentials included)", "snapshot.export.secrets.warn": "This writes secrets verbatim. The directory is meant to travel to another machine or a cloud drive — check it only if you mean it.",
3886
4122
  "snapshot.counts": "{items}", "snapshot.notes": "Notes: {items}",
3887
- "snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "Memories", "snapshot.domain.skills": "Skills", "snapshot.domain.subagents": "Personas", "snapshot.domain.prompts": "Prompts", "snapshot.domain.settings": "Sidecar config", "snapshot.domain.scenes": "Scene profiles",
4123
+ "snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "Memories", "snapshot.domain.skills": "Skills", "snapshot.domain.subagents": "Personas", "snapshot.domain.prompts": "Global prompts", "snapshot.domain.quick-prompts": "Quick prompts", "snapshot.domain.settings": "Sidecar config", "snapshot.domain.scenes": "Scene profiles",
3888
4124
  "snapshot.import.dir": "Snapshot location", "snapshot.import.dir.placeholder": "e.g. D:\\backup\\dsh-tool-management-snapshot-…",
3889
4125
  "snapshot.import.preview": "Import", "snapshot.import.busy": "Working…",
3890
4126
  "snapshot.import.row": "{domain} {count} item(s) · {clashes} clash",
3891
4127
  "snapshot.import.conflict.skip": "Skip duplicates", "snapshot.import.conflict.overwrite": "Overwrite duplicates",
3892
4128
  "snapshot.import.run": "Run import", "snapshot.import.done": "Import finished", "snapshot.import.failed": "Import failed",
3893
4129
  "snapshot.import.none": "No area selected", "snapshot.import.notFound": "Nothing importable in this snapshot",
3894
- "snapshot.import.report": "{domain}: {imported} in · {skipped} skipped", "snapshot.import.reportFailed": "{domain}: failed ({error})",
4130
+ "snapshot.import.report": "{domain}: {imported} in · {skipped} skipped", "snapshot.import.reportFailed": "{domain}: failed ({error})", "snapshot.import.reportWarning": "⚠ Needs attention: {warnings}",
3895
4131
  "snapshot.import.locked": "{names} is locked — unlock it before importing.",
3896
4132
  "snapshot.import.versionNote": "Made by plugin {version}",
3897
4133
  "snapshot.import.masked": "secrets masked",
@@ -3909,7 +4145,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3909
4145
  "compat.audit.op.ruleSave": "Saved a memory", "compat.audit.op.ruleDelete": "Deleted a memory", "compat.audit.op.ruleSwitch": "Enabled/disabled a memory", "compat.audit.op.ruleImport": "Imported a memory", "compat.audit.op.ruleAttach": "Attached to / detached from a scene", "compat.audit.op.ruleIndex": "Rebuilt the memory index", "compat.audit.op.memorySwitch": "Switched the active memory set",
3910
4146
  "compat.audit.op.sceneSave": "Saved a scene profile", "compat.audit.op.sceneDelete": "Deleted a scene", "compat.audit.op.sceneRebind": "Rebound the prompt", "compat.audit.op.sceneLock": "Changed the scene lock", "compat.audit.op.sceneEnter": "Entered / left a scene", "compat.audit.op.engineApply": "Scene engine applied a profile", "compat.audit.op.engineRestore": "Scene engine restored the snapshot",
3911
4147
  "compat.audit.op.personaSave": "Saved a persona", "compat.audit.op.personaDelete": "Deleted a persona", "compat.audit.op.personaImport": "Imported a persona", "compat.audit.op.personaSwitch": "Enabled/disabled a persona",
3912
- "compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
4148
+ "compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.quickSave": "Saved a quick prompt", "compat.audit.op.quickSwitch": "Toggled a quick prompt", "compat.audit.op.quickDelete": "Deleted a quick prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
3913
4149
  "compat.audit.op.trashRestore": "Restored from trash", "compat.audit.op.trashDelete": "Deleted forever",
3914
4150
  "compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.backupsClean": "Cleaned old backups",
3915
4151
  "compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
@@ -3967,9 +4203,67 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3967
4203
  "compat.ov.hostIdentity": "Host identity & modules", "compat.ov.hostIdentity.ok": "Modules are shared with the host (host DSH {version})", "compat.ov.hostIdentity.asar": "Official packages match the host version (DSH {version}), but the host runs inside {name} while the plugin sits outside it", "compat.ov.hostIdentity.degraded": "Blockers present: {list}",
3968
4204
  "compat.ov.memory": "Memory", "compat.ov.prompts": "Prompts", "compat.ov.subagents": "Subagents", "compat.ov.scenes": "Scene profiles",
3969
4205
  "compat.ov.plain.ok": "No degradation reported (assembly failures would appear here)",
4206
+ // Slash-command "Tools" section (chat composer). Domain labels stay short — the row keeps
4207
+ // room on the right for the summary and the drill chevron.
4208
+ "slash.section.tools": "Tools",
4209
+ "slash.domain.scene": "Scenes", "slash.domain.skill": "Skills", "slash.domain.mcp": "MCP servers",
4210
+ "slash.domain.agent": "Subagents", "slash.domain.preset": "Prompts", "slash.domain.memory": "Memories",
4211
+ "slash.sum.current": "now: {name}", "slash.sum.count": "{n} items",
4212
+ "slash.sum.onoff": "{on} on / {off} off", "slash.sum.sceneGlobal": "{n} in scene “{scene}” + global",
4213
+ "slash.sum.quick": "quick {on}/{total}",
4214
+ "slash.group.noScene": "No scene",
4215
+ "slash.state.on": "Enabled", "slash.state.off": "Disabled",
4216
+ "slash.skill.sourceRow": "directory",
4217
+ "slash.did.on": "Enabled {name}", "slash.did.off": "Disabled {name}",
4218
+ "slash.did.scene": "Entered scene “{name}”", "slash.did.sceneOff": "Left scene “{name}” (only global and shared stay injected)",
4219
+ "slash.did.preset": "Applied baseline “{name}”",
4220
+ "slash.did.quickOn": "Enabled quick prompt “{name}” — it is back in the / “Quick prompts” block",
4221
+ "slash.did.quickOff": "Disabled quick prompt “{name}” (hidden from the / “Quick prompts” block; the text stays)",
4222
+ "slash.did.sourceOn": "Enabled directory “{name}” — the skills inside it that were on are live again",
4223
+ "slash.did.sourceOff": "Disabled directory “{name}” (the whole folder stops being read)",
4224
+ "slash.did.already": "“{name}” is already the current one",
4225
+ "slash.err.load": "Could not read the {domain} list: {error}", "slash.err.gone": "That entry is gone — reopen the list",
4226
+ "slash.err.failed": "The change did not go through", "slash.err.pick": "Use ↑↓ to pick a row, then Enter (this half-typed command was not sent)",
4227
+ // Quick prompts (second section of the prompts page; its own block in the / menu).
4228
+ // The whole difference between the two kinds lives in these strings: one writes into
4229
+ // AGENTS.md and stays in context, the other is just text pasted into the composer.
4230
+ "prompts.kind.label": "Prompt type",
4231
+ "prompts.kind.global": "Global prompt", "prompts.kind.quick": "Quick prompt",
4232
+ "prompts.kind.global.hint": "Applying writes it into AGENTS.md and it takes effect next turn; scenes can bind it.",
4233
+ "prompts.kind.quick.hint": "For use in the composer: inserts the prompt into the input box.",
4234
+ "prompts.group.global": "Global prompts", "prompts.group.quick": "Quick prompts", "prompts.bulk.quick": "Turn every visible quick prompt in this group on / off", "prompts.bulk.failed": "{done} changed, {failed} did not",
4235
+ "quickprompt.stat": " quick prompts",
4236
+ "quickprompt.toggle": "Show in the / menu",
4237
+ "quickprompt.toggle.hint": "Turned off, this one stops appearing in the chat box's / “Quick prompts” block — its text and note are kept, and you can turn it back on (it stays listed under “Tools › Prompts”, where the same switch works)",
4238
+ "quickprompt.empty": "No quick prompts yet. Click “New prompt” and pick the type “Quick prompt”.",
4239
+ "quickprompt.edit.title": "Edit quick prompt",
4240
+ "quickprompt.field.id.hint": "This is also the row's name in the / menu; non-ASCII is fine.",
4241
+ "quickprompt.field.desc.placeholder": "One line to remember what it's for (only for you)",
4242
+ "quickprompt.field.content": "Text",
4243
+ "quickprompt.field.content.placeholder": "This is pasted verbatim into the composer when you click it",
4244
+ "quickprompt.remove.title": "Delete quick prompt",
4245
+ "quickprompt.remove.hint": "Deleting moves it to the trash — restore it from the Trash button on this page.",
4246
+ "trash.group.quick": "Quick prompts",
4247
+ "trash.section.quick.sub": "Quick-prompt folders were moved: restore brings them back, delete-forever removes them",
4248
+ "slash.section.quick": "Quick prompts",
4249
+ "slash.quick.empty": "This quick prompt has no text — add it under Settings → Tools → Prompts",
4250
+ "compat.ov.slashCommands": "Slash command entry", "compat.ov.slashCommands.on": "Both on (type / for the “Tools” and “Quick prompts” blocks)",
4251
+ "compat.ov.slashCommands.onTools": "Only the “Tools” section is on", "compat.ov.slashCommands.onQuick": "Only the “Quick prompts” section is on",
4252
+ "compat.ov.slashCommands.off": "Both sections turned off (re-enable them on this page)",
4253
+ "compat.slash.title": "Slash command entry",
4254
+ "compat.slash.tools": "“Tools”", "compat.slash.tools.desc": "Enter scenes; toggle skills / MCP / subagents / prompts (both kinds) / memories",
4255
+ "compat.slash.quick": "“Quick prompts”", "compat.slash.quick.desc": "Reuse saved snippets, one click into the composer (their switches live under “Tools › Prompts”)",
4256
+ "compat.slash.restartHint": "Slash commands: restart DSH for the change to take effect",
4257
+ "compat.slash.live.on": "In effect", "compat.slash.live.off": "Not in effect",
4258
+ "compat.slash.mount.registered": "Mounted this startup", "compat.slash.mount.pending": "Not mounted yet (host service still starting)", "compat.slash.mount.unregistered": "Unmounted this startup",
4259
+ "compat.slash.mount.disabled": "Switched off by its own checkbox on this page",
4260
+ "compat.slash.mount.missing": "This host has no inputTriggers service, so the entry never appears",
4261
+ "compat.slash.mount.conflict": "Another plugin already owns the source name, so it was not mounted",
3970
4262
  // Templates for degraded runtime notes (detailKey per src/compat/runtime-notes.ts; placeholders mirror the zh side one for one).
3971
4263
  "compat.note.skillsProvider": "Skills provider setup failed ({reason}): skill enable/disable and the agent skill catalog will not take effect this round.",
3972
4264
  "compat.note.contextInjection": "Injection channel setup failed ({reason}): no injection domain's content will reach the model this round.",
4265
+ "compat.note.contextInjectionRuntime.domain": "Injection domain \"{domain}\" failed to produce its text this round: nothing was sent for it, no false \"cleared\" notice was emitted (the previous content is left in place), and the next step retries automatically.",
4266
+ "compat.note.contextInjectionRuntime.throw": "Injection threw this step: no injection domain's content reached the model this round. The channel is still mounted and the next step retries automatically.",
3973
4267
  "compat.note.cordisOriginalSymbol": "The exported symbols.original is not the same symbol as Symbol.for(\"cordis.original\"): capability probing may treat proxies as raw objects, so identity checks can be skewed.",
3974
4268
  "compat.note.patchWriteGuard.noDep": "Validation dependency unavailable: {detail} (the write proceeds; only the guard against corrupting the boot config is missing)",
3975
4269
  "compat.note.patchWriteGuard.stale": "The replica may be outdated: {detail} (the write proceeds; only the guard against corrupting the boot config is missing)",
@@ -3980,6 +4274,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3980
4274
  "compat.note.mcpToolVisibility.noRestrict": "No tools.restrict is available on the agent scope (the official interface changed, or the scope does not expose tools): disabled MCP tools and plugin tools turned off on the compat page still appear in the model-visible tool table; call-side interception still applies.",
3981
4275
  "compat.note.mcpToolVisibility.partial": "Some tools could not be removed from the model-visible tool table ({reason}): call-side interception still applies, but the model still sees those tool names.",
3982
4276
  "compat.note.workspaceDeleteNative": "The host now offers native {entry}: this plugin still uses its own full sequence (cascade sub-sessions + spill + bookkeeping + cache rows) and has not switched to the native entry.",
4277
+ "compat.note.mcpServerNameAmbiguous.separator": "These MCP server names contain __ ({names}): the tool full name mcp__<server>__<tool> is therefore not uniquely splittable, so a checkbox in the UI may not land on the key used in the disabled-tools table (the tool itself still works). Renaming removes the ambiguity; leaving it as is still works.",
4278
+ "compat.note.mcpServerNameAmbiguous.prefix": "These MCP server names are __-prefix related ({pairs}): for example mcp__a__b__c can be read as a's b__c or as a__b's c, so a checkbox in the UI may not land on the key used in the disabled-tools table (the tool itself still works). Renaming removes the ambiguity; leaving it as is still works.",
3983
4279
  // Labels for route refusals: ids mirror the spec ids in src/compat/probe.ts plus the operation fallback.
3984
4280
  "compat.cap.workspaceReadState": "Read workspace state", "compat.cap.workspaceReadTable": "Read the workspace table", "compat.cap.workspaceIndexShape": "Workspace index shape",
3985
4281
  "compat.cap.workspaceReadHeader": "Read session header", "compat.cap.workspaceEnqueue": "Serialized write transaction", "compat.cap.workspaceSetState": "Write workspace state",
@@ -4033,7 +4329,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4033
4329
  "compat.inject.force": "Also inject in Minimal mode", "compat.inject.force.desc": "Off by default (follow the preset)",
4034
4330
  "compat.inject.domains": "What to inject",
4035
4331
  "compat.inject.domain.scene": "Scene", "compat.inject.domain.memory": "Memory", "compat.inject.domain.mcp": "MCP",
4036
- "compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Prompt",
4332
+ "compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Global prompts",
4037
4333
  "compat.inject.domain.skills": "Skills",
4038
4334
  "compat.inject.domain.carrier.title": "Turning this off also stops the copy the system sends by itself",
4039
4335
  "compat.tools": "Model tool table", "compat.tools.hint": "which tools reach the model: the table is sent with every request, so a switched-off tool is not sent at all (≈ is an estimate)",
@@ -4091,8 +4387,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4091
4387
  "compat.tools.about.subagent_manager_run": "Run a persona as a subagent",
4092
4388
  "compat.tools.about.subagent_manager_switch": "Enable/disable a persona",
4093
4389
  "compat.tools.about.subagent_manager_save": "Create/update a persona",
4094
- "compat.tools.about.prompt_manager_list": "List prompt presets",
4095
- "compat.tools.about.prompt_manager_apply": "Apply a prompt preset",
4390
+ "compat.tools.about.prompt_manager_list": "List global prompt presets",
4391
+ "compat.tools.about.prompt_manager_apply": "Apply a global prompt preset",
4096
4392
  "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.",
4097
4393
  "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.",
4098
4394
  "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.",
@@ -4111,7 +4407,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4111
4407
  "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.",
4112
4408
  "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.",
4113
4409
  "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.",
4114
- "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.",
4410
+ "compat.tools.hover.prompt_manager_list": "Lists the global prompt presets (the ones written into AGENTS.md) 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. Quick prompts are not this domain: the user picks those in the composer, they never enter the context, and there is nothing for the model to do with them.",
4115
4411
  "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.",
4116
4412
  "compat.live.title": "Live injection", "compat.live.hint": "What the model actually sees (read-only)",
4117
4413
  "compat.live.never": "nothing delivered in this conversation yet", "compat.live.delivered": "{count} delivered in this conversation · last {time}",
@@ -4119,6 +4415,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4119
4415
  "compat.live.state.inContext": "Injected", "compat.live.state.absent": "Not delivered", "compat.live.state.cleared": "Cleared",
4120
4416
  "compat.live.state.off": "Off", "compat.live.state.unknown": "No session",
4121
4417
  "compat.live.state.child": "Not injected here",
4418
+ "compat.live.state.error": "Failed to load",
4419
+ "compat.live.state.error.title": "This domain threw while producing its text this round (the content exists; this step just could not read it). The Compat page's \"Context injection channel\" row carries a degraded note with the reason. No false \"cleared\" notice is sent, and the next step retries automatically.",
4122
4420
  "compat.live.state.child.title": "This session is deeper than the personas' catalog injection depth (catalogDepth), so the standing catalog is not injected here. To let the catalog reach subagent sessions, raise the persona's catalog injection depth (or pick “No nesting limit”). Note: this does not affect delegation — subagents can always delegate further.",
4123
4421
  "compat.live.adopt": "injected {injected} · called {used} · adopted {adopted}",
4124
4422
  "compat.live.adopt.none": "injected {injected} · never called",
@@ -4144,7 +4442,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4144
4442
  "scenes.mode.active": "Active mode: {name}", "scenes.mode.profile": "Profile: {parts}", "scenes.mode.noProfile": "This scene has no profile yet",
4145
4443
  "scenes.empty": "No dedicated scenes yet — global memories are injected without one; create a scene when you need to switch MCP servers, skills or personas.",
4146
4444
  "scenes.search.placeholder": "Search scene names or descriptions", "scenes.empty.search": "No matching scenes",
4147
- "scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
4445
+ "scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.quickPrompts": "{count} quick prompt(s)", "scenes.profile.memories": "{count} memor(ies)",
4148
4446
  "scenes.mcp.hint": "Add the MCP section first, then check the servers to enable (stopped ones included); use Pick tools to narrow to specific tools. Unchecked servers — and scenes with no section at all — are turned off on entry, running processes included.",
4149
4447
  "scenes.seg.checked": "{checked}/{total} selected",
4150
4448
  "bulk.selectAll": "Select all", "bulk.unselectAll": "Deselect all",
@@ -4169,7 +4467,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4169
4467
  "memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent set", "memory.archive.memories": "Memory set", "memory.archive.applyFailed": "Could not apply at runtime: {reason}",
4170
4468
  "memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagent set",
4171
4469
  "memory.archive.removeSection": "Remove section", "memory.archive.save": "Save to scene",
4172
- "memory.archive.emptySection": "Nothing checked = all disabled (a missing section means the same)", "memory.archive.emptySubagents": "Nothing checked = no personas enabled in this scene (a missing section means the same)", "memory.archive.stale": "Skipped {items} (missing, shadowed by a same-name skill, or structurally broken — it could never take effect)",
4470
+ "memory.archive.stale": "Skipped {items} (missing, shadowed by a same-name skill, or structurally broken — it could never take effect)",
4173
4471
  "memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeSwitched": "{mcp} server(s) / {skills} source(s) switched", "memory.result.modeExited": "Exited mode",
4174
4472
  "memory.desc": "Manage memories: enabled memories enter the system prompt; enable them per scene.",
4175
4473
  "memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
@@ -4224,7 +4522,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4224
4522
  "memory.table.name": "Memory name and description", "memory.table.tags": "Tags",
4225
4523
  "error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.exists": "A memory with this name already exists", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.outsideRoot": "Refused: the resolved path falls outside the memories root (a directory link may point outside)", "error.rules.nameTaken": "That name is already taken", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
4226
4524
  "subagents.title": "Subagents", "subagents.stat.total": " subagents", "subagents.stat.limited": " tool-restricted", "subagents.desc": "Manage subagents: create and import personas; a persona's body is the subagent's system prompt.",
4227
- "prompts.desc": "Manage prompts: preset the global instruction baseline; applying writes AGENTS.md and takes effect on the next turn.",
4525
+ "prompts.desc": "Manage prompts: the global ones are written into AGENTS.md, the quick ones are only for the composer.",
4228
4526
  "sessions.desc": "Manage archived sessions: restore or delete them permanently; expired ones are cleaned up automatically.",
4229
4527
  "subagents.new": "New subagent", "subagents.empty": "No subagents yet — click “New subagent” to create the first one.", "subagents.empty.search": "No matching subagents", "subagents.search.placeholder": "Search persona names or descriptions",
4230
4528
  "subagents.create": "New persona", "subagents.edit": "Edit persona",
@@ -4270,7 +4568,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4270
4568
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
4271
4569
  "error.import.noFiles": "No files selected",
4272
4570
  "nav.title": "Tools",
4273
- "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": "Sets the global prompt for this scene", "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.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "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",
4571
+ "agm.stat.total": " global 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.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New 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.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.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "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.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(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.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "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); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.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.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.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",
4274
4572
  }
4275
4573
  };
4276
4574
 
@@ -4612,6 +4910,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4612
4910
  }
4613
4911
  probeTokenLock(0)
4614
4912
 
4913
+ // ── 斜杠命令「工具」段与「快捷提示词」段(实现与理由见 48-slash.js)───────────
4914
+ // 放在 slots 守卫**之前**:这两段与设置页挂载与否无关,宿主没给 slots 时对话页照样该有它。
4915
+ // 开关读一次就定本次启动的形态(改了开关要重开页面才生效 —— 源注册是启动期一次性的事,
4916
+ // 热挂热撤会让 `/` 菜单在用户眼皮底下变样,比"重启后生效"更难解释)。
4917
+ // 读失败按**开**处理:这两颗开关的默认值就是开,判不出来时不该把一个官方通道上的功能藏掉。
4918
+ apiCall('slash-settings', {}).then(function (res) {
4919
+ mountSlashCommands(ctx, t, !res || res.ok !== true ? {} : res.settings || {})
4920
+ }).catch(function () {
4921
+ mountSlashCommands(ctx, t, {})
4922
+ })
4923
+
4615
4924
  const slots = ctx.get('slots')
4616
4925
  if (slots === undefined) return
4617
4926
 
@@ -5160,9 +5469,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5160
5469
  const renderGroup = (group, groupRows) => {
5161
5470
  if (groupRows.length === 0) return null
5162
5471
  const open = collapsed[group.key] !== true || !!normalizedQuery
5472
+ // 翻的是**收起标记本身**(与记忆 / 会话页的 `toggleCollapse` 同一写法)。这里原先写的是
5473
+ // `!open` —— 展开态下 `!open` 是 false,而 `false` 在这张表里的意思正是"没收起",
5474
+ // 于是每次点击都把同一个值写回去:**这张卡片从写下那天起就收不起来**(0.11.0 起,用户实测)。
5475
+ const flip = () => setCollapsed((prev) => Object.assign({}, prev, { [group.key]: prev[group.key] !== true }))
5163
5476
  return React.createElement('section', { key: group.key, className: 'dsm-source' },
5164
5477
  React.createElement('div', { className: 'dsm-source-head' },
5165
- React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: () => setCollapsed(Object.assign({}, collapsed, { [group.key]: !open })) },
5478
+ React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: flip },
5166
5479
  React.createElement('span', { className: 'dsm-source-title', title: group.title }, group.title),
5167
5480
  React.createElement('span', { className: 'dsm-count' }, mt('mcp.servers.count', { count: groupRows.length })),
5168
5481
  group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
@@ -5419,18 +5732,46 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5419
5732
  var xo = React.useState(null), exportState = xo[0], setExportState = xo[1]
5420
5733
  var flipRef = React.useRef(null)
5421
5734
  useFlipReorder(flipRef)
5422
- /** 打开/刷新回收站(每次操作后重读,界面永远显示磁盘上的真实条目)。 */
5735
+ // ── 快捷提示词(用户存好的现成文字,只在对话框 `/` 菜单里由自己点出来)────
5736
+ // 独立一处状态,不塞进上面的 `state`:那边 `refresh()` 是整对象替换,两份数据混在
5737
+ // 一次替换里更容易漏;而快捷列表读失败不该把预设列表一起打成错误页。
5738
+ var qs = React.useState({ loading: true, error: null, prompts: [] })
5739
+ var quick = qs[0], setQuick = qs[1]
5740
+ var qk = React.useState(null), quickEdit = qk[0], setQuickEdit = qk[1]
5741
+ var qf = React.useState(null), quickConfirm = qf[0], setQuickConfirm = qf[1]
5742
+ function refreshQuick() {
5743
+ apiCall('quickprompt-list', {})
5744
+ .then(function (res) {
5745
+ if (res && res.ok) setQuick({ loading: false, error: null, prompts: res.prompts || [] })
5746
+ else setQuick({ loading: false, error: (res && res.error) || t('mcp.msg.loadFailed'), prompts: [] })
5747
+ })
5748
+ .catch(function (e) { setQuick({ loading: false, error: errMsg(e), prompts: [] }) })
5749
+ }
5750
+ /** 打开/刷新回收站(两个提示词域一起列:点一次「回收站」要看全)。 */
5423
5751
  function loadTrash(keepOpen) {
5424
- var next = Object.assign({ loading: true, error: null, entries: [] }, keepOpen ? trash || {} : {})
5425
- setTrash(next)
5426
- apiCall('agentsmd-trash-list', {}).then(function (res) {
5427
- if (res && res.ok) setTrash({ loading: false, error: null, entries: res.trash || [] })
5428
- else setTrash({ loading: false, error: (res && res.error) || t('trash.loadFailed'), entries: [] })
5429
- }).catch(function (e) { setTrash({ loading: false, error: errMsg(e), entries: [] }) })
5752
+ var prev = keepOpen ? trash || {} : {}
5753
+ setTrash(Object.assign({ loading: true, error: null, presets: [], quick: [] }, prev))
5754
+ var read = function (op) {
5755
+ return apiCall(op, {}).catch(function (e) { return { ok: false, error: errMsg(e) } })
5756
+ }
5757
+ Promise.all([read('agentsmd-trash-list'), read('quickprompt-trash-list')]).then(function (both) {
5758
+ var errs = []
5759
+ if (!both[0] || both[0].ok === false) errs.push((both[0] && both[0].error) || t('trash.loadFailed'))
5760
+ if (!both[1] || both[1].ok === false) errs.push((both[1] && both[1].error) || t('trash.loadFailed'))
5761
+ setTrash({
5762
+ loading: false, error: errs.length ? errs.join(t('agm.ref.sep')) : null,
5763
+ presets: ((both[0] && both[0].trash) || []).map(function (e) { return Object.assign({ kind: 'presets' }, e) }),
5764
+ quick: ((both[1] && both[1].trash) || []).map(function (e) { return Object.assign({ kind: 'quick' }, e) }),
5765
+ })
5766
+ })
5767
+ }
5768
+ /** 回收站两个域共用一套按钮:条目自带 `kind`,op 名按它挑(不再各写一份恢复/永久删除)。 */
5769
+ function trashOp(item, action) {
5770
+ return item.kind === 'quick' ? 'quickprompt-trash-' + action : 'agentsmd-trash-' + action
5430
5771
  }
5431
5772
  function restorePreset(item) {
5432
5773
  setTrashBusy(true)
5433
- apiCall('agentsmd-trash-restore', { id: item.id }).then(function (res) {
5774
+ apiCall(trashOp(item, 'restore'), { id: item.id }).then(function (res) {
5434
5775
  setTrashBusy(false)
5435
5776
  if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: { kind: 'ok', text: t('trash.restored', { name: item.name }) } }) }); refresh(); loadTrash(true) }
5436
5777
  else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
@@ -5438,7 +5779,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5438
5779
  }
5439
5780
  function purgePreset(item) {
5440
5781
  setTrashBusy(true)
5441
- apiCall('agentsmd-trash-delete', { id: item.id }).then(function (res) {
5782
+ apiCall(trashOp(item, 'delete'), { id: item.id }).then(function (res) {
5442
5783
  setTrashBusy(false)
5443
5784
  if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: { kind: 'ok', text: t('trash.purged', { name: item.name }) } }) }); loadTrash(true) }
5444
5785
  else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
@@ -5446,9 +5787,33 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5446
5787
  }
5447
5788
  // 导入:与技能页同一个 ImportModal(拖拽区 + 文件要求清单 + 结果反馈)。
5448
5789
  // 一次可选多份 .md,逐个读取正文导入;同名/失败逐条回报,不因一个失败中断其余的。
5790
+ // 两个提示词域共用这一颗按钮,类型在弹窗顶部选(`extra` 槽)—— 选完导入的是哪一档
5791
+ // 就写进哪一档,不靠"看当前在哪个分组"猜。
5449
5792
  var impOpenState = React.useState(false); var importOpen = impOpenState[0], setImportOpen = impOpenState[1]
5450
5793
  var impBusyState = React.useState(false); var importBusy = impBusyState[0], setImportBusy = impBusyState[1]
5451
5794
  var impResultState = React.useState(null); var importResult = impResultState[0], setImportResult = impResultState[1]
5795
+ var impKindState = React.useState('global'); var importKind = impKindState[0], setImportKind = impKindState[1]
5796
+ /**
5797
+ * 「全局提示词 / 快捷提示词」二选一的勾选行(与兼容页同款打勾框)。
5798
+ *
5799
+ * 为什么放在弹窗里而不是页头:这两类东西的**读法完全不同**(一份会覆盖写全局
5800
+ * AGENTS.md、一份只是贴进对话框的文字),把它们混进同一个列表再打标签,用户要先
5801
+ * 看懂标签才知道点下去会发生什么。新建与导入都先问一次,问在最贴近动作的地方。
5802
+ *
5803
+ * 两颗框恒有一棵勾着(点已勾着的那颗不改状态)—— 没有"都不选"这一态,所以是
5804
+ * 单选的语义、打勾框的样子。
5805
+ */
5806
+ function kindPicker(value, onChange) {
5807
+ const pick = function (kind) {
5808
+ return React.createElement('label', { className: 'dsm-kind-pick' },
5809
+ React.createElement('input', { type: 'checkbox', checked: value === kind, onChange: function () { onChange(kind) } }),
5810
+ t('prompts.kind.' + kind))
5811
+ }
5812
+ return React.createElement('div', { className: 'dsm-field' },
5813
+ React.createElement('span', { className: 'dsm-label' }, t('prompts.kind.label')),
5814
+ React.createElement('div', { className: 'dsm-kind-picks' }, pick('global'), pick('quick')),
5815
+ React.createElement('p', { className: 'dsm-help' }, t(value === 'quick' ? 'prompts.kind.quick.hint' : 'prompts.kind.global.hint')))
5816
+ }
5452
5817
  function importIdOf(name) {
5453
5818
  return String(name || '').replace(/\.(md|markdown|txt)$/i, '').trim().replace(/[\\/<>:"|?*]+/g, '-') || 'imported'
5454
5819
  }
@@ -5457,12 +5822,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5457
5822
  if (!list.length) return
5458
5823
  setImportBusy(true); setImportResult(null)
5459
5824
  var done = [], failed = []
5825
+ var quickIn = importKind === 'quick'
5460
5826
  var chain = Promise.resolve()
5461
5827
  list.forEach(function (file) {
5462
5828
  chain = chain.then(function () {
5463
5829
  return file.text().then(function (text) {
5464
5830
  var id = importIdOf(file.name)
5465
- return apiCall('agentsmd-import', { id: id, content: String(text) }).then(function (res) {
5831
+ return apiCall(quickIn ? 'quickprompt-create' : 'agentsmd-import', { id: id, content: String(text) }).then(function (res) {
5466
5832
  if (res && res.ok) done.push(res.id || id)
5467
5833
  else failed.push(String(file.name) + '(' + ((res && res.error) || '') + ')')
5468
5834
  })
@@ -5484,6 +5850,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5484
5850
  // 手动刷新(含首次加载)要把按钮切到「刷新中…」——点完毫无反馈等于没点。
5485
5851
  // 页面内部那些「操作完顺手重读」的调用传 silent,免得按钮每次都闪一下。
5486
5852
  if (!silent) setState(function (s) { return Object.assign({}, s, { loading: true }) })
5853
+ refreshQuick()
5487
5854
  apiCall('agentsmd-list', {}).then(function (res) {
5488
5855
  // 刷新即清掉上一次的「拒绝」说明(它解释的是那一次点击,不是常驻状态)。
5489
5856
  setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')), presets: (res && res.presets) || [], current: s.current, scenePrompt: (res && res.scenePrompt) || null, anyLocked: (res && res.anyLocked) === true, deny: res && res.ok ? null : s.deny } })
@@ -5575,14 +5942,25 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5575
5942
  else refresh()
5576
5943
  }).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
5577
5944
  }
5578
- function doCreate(id, from, content, description) {
5945
+ function doCreate(id, content, description) {
5579
5946
  beginAction()
5580
5947
  setBusy('create')
5581
- // content 优先(用户直接写的内容);留空才走 from 复制;都没有 = 宿主空模板。
5948
+ if (createModal && createModal.kind === 'quick') {
5949
+ // 快捷提示词:正文原样存。没有"空模板"也没有"从现有复制"——那两个概念都属于
5950
+ // 会覆盖全局 AGENTS.md 的基线预设,套到快捷词上只会让人以为贴进对话框的文字会被注入。
5951
+ apiCall('quickprompt-create', {
5952
+ id: id, content: String(content || ''), description: String(description == null ? '' : description),
5953
+ }).then(function (res) {
5954
+ setBusy(null)
5955
+ if (res && res.ok) { setCreateModal(null); refreshQuick() }
5956
+ else if (res) setCreateModal(Object.assign({}, createModal, { error: res.error }))
5957
+ }).catch(function () { setBusy(null) })
5958
+ return
5959
+ }
5960
+ // 留空 = 宿主那一份空白模板(`agentsmd-create` 自己的默认)。
5582
5961
  apiCall('agentsmd-create', {
5583
5962
  id: id,
5584
5963
  ...(content ? { content: content } : {}),
5585
- ...(from ? { from: from } : {}),
5586
5964
  // 描述存 meta.json,**不写进 AGENTS.md**(那个文件的正文会被原样注入)。
5587
5965
  // 0.14.0 起模型也在 `prompt_manager_list` 的清单里看得到它,所以有了字数上限
5588
5966
  // (输入框那枚 maxLength / 计数就是它)。
@@ -5605,16 +5983,53 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5605
5983
  } else if (res) setEditModal(Object.assign({}, editModal, { error: res.error }))
5606
5984
  }).catch(function () { setBusy(null) })
5607
5985
  }
5608
- /** 「或从现有预设复制」:把选中预设的正文读进表单,之后仍可自由编辑。 */
5609
- function loadFromPreset(target, id) {
5610
- if (!id) { if (target === 'create') setCreateModal(Object.assign({}, createModal, { from: '' })); return }
5611
- apiCall('agentsmd-read', { id: id }).then(function (res) {
5612
- if (!res || !res.ok) return
5613
- if (target === 'create') setCreateModal(Object.assign({}, createModal, { from: id, content: res.content }))
5614
- else actionFailed(res)
5615
- }).catch(actionFailed)
5986
+ /**
5987
+ * 快捷提示词的编辑与删除。
5988
+ *
5989
+ * 打开编辑**不再发一次读请求**:`quickprompt-list` 连正文一起回(服务端那份清单的
5990
+ * 理由见 `src/prompts/quick-service.ts` 头注 —— `/` 菜单点的时候要当场有字可插),
5991
+ * 预设那边要单独 `agentsmd-read` 是因为它的 list 只有元信息。
5992
+ * 改名也不牵连别处:快捷提示词不被场景绑定、不被引用,改完就是改完。
5993
+ */
5994
+ function openQuickEdit(p) {
5995
+ beginAction()
5996
+ setQuickEdit({ id: p.id, nextId: p.id, content: p.content || '', description: p.description || '' })
5997
+ }
5998
+ function doQuickUpdate() {
5999
+ beginAction()
6000
+ setBusy('quick-update')
6001
+ apiCall('quickprompt-update', {
6002
+ id: quickEdit.id, nextId: String(quickEdit.nextId || quickEdit.id),
6003
+ content: quickEdit.content, description: String(quickEdit.description == null ? '' : quickEdit.description),
6004
+ }).then(function (res) {
6005
+ setBusy(null)
6006
+ if (res && res.ok) { setQuickEdit(null); refreshQuick() }
6007
+ else if (res) setQuickEdit(Object.assign({}, quickEdit, { error: res.error }))
6008
+ }).catch(function () { setBusy(null) })
6009
+ }
6010
+ function doQuickRemove(id) {
6011
+ beginAction()
6012
+ setBusy('quick-remove')
6013
+ apiCall('quickprompt-remove', { id: id }).then(function (res) {
6014
+ setBusy(null); setQuickConfirm(null)
6015
+ if (res && res.ok) refreshQuick()
6016
+ else if (res) actionFailed(res)
6017
+ }).catch(function (e) { setBusy(null); setQuickConfirm(null); actionFailed(e) })
6018
+ }
6019
+ /**
6020
+ * 那颗开关:只改"这条在不在对话框 `/` 菜单里出现",正文与备注一个字不动。
6021
+ * 与子智能体那一档同一条口径:成功不弹提示(列表重读后开关自己翻过来),失败要说人话。
6022
+ */
6023
+ function toggleQuick(p) {
6024
+ beginAction()
6025
+ const next = p.enabled === false
6026
+ setBusy('quick-toggle-' + p.id)
6027
+ apiCall('quickprompt-toggle', { id: p.id, enabled: next }).then(function (res) {
6028
+ setBusy(null)
6029
+ if (res && res.ok) refreshQuick()
6030
+ else if (res) actionFailed(res)
6031
+ }).catch(function (e) { setBusy(null); actionFailed(e) })
5616
6032
  }
5617
-
5618
6033
  // 只有「还没有任何内容」时才让加载占位顶掉列表:手动点刷新时列表不该凭空消失,
5619
6034
  // 反馈由刷新按钮自己的「刷新中…」承担(与记忆页 / 技能页同一口径)。
5620
6035
  // 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
@@ -5629,6 +6044,102 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5629
6044
  var activeCount = React.useMemo(function () {
5630
6045
  return state.presets.filter(function (p) { return p.active }).length
5631
6046
  }, [state.presets])
6047
+ var visibleQuick = React.useMemo(function () {
6048
+ return quick.prompts.filter(function (p) { return matchPresetQuery(p, dq) })
6049
+ }, [quick.prompts, dq])
6050
+ // 与上面预设那一档同一条口径:亮着的排前面。关掉的照样列着(那只是"不在 `/` 菜单里出现",
6051
+ // 不是删掉),沉到底部是为了让常用的那几条留在第一眼。
6052
+ var orderedQuick = React.useMemo(function () {
6053
+ return enabledFirst(visibleQuick, function (p) { return p.enabled !== false })
6054
+ }, [visibleQuick])
6055
+ // ── 两组都是**可折叠卡片**(用户 2026-10-01 裁定:做成技能那样)──────────
6056
+ // 默认展开;打了字强制展开 —— 卡片收着的话搜索结果一行都看不见(技能页 / MCP 页同口径)。
6057
+ var cg = React.useState({})
6058
+ var groupsCollapsed = cg[0], setGroupsCollapsed = cg[1]
6059
+ function toggleGroup(key) {
6060
+ // 翻的是**收起标记本身**:`groupsCollapsed[key] === true` 才是收着的。
6061
+ // 写成 `!(groupsCollapsed[key] !== true)` 的话,展开态下算出来是 `false`,
6062
+ // 而 `false` 在这张表里的意思正是"没收起" —— 每次点击把同一个值写回去,卡片收不起来。
6063
+ setGroupsCollapsed(function (prev) { return Object.assign({}, prev, { [key]: prev[key] !== true }) })
6064
+ }
6065
+ /** 一张分组卡片:标题 + 条数(点标题行收起/展开)+ 右侧动作 + 展开时的卡片体。 */
6066
+ function groupCard(key, label, count, actions, bodyNode) {
6067
+ var open = groupsCollapsed[key] !== true || !!dq
6068
+ return React.createElement('section', { key: key, className: 'dsm-source' },
6069
+ React.createElement('div', { className: 'dsm-source-head' },
6070
+ React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleGroup(key) } },
6071
+ React.createElement('span', { className: 'dsm-source-title', title: label }, label),
6072
+ React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: count }))),
6073
+ actions ? React.createElement('div', { className: 'dsm-source-actions' }, actions) : null),
6074
+ open ? React.createElement('div', { className: 'dsm-source-body' }, bodyNode) : null)
6075
+ }
6076
+ /**
6077
+ * 快捷那组的「全选 / 取消全选」:只作用于**当前可见**的那几条(与技能页那颗同一条口径),
6078
+ * 目标集 = 状态与"这一组现在是不是全开"相反的行。点完顺带把卡片展开 —— 收着的话
6079
+ * 到底改了哪几条看不见。
6080
+ *
6081
+ * 全局那一组**不给**这颗按钮(用户 2026-10-01 明确裁定):那一档是单选,
6082
+ * "把所有基线都应用"根本没有意义 —— 应用一份就是关掉其余每一份。
6083
+ */
6084
+ function toggleQuickBulk() {
6085
+ var list = visibleQuick
6086
+ if (!list.length) return
6087
+ var turnOn = !list.every(function (p) { return p.enabled !== false })
6088
+ var targets = list.filter(function (p) { return (p.enabled !== false) !== turnOn })
6089
+ if (!targets.length) return
6090
+ beginAction()
6091
+ setGroupsCollapsed(Object.assign({}, groupsCollapsed, { quick: false }))
6092
+ setBusy('quick-bulk')
6093
+ // 逐条走同一个 `quickprompt-toggle`(不新增批量 op):每条都要能被场景档案同步跟上
6094
+ // (登记表里那颗 syncsArchive),自己拼一条批量通道就会把这件事漏掉。
6095
+ Promise.all(targets.map(function (p) {
6096
+ return apiCall('quickprompt-toggle', { id: p.id, enabled: turnOn })
6097
+ .catch(function (e) { return { ok: false, error: errMsg(e) } })
6098
+ })).then(function (all) {
6099
+ setBusy(null)
6100
+ refreshQuick()
6101
+ var bad = all.filter(function (r) { return !r || r.ok !== true }).length
6102
+ if (bad) setState(function (s) { return Object.assign({}, s, { notice: { kind: 'err', text: t('prompts.bulk.failed', { done: all.length - bad, failed: bad }) } }) })
6103
+ })
6104
+ }
6105
+ var quickAllOn = visibleQuick.length > 0 && visibleQuick.every(function (p) { return p.enabled !== false })
6106
+ var quickBulkButton = React.createElement('button', {
6107
+ type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk',
6108
+ disabled: busy !== null || state.anyLocked === true || !visibleQuick.length,
6109
+ title: t('prompts.bulk.quick'),
6110
+ onClick: toggleQuickBulk,
6111
+ }, bulkPair(t(quickAllOn ? 'bulk.unselectAll' : 'bulk.selectAll'), t(quickAllOn ? 'bulk.selectAll' : 'bulk.unselectAll')))
6112
+ function renderQuick(p) {
6113
+ return React.createElement('div', { key: p.id, className: 'dsm-source', 'data-flip-key': 'q:' + p.id },
6114
+ React.createElement('div', { className: 'dsm-source-head' },
6115
+ // 与子智能体行同一形状:名字一行、备注一行,都在头部里 —— 备注不再单独占一条
6116
+ // 带分隔线的卡片底(用户 2026-09-30 要求少占空间)。
6117
+ // 不挂「生效中」那类标签:快捷提示词不写 AGENTS.md、不进上下文,它唯一的两种状态
6118
+ // (在不在 `/` 菜单里出现)就是左边那颗开关。
6119
+ React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
6120
+ React.createElement('div', { className: 'dsm-persona-name-row' },
6121
+ React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id)),
6122
+ p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
6123
+ React.createElement('div', { className: 'dsm-source-actions' },
6124
+ React.createElement(Switch, {
6125
+ on: p.enabled !== false,
6126
+ disabled: busy !== null || state.anyLocked === true,
6127
+ label: t('quickprompt.toggle') + ' ' + p.id,
6128
+ title: t('quickprompt.toggle.hint'),
6129
+ onClick: function () { toggleQuick(p) } }),
6130
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openQuickEdit(p) } }, t('agm.btn.edit')),
6131
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: state.anyLocked === true ? t('scenes.lock.blockedEdit') : '', onClick: function () { setQuickConfirm({ id: p.id }) } }, t('agm.btn.delete')))))
6132
+ }
6133
+ // 快捷提示词那一组。整段独立于上面的 `body`:它读失败只红自己那一组,
6134
+ // 不该把预设列表一起打成错误页(两件事各自有各自的结论)。
6135
+ var quickSection = groupCard('quick', t('prompts.group.quick'), quick.prompts.length, quickBulkButton,
6136
+ quick.error
6137
+ ? React.createElement(Notice, { kind: 'err', text: quick.error })
6138
+ : !quick.prompts.length
6139
+ ? React.createElement('div', { className: 'dsm-empty' }, t('quickprompt.empty'))
6140
+ : !visibleQuick.length
6141
+ ? React.createElement('div', { className: 'dsm-empty' }, t('agm.empty.search'))
6142
+ : React.createElement('div', { className: 'dsm-sources' }, orderedQuick.map(renderQuick)))
5632
6143
  var body = state.loading && !state.presets.length
5633
6144
  ? React.createElement('div', { className: 'dsm-empty' }, t('agm.loading'))
5634
6145
  : state.error
@@ -5645,7 +6156,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5645
6156
  var restoreRef = refs.some(function (r) { return r.kind === 'restore' })
5646
6157
  var refTexts = refsOf(p)
5647
6158
  // 场景绑的就是它,但 AGENTS.md 里的内容不是它(被手改过?)→ 说清楚,
5648
- // 并留「重新应用」这条路写回。
6159
+ // 并留"再点一次那颗开关"这条路写回。
5649
6160
  var fileMismatch = !!sceneDriver && sceneDriver.presetId === p.id && !p.fileApplied
5650
6161
  // 场景接管的是**别的**预设时,应用这一份 = 把该场景改绑到它(用户裁定
5651
6162
  // 2026-09-17:未锁定时切换要可用,并且同步写进场景档案),所以不再拦截;
@@ -5653,23 +6164,38 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5653
6164
  var applyRebinds = !!sceneDriver && sceneDriver.presetId !== p.id
5654
6165
  return React.createElement('div', { key: p.id, className: 'dsm-source', 'data-flip-key': p.id, 'data-flip-on': p.active ? '1' : '0' },
5655
6166
  React.createElement('div', { className: 'dsm-source-head' },
5656
- React.createElement('div', { className: 'dsm-source-head-main' },
5657
- React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id),
5658
- p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.label) || (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
5659
- !p.active && p.fileApplied ? React.createElement('span', { className: 'dsm-tag', title: t('agm.file.applied.hint') }, t('agm.file.applied')) : null,
5660
- fileMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('agm.file.mismatch.hint') }, t('agm.file.mismatch')) : null,
5661
- // 场景绑定**不在这里标标签**(用户裁定):多场景绑同一份时标签会把行挤爆;
5662
- // 谁在引用它,点「删除」时会逐条说出来(`agm.btn.delete.blocked.refs`)。
5663
- restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
6167
+ // 与子智能体行同一形状:名字(带标签)一行、备注一行,都在头部里 ——
6168
+ // 备注不再单独占一条带分隔线的卡片底(用户 2026-09-30 要求少占空间)。
6169
+ React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
6170
+ React.createElement('div', { className: 'dsm-persona-name-row' },
6171
+ React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id),
6172
+ p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.label) || (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
6173
+ !p.active && p.fileApplied ? React.createElement('span', { className: 'dsm-tag', title: t('agm.file.applied.hint') }, t('agm.file.applied')) : null,
6174
+ fileMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('agm.file.mismatch.hint') }, t('agm.file.mismatch')) : null,
6175
+ // 场景绑定**不在这里标标签**(用户裁定):多场景绑同一份时标签会把行挤爆;
6176
+ // 谁在引用它,点「删除」时会逐条说出来(`agm.btn.delete.blocked.refs`)。
6177
+ restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
6178
+ p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
5664
6179
  React.createElement('div', { className: 'dsm-source-actions' },
6180
+ // 「应用 / 重新应用」换成与子智能体同一颗药丸开关(用户 2026-09-30 裁定):
6181
+ // 那一档是单选,屏幕上永远只有一份生效中 —— 开关说的正是这件事,而两颗文字
6182
+ // 按钮把"当前是哪份"留给标签去猜。关不掉是**语义**不是缺功能:基线没有
6183
+ // "全部不应用"这一态,所以点已亮着的那颗 = 重新写回(把被手改的 AGENTS.md
6184
+ // 拉回来),与旧「重新应用」同一个出口,确认弹窗也照旧。
6185
+ React.createElement(Switch, {
6186
+ on: p.active === true,
6187
+ disabled: busy !== null || state.anyLocked === true,
6188
+ label: t('agm.apply.switch') + ' ' + p.id,
6189
+ title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel })
6190
+ : p.active ? t('agm.apply.switch.on') : t('agm.apply.hint'),
6191
+ onClick: function () { setApplyConfirm({ id: p.id }) } }),
5665
6192
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
5666
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.anyLocked === true, onClick: function () { setApplyConfirm({ id: p.id }) }, title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel }) : t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
5667
6193
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: refTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
5668
6194
  // 被引用的预设**点得动但删不掉**:当面说出「谁在用」,而不是让用户对着
5669
6195
  // 禁用按钮猜(disabled 的按钮连 title 都不弹)。宿主侧同样拒绝,这里只是先说。
5670
6196
  if (refTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) }) }); return }
5671
6197
  setApplyConfirm({ id: p.id, remove: true })
5672
- } }, t('agm.btn.delete')))), p.description ? React.createElement("div", { key: "desc", className: "dsm-source-note", title: p.description }, p.description) : null)
6198
+ } }, t('agm.btn.delete')))))
5673
6199
  }))
5674
6200
 
5675
6201
  return React.createElement('section', { className: 'dsm-section' },
@@ -5680,9 +6206,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5680
6206
  React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc'))),
5681
6207
  React.createElement('div', { className: 'dsm-actions' },
5682
6208
  refreshButton(t, state.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: state.loading, onClick: function () { refresh() } }),
5683
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({ id: '', from: '', content: '', description: '', error: null }) } }, t('agm.btn.new')),
6209
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({ kind: 'global', id: '', content: '', description: '', error: null }) } }, t('agm.btn.new')),
5684
6210
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.anyLocked === true, onClick: function () { setImportResult(null); setImportOpen(true) } }, t('agm.btn.import')),
5685
- React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !state.presets.length, onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
6211
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !(state.presets.length || quick.prompts.length), onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
5686
6212
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
5687
6213
  state.anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
5688
6214
  // 场景接管时把「为什么应用按钮点不动」直接写出来,而不是让用户对着置灰的按钮猜。
@@ -5693,8 +6219,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5693
6219
  (state.anyLocked !== true && sceneDriver) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('agm.sceneLock.notice', { scene: sceneDriverLabel, id: sceneDriver.presetId })) : null,
5694
6220
  // 一次性拒绝说明(被引用的预设删不掉):不替换列表,只加一条横幅。
5695
6221
  state.deny ? React.createElement(Notice, { kind: 'warn', text: state.deny }) : null,
5696
- React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '2' } },
5697
- [['total', state.presets.length, t('agm.stat.total')], ['active', activeCount, t('agm.stat.active')]].map(function (item) {
6222
+ // 三枚指标:两类各自计数 + 生效中。第一枚的文案点明「全局」—— 页面上现在有两类
6223
+ // 提示词,只说「个提示词」会让用户以为快捷那几条没被算进来。
6224
+ React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '3' } },
6225
+ [['total', state.presets.length, t('agm.stat.total')], ['active', activeCount, t('agm.stat.active')],
6226
+ ['quick', quick.prompts.length, t('quickprompt.stat')]].map(function (item) {
5698
6227
  return React.createElement('div', { key: item[0], className: 'dsm-stat' },
5699
6228
  React.createElement('strong', null, item[1]), item[2])
5700
6229
  })),
@@ -5706,28 +6235,50 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5706
6235
  placeholder: t('agm.search.placeholder'),
5707
6236
  onChange: function (e) { setQuery(e.target.value) },
5708
6237
  })),
5709
- body,
6238
+ // 两段各一张可折叠卡片:卡片头是组名 + 条数(点它收起/展开),卡片体里才是条目。
6239
+ groupCard('global', t('prompts.group.global'), state.presets.length, null, body),
6240
+ quickSection,
5710
6241
  state.notice ? React.createElement(Notice, { kind: state.notice.kind, text: state.notice.text }) : null,
5711
6242
  exportState ? React.createElement(ExportModal, {
5712
6243
  key: 'export', t: t, title: t('export.presets'),
5713
- items: state.presets.map(function (p) { return { key: p.id, name: p.id, desc: p.description || '' } }),
6244
+ // 两类一起列、按类分组(键前缀 `q:` 标快捷):勾完一次就能把两类都带走,
6245
+ // 不必为了导另一类再开一次弹窗。
6246
+ items: state.presets.map(function (p) { return { key: p.id, name: p.id, desc: p.description || '', group: t('prompts.group.global') } })
6247
+ .concat(quick.prompts.map(function (p) { return { key: 'q:' + p.id, name: p.id, desc: p.description || '', group: t('prompts.group.quick') } })),
5714
6248
  busy: exportState.busy, result: exportState.result,
5715
6249
  onClose: function () { setExportState(null) },
5716
6250
  onSubmit: function (names, outDir) {
5717
- setExportState({ busy: true, result: null });
5718
- apiCall('bundle-export', { kind: 'presets', names: names, outDir: outDir }).then(function (res) {
5719
- if (res && res.ok) setExportState({ busy: false, result: { ok: true, text: exportResultText(t, res) } });
5720
- else setExportState({ busy: false, result: { ok: false, text: translateError(t, res) } });
5721
- }).catch(function (e) { setExportState({ busy: false, result: { ok: false, text: errMsg(e) } }) });
6251
+ setExportState({ busy: true, result: null })
6252
+ var quickIds = [], presetIds = []
6253
+ names.forEach(function (k) {
6254
+ if (String(k).indexOf('q:') === 0) quickIds.push(String(k).slice(2))
6255
+ else presetIds.push(String(k))
6256
+ })
6257
+ var calls = []
6258
+ if (presetIds.length) calls.push({ kind: 'presets', call: apiCall('bundle-export', { kind: 'presets', names: presetIds, outDir: outDir }) })
6259
+ if (quickIds.length) calls.push({ kind: 'quick-prompts', call: apiCall('bundle-export', { kind: 'quick-prompts', names: quickIds, outDir: outDir }) })
6260
+ Promise.all(calls.map(function (c) { return c.call.catch(function (e) { return { ok: false, error: errMsg(e) } }) })).then(function (all) {
6261
+ // 两类各打一个 zip(文件名里带 kind,不会互相覆盖)。两份结果都要说出来:
6262
+ // 只报第一个就是"看起来全导了",而第二个可能根本没写出去。
6263
+ var texts = all.map(function (res, i) {
6264
+ var one = res && res.ok ? exportResultText(t, res) : translateError(t, res)
6265
+ return calls.length > 1 ? t(calls[i].kind === 'presets' ? 'prompts.group.global' : 'prompts.group.quick') + ':' + one : one
6266
+ })
6267
+ var okAll = all.length > 0 && all.every(function (res) { return res && res.ok })
6268
+ setExportState({ busy: false, result: { ok: okAll, warning: okAll && all.some(function (r) { return (r.missing || []).length }) }, text: texts.join(';') })
6269
+ })
5722
6270
  },
5723
6271
  }) : null,
5724
6272
  trash ? React.createElement(TrashModal, {
5725
6273
  t: t,
5726
6274
  title: t('trash.title'),
5727
- groupTitle: t('trash.group.presets'),
5728
- groupSub: t('trash.section.presets.sub'),
6275
+ // 两组一起列:回收站是"找回被删的东西"的地方,让用户先猜它在哪个域里再切弹窗,
6276
+ // 等于把恢复这条路堵在自己手里。
6277
+ groups: [
6278
+ { title: t('trash.group.presets'), sub: t('trash.section.presets.sub'), entries: trash.presets || [] },
6279
+ { title: t('trash.group.quick'), sub: t('trash.section.quick.sub'), entries: trash.quick || [] },
6280
+ ],
5729
6281
  locked: state.anyLocked === true,
5730
- entries: trash.entries,
5731
6282
  loading: trash.loading,
5732
6283
  error: trash.error,
5733
6284
  busy: trashBusy,
@@ -5756,9 +6307,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5756
6307
  editModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, editModal.error) : null,
5757
6308
  React.createElement('div', { className: 'dsm-modal-actions' },
5758
6309
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(editModal.nextId || '').trim(), onClick: doUpdate }, t('agm.btn.save')))) : null,
5759
- // 新建:id + **正文文本框**(用户直接写内容);「从现有预设复制」是可选下拉,
5760
- // 选中后把那份正文填进文本框,仍可继续改——不再是容易被当成搜索框的文本输入。
5761
- createModal ? React.createElement(Modal, { title: t('agm.btn.new'), closeLabel: t('btn.close'), onClose: function () { setCreateModal(null) } },
6310
+ // 新建:id + 描述 + **正文文本框**(用户直接写内容)。原先还有一格「或从现有预设复制」,
6311
+ // 用户 2026-09-30 要求删掉 —— 抄一份再改是编辑页的事,新建时只想直接写。
6312
+ // 弹窗定高(`dsm-modal-fixed`):切类型时说明那一句长短不同,框不该跟着跳(手正停在
6313
+ // 下一格上),多出来的高度给正文文本框吃掉。
6314
+ createModal ? React.createElement(Modal, { title: t('agm.btn.new'), closeLabel: t('btn.close'), className: 'dsm-modal-fixed', onClose: function () { setCreateModal(null) } },
6315
+ kindPicker(createModal.kind, function (v) { setCreateModal(Object.assign({}, createModal, { kind: v, error: null })) }),
5762
6316
  React.createElement('div', { className: 'dsm-field' },
5763
6317
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
5764
6318
  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 })) } }),
@@ -5768,19 +6322,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5768
6322
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
5769
6323
  React.createElement('span', { className: 'dsm-char-count' + (String(createModal.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(createModal.description || '').length + ' / ' + PRESET_DESC_MAX)),
5770
6324
  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 })) } })),
5771
- (state.presets || []).length ? React.createElement('div', { className: 'dsm-field' },
5772
- React.createElement('label', { className: 'dsm-label' }, t('agm.field.copyFrom')),
5773
- React.createElement(SourceSelect, {
5774
- options: [{ value: '', label: t('agm.field.copyFrom.none') }].concat((state.presets || []).map(function (p) { return { value: p.id, label: p.id } })),
5775
- value: createModal.from || '',
5776
- onChange: function (v) { loadFromPreset('create', v) },
5777
- })) : null,
5778
- React.createElement('div', { className: 'dsm-field' },
6325
+ // 原先这里有一格「或从现有预设复制」—— 用户 2026-09-30 要求删掉:新建时想要的正文
6326
+ // 直接写在下面那格里,抄一份再改是编辑页的事。
6327
+ React.createElement('div', { className: 'dsm-field dsm-field-grow' },
5779
6328
  React.createElement('label', { className: 'dsm-label' }, t('agm.field.content')),
5780
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', placeholder: t('agm.field.content.placeholder'), value: createModal.content || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { content: e.target.value })) } })),
6329
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', placeholder: t(createModal.kind === 'quick' ? 'quickprompt.field.content.placeholder' : 'agm.field.content.placeholder'), value: createModal.content || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { content: e.target.value })) } })),
5781
6330
  createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
5782
6331
  React.createElement('div', { className: 'dsm-modal-actions' },
5783
- React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.from, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
6332
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
5784
6333
  applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : (t('agm.applyModal.title') + ' · ' + applyConfirm.id), closeLabel: t('btn.close'), onClose: function () { setApplyConfirm(null) } },
5785
6334
  applyConfirm.remove
5786
6335
  ? React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix'))
@@ -5792,10 +6341,35 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
5792
6341
  importOpen ? React.createElement(ImportModal, {
5793
6342
  key: 'imp', t: t, title: t('agm.btn.import'), accept: '.md',
5794
6343
  busy: importBusy, result: importResult,
6344
+ // 类型选择放在弹窗顶部(`extra` 槽):一次导入只进一类,选完再拖文件。
6345
+ extra: kindPicker(importKind, setImportKind),
5795
6346
  requirements: [t('upload.requirement.prompt.1'), t('upload.requirement.prompt.2'), t('upload.requirement.prompt.3')],
5796
6347
  onClose: function () { setImportOpen(false); setImportResult(null) },
5797
6348
  onSubmit: function (entries, files) { doImportFiles(files) },
5798
- }) : null)
6349
+ }) : null,
6350
+ // 快捷提示词的编辑:正文直接来自列表(`quickprompt-list` 连正文一起回),
6351
+ // 不像预设那样还要再发一次 read。
6352
+ quickEdit ? React.createElement(Modal, { title: t('quickprompt.edit.title') + ' · ' + quickEdit.id, closeLabel: t('btn.close'), onClose: function () { setQuickEdit(null) } },
6353
+ React.createElement('div', { className: 'dsm-field' },
6354
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
6355
+ React.createElement('input', { className: 'dsm-control', value: quickEdit.nextId || '', onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { nextId: e.target.value, error: null })) } }),
6356
+ React.createElement('p', { className: 'dsm-help' }, t('quickprompt.field.id.hint'))),
6357
+ React.createElement('div', { className: 'dsm-field' },
6358
+ React.createElement('div', { className: 'dsm-budget-meta' },
6359
+ React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
6360
+ React.createElement('span', { className: 'dsm-char-count' + (String(quickEdit.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(quickEdit.description || '').length + ' / ' + PRESET_DESC_MAX)),
6361
+ React.createElement('input', { className: 'dsm-control', maxLength: PRESET_DESC_MAX, placeholder: t('quickprompt.field.desc.placeholder'), value: quickEdit.description || '', onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { description: e.target.value })) } })),
6362
+ React.createElement('div', { className: 'dsm-field' },
6363
+ React.createElement('label', { className: 'dsm-label' }, t('quickprompt.field.content')),
6364
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', value: quickEdit.content, onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { content: e.target.value })) } })),
6365
+ quickEdit.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, quickEdit.error) : null,
6366
+ React.createElement('div', { className: 'dsm-modal-actions' },
6367
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(quickEdit.nextId || '').trim(), onClick: doQuickUpdate }, t('agm.btn.save')))) : null,
6368
+ quickConfirm ? React.createElement(Modal, { title: t('quickprompt.remove.title') + ' · ' + quickConfirm.id, closeLabel: t('btn.close'), onClose: function () { setQuickConfirm(null) } },
6369
+ React.createElement('p', { className: 'dsm-help' }, t('quickprompt.remove.hint')),
6370
+ React.createElement('div', { className: 'dsm-modal-actions' },
6371
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: function () { setQuickConfirm(null) } }, t('btn.cancel')),
6372
+ React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy !== null, onClick: function () { doQuickRemove(quickConfirm.id) } }, t('agm.btn.confirmRemove')))) : null)
5799
6373
  }
5800
6374
 
5801
6375
  // 「工具」设置页:一个侧栏项,内部 tab 切换 场景 / MCP / 技能 / 子智能体 / 提示词 / 记忆 / 会话。
@@ -6391,15 +6965,14 @@ function enabledMemoryCount(rules) {
6391
6965
  /* ── 勾选列表原语(场景档案编辑器 / 人设工具选择器 / 任何「多选一组条目」的地方共用)────
6392
6966
  原先这套排版只在 ScenesPage 内部,人设的工具选择器要么复制一份、要么写成裸 input;
6393
6967
  现在提到模块作用域,页面之间才可能真正长得一样。 */
6394
- /** 段卡片:标题栏 + 固定高度滚动体 + 脚注(高度不随内容增减变化,弹窗不跳)。 */
6968
+ /** 段卡片:标题栏 + 固定高度滚动体(高度不随内容增减变化,弹窗不跳)。 */
6395
6969
  function seg(props) {
6396
6970
  return h('div', { className: 'dsm-seg' },
6397
6971
  h('div', { className: 'dsm-seg-head' },
6398
6972
  h('span', { className: 'dsm-seg-title' }, props.title),
6399
6973
  h('span', { className: 'dsm-seg-count' }, props.count),
6400
6974
  h('div', { className: 'dsm-seg-actions' }, props.actions)),
6401
- props.body,
6402
- props.foot || null)
6975
+ props.body)
6403
6976
  }
6404
6977
  /**
6405
6978
  * 通用回收站弹窗:一行一个条目(名字 + 删除时间),右侧「恢复 / 永久删除」。
@@ -6411,7 +6984,6 @@ function enabledMemoryCount(rules) {
6411
6984
  const t = props.t
6412
6985
  const c = React.useState(null)
6413
6986
  const confirmId = c[0], setConfirmId = c[1]
6414
- const entries = props.entries || []
6415
6987
  const row = function (item) {
6416
6988
  const confirming = confirmId === item.id
6417
6989
  const actions = confirming
@@ -6432,11 +7004,26 @@ function enabledMemoryCount(rules) {
6432
7004
  }))),
6433
7005
  actions)
6434
7006
  }
6435
- const body = props.loading
6436
- ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
6437
- : entries.length === 0
6438
- ? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
6439
- : entries.map(row)
7007
+ // 组:默认就是调用方传的那一组(`groupTitle` / `entries`);给了 `groups` 就按数组列多组
7008
+ // —— 提示词页的回收站要一次看完"全局"与"快捷"两类,让用户先猜被删的东西在哪个域里,
7009
+ // 等于把恢复这条路堵在他自己手里。每组各自有空的说明,不合并成一句"什么都没删"。
7010
+ const groups = props.groups || [{ title: props.groupTitle, sub: props.groupSub, entries: props.entries || [] }]
7011
+ const renderGroup = function (g) {
7012
+ const list = g.entries || []
7013
+ const rows = props.loading
7014
+ ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
7015
+ : list.length === 0
7016
+ ? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
7017
+ : list.map(row)
7018
+ return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
7019
+ React.createElement('div', { className: 'dsm-trash-group-head' },
7020
+ React.createElement('div', { className: 'dsm-trash-group-head-row' },
7021
+ React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
7022
+ React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
7023
+ // 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
7024
+ g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null),
7025
+ React.createElement('div', { className: 'dsm-trash-group-body' }, rows))
7026
+ }
6440
7027
  // 列表类弹窗:宽高都固定(`.dsm-modal-list`),滚动只发生在 Modal 的 body 这一层 ——
6441
7028
  // 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
6442
7029
  return React.createElement(Modal, {
@@ -6448,14 +7035,7 @@ function enabledMemoryCount(rules) {
6448
7035
  onClose: props.onClose,
6449
7036
  },
6450
7037
  props.error ? React.createElement(Notice, { kind: 'err', text: props.error }) : null,
6451
- React.createElement('div', { className: 'dsm-trash-group' },
6452
- React.createElement('div', { className: 'dsm-trash-group-head' },
6453
- React.createElement('div', { className: 'dsm-trash-group-head-row' },
6454
- React.createElement('span', { className: 'dsm-trash-group-title' }, props.groupTitle),
6455
- React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: entries.length }))),
6456
- // 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
6457
- props.groupSub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, props.groupSub) : null),
6458
- React.createElement('div', { className: 'dsm-trash-group-body' }, body)))
7038
+ groups.map(renderGroup))
6459
7039
  }
6460
7040
 
6461
7041
  /** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
@@ -6480,6 +7060,15 @@ function enabledMemoryCount(rules) {
6480
7060
  h('span', null, label),
6481
7061
  h('span', { className: 'dsm-seg-group-line' }))
6482
7062
  }
7063
+ /**
7064
+ * 表单分段标题(「基本信息 / 场景绑定」这类模块切分)。
7065
+ *
7066
+ * 与 `segGroup` 的差别只在**用在哪**:那个是滚动列表内部的地标(左右各留了内缩),
7067
+ * 这个是表单第一层,必须与下面的字段左边缘对齐,所以不套那圈 padding。
7068
+ */
7069
+ function formSection(label) {
7070
+ return h('div', { className: 'dsm-form-section' }, h('span', null, label))
7071
+ }
6483
7072
  /**
6484
7073
  * 「全选 ↔ 取消全选」二合一按钮的标签对 —— fixedLabelPair 的语义化别名(同一套定宽机制)。
6485
7074
  *
@@ -6503,11 +7092,6 @@ function enabledMemoryCount(rules) {
6503
7092
  if (props.onRemove) out.push(h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel))
6504
7093
  return out
6505
7094
  }
6506
- /** 段脚注:段已定义但一项未勾 = 该域全部停用(必须显式提示,否则像没保存上)。 */
6507
- function segFoot(defined, count, emptyLabel) {
6508
- if (!defined || count > 0) return null
6509
- return h('div', { className: 'dsm-seg-foot' }, emptyLabel)
6510
- }
6511
7095
  /** 固定高度滚动体内的条目列表(空态居中提示)。 */
6512
7096
  function segList(nodes, emptyText) {
6513
7097
  if (!nodes.length) return h('div', { className: 'dsm-seg-body' }, h('div', { className: 'dsm-pick-empty' }, emptyText))
@@ -7182,7 +7766,10 @@ function enabledMemoryCount(rules) {
7182
7766
  // 胶囊配色:橙=诊断异常(要人去看),灰=没生效但不是故障(被同名覆盖、本条停用),绿=生效中。
7183
7767
  var tone = skill.shadowedBy ? "muted" : skill.loadable === false ? "warn" : enabled ? "ok" : "muted";
7184
7768
  var canToggle = root.toggleable !== false && skill.loadable !== false;
7185
- return h("div", { key: skill.name, className: "dsm-row", "data-flip-key": root.key + "/" + skill.name, "data-flip-on": enabled ? "1" : "0" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name-row" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h(StatusTag, { tone: tone, text: t(key) })), h("div", { className: "dsm-note" }, skill.description || t("note.missing")), skill.shadowedBy ? h("div", { className: "dsm-rule-hint" }, t("status.shadowed.hint", { name: skill.shadowedBy.name })) : null), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), skill.preferred === true ? h("span", { className: "dsm-tag dsm-tag-on" }, t("status.preferred")) : null, !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null, root.mutable && !root.deletable ? h("span", { className: "dsm-tag", title: t("status.notDeletable.hint") }, t("status.notDeletable")) : null), h("div", { className: "dsm-row-actions" }, canToggle && skill.shadowedBy ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.activate.title"), onClick: function () { activateShadowed(root, skill); } }, t("btn.activate")) : null, canToggle && !skill.shadowedBy ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || anyLocked, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, skill.preferred === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.unprefer.title"), onClick: function () { clearPreferred(root, skill); } }, t("btn.unprefer")) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.deletable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
7769
+ // 被覆盖 ≠ 另一份在跑:赢家自己被停用时两份都不生效,这时必须换一句实话,
7770
+ // 否则界面会一边把这一行标成「被覆盖」、一边说「同名技能 X 正在生效」(审查 F8)。
7771
+ var shadowedHint = skill.shadowedBy ? t(skill.shadowedBy.enabled === false ? "status.shadowed.inert" : "status.shadowed.hint", { name: skill.shadowedBy.name }) : null;
7772
+ return h("div", { key: skill.name, className: "dsm-row", "data-flip-key": root.key + "/" + skill.name, "data-flip-on": enabled ? "1" : "0" }, h("div", { className: "dsm-main" }, h("div", { className: "dsm-name-row" }, h("div", { className: "dsm-name" }, skill.declaredName || skill.name), h(StatusTag, { tone: tone, text: t(key) })), h("div", { className: "dsm-note" }, skill.description || t("note.missing")), shadowedHint ? h("div", { className: "dsm-rule-hint" }, shadowedHint) : null), h("div", { className: "dsm-tags" }, h("span", { className: "dsm-tag" }, rootDisplayName(t, root)), skill.preferred === true ? h("span", { className: "dsm-tag dsm-tag-on" }, t("status.preferred")) : null, !root.mutable ? h("span", { className: "dsm-tag" }, t("status.readonly")) : null, root.mutable && !root.deletable ? h("span", { className: "dsm-tag", title: t("status.notDeletable.hint") }, t("status.notDeletable")) : null), h("div", { className: "dsm-row-actions" }, canToggle && skill.shadowedBy ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.activate.title"), onClick: function () { activateShadowed(root, skill); } }, t("btn.activate")) : null, canToggle && !skill.shadowedBy ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || anyLocked, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, skill.preferred === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", disabled: busy, title: t("btn.unprefer.title"), onClick: function () { clearPreferred(root, skill); } }, t("btn.unprefer")) : null, h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.deletable === true ? h("button", { type: "button", className: "dsm-btn dsm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
7186
7773
  }
7187
7774
  function renderRoot(root) {
7188
7775
  if (activeSource && activeSource !== root.key) return null;
@@ -7914,7 +8501,7 @@ function enabledMemoryCount(rules) {
7914
8501
  accept: '.jsonl,.json,.md,.markdown,.txt',
7915
8502
  busy: imp.busy === true,
7916
8503
  requirements: [t('upload.requirement.session.1'), t('upload.requirement.session.2'), t('upload.requirement.session.3')],
7917
- result: imp.error ? { ok: false, text: String(imp.error) } : (imp.result ? { ok: true, text: t('hist.import.done', { id: imp.result.sessionId, count: imp.result.count }) } : null),
8504
+ result: imp.error ? { ok: false, text: String(imp.error) } : (imp.result ? { ok: true, warning: imp.result.lossy === true, text: t('hist.import.done', { id: imp.result.sessionId, count: imp.result.count }) + (imp.result.lossy === true ? ' ' + t('hist.import.lossy') : '') } : null),
7918
8505
  extra: React.createElement('div', null,
7919
8506
  React.createElement('div', { className: 'dsm-field' },
7920
8507
  React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
@@ -8109,6 +8696,9 @@ function enabledMemoryCount(rules) {
8109
8696
  // 工具表方案下拉的数据源(C1;按用户 2026-09-28 裁定从档案编辑器移进「编辑场景」表单)。
8110
8697
  var tts = React.useState(null)
8111
8698
  var ttInfo = tts[0], setTtInfo = tts[1]
8699
+ // 快捷提示词候选(同一族:勾选入口在「修改场景」表单里,档案编辑器不带这一栏)。
8700
+ var qps = React.useState([])
8701
+ var quickOptions = qps[0], setQuickOptions = qps[1]
8112
8702
  var dts = React.useState(null)
8113
8703
  var drillTools = dts[0], setDrillTools = dts[1]
8114
8704
  // 场景回收站(记录 + 档案):null = 关闭;{loading, error, entries}
@@ -8200,10 +8790,14 @@ function enabledMemoryCount(rules) {
8200
8790
  function agentsMdNote(res) {
8201
8791
  var sync = res && res.agentsMd
8202
8792
  if (!sync) return ''
8203
- if (sync.error) return ' · ' + t('scenes.agents.note.error', { reason: String(sync.error) })
8204
- if (sync.applied) return ' · ' + t('scenes.agents.note.applied', { id: String(sync.applied) })
8205
- if (sync.restored) return ' · ' + t('scenes.agents.note.restored')
8206
- return ''
8793
+ var note = ''
8794
+ if (sync.error) note += ' · ' + t('scenes.agents.note.error', { reason: String(sync.error) })
8795
+ else if (sync.applied) note += ' · ' + t('scenes.agents.note.applied', { id: String(sync.applied) })
8796
+ else if (sync.restored) note += ' · ' + t('scenes.agents.note.restored')
8797
+ // 「基线快照没能写入」与「AGENTS.md 有没有写」是两件事:前者是**切进去了但退不回来**,
8798
+ // 必须单独说,否则用户以为还能退出场景恢复原样(审查 F14)。
8799
+ if (sync.baselineError) note += ' · ' + t('scenes.agents.note.baselineLost', { reason: String(sync.baselineError) })
8800
+ return note
8207
8801
  }
8208
8802
  // ── 场景启用:**除「全局」外同时只能启用一个**(用户裁定)──
8209
8803
  // 空数组 = 全部关闭(只留恒常的 `_shared` 与 `global`);服务端也会拒绝多个。
@@ -8212,7 +8806,7 @@ function enabledMemoryCount(rules) {
8212
8806
  setBusy(true); setResult(null)
8213
8807
  apiCall('rules-set-active', { scenes: names }).then(function (res) {
8214
8808
  setBusy(false)
8215
- if (res && res.ok) { setResult({ ok: !(res.agentsMd && res.agentsMd.error), text: t('memory.result.active') + agentsMdNote(res) }); refresh(true) }
8809
+ if (res && res.ok) { setResult({ ok: !(res.agentsMd && (res.agentsMd.error || res.agentsMd.baselineError)), text: t('memory.result.active') + agentsMdNote(res) }); refresh(true) }
8216
8810
  else setResult({ ok: false, text: translateError(t, res) })
8217
8811
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
8218
8812
  }
@@ -8265,15 +8859,23 @@ function enabledMemoryCount(rules) {
8265
8859
  setSceneForm(function (f) { return (f && String(f.prompt || '') !== '') ? f : Object.assign({}, f, { prompt: activeId }) })
8266
8860
  }
8267
8861
  // ── 场景建 / 改描述 / 改绑定的提示词 / 删 ──
8268
- function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets() }
8862
+ // `quickPrompts` 三态:null = 「当前启用的」(这一域不绑、进出不碰);数组 = 按场景勾选
8863
+ // (**空数组也是"绑了"**,意思是进入后全部停用)。表单里那颗下拉只给这两个方向。
8864
+ function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', quickPrompts: null, error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets(); loadQuickOptions() }
8269
8865
  function openEditScene(scene) {
8270
8866
  var sceneArchive = (data.archives || {})[scene.name] || {}
8271
- setSceneForm({ name: scene.name, description: scene.description || '', prompt: scene.prompt || '', toolTablePreset: typeof sceneArchive.toolTablePreset === 'string' ? sceneArchive.toolTablePreset : '', error: null })
8867
+ setSceneForm({
8868
+ name: scene.name, description: scene.description || '', prompt: scene.prompt || '',
8869
+ toolTablePreset: typeof sceneArchive.toolTablePreset === 'string' ? sceneArchive.toolTablePreset : '',
8870
+ quickPrompts: Array.isArray(sceneArchive.quickPrompts) ? sceneArchive.quickPrompts.slice() : null,
8871
+ error: null,
8872
+ })
8272
8873
  setModal({ type: 'scene-edit', name: scene.name })
8273
8874
  // 该场景还没绑定时,默认选中当前生效的那份(用户裁定「默认就是当前启动的」)。
8274
8875
  if (!scene.prompt) loadPresetOptions(defaultPromptToActive)
8275
8876
  else loadPresetOptions()
8276
8877
  loadToolTablePresets()
8878
+ loadQuickOptions()
8277
8879
  }
8278
8880
  /** 新建与编辑共用一个表单:字段相同,只是分别走 rules-create-scene / rules-update-scene。 */
8279
8881
  function submitSceneForm() {
@@ -8288,9 +8890,13 @@ function enabledMemoryCount(rules) {
8288
8890
  // 工具表方案绑定(C1)单独一个参数:空串 = 解绑;服务端写进档案,
8289
8891
  // 改的是启用中的场景时会立即应用那份方案(见 scene-sync 的包装)。
8290
8892
  var toolTablePreset = String(sceneForm.toolTablePreset || '')
8893
+ // 快捷提示词:null = 解绑(回到"保持现状"),数组 = 勾选集(空数组是"一条都不勾")。
8894
+ // 两个方向都必须**显式送出去**,不能省略 —— 省略在服务端那边是"这一域不碰"。
8895
+ var quickPrompts = sceneForm.quickPrompts === null || sceneForm.quickPrompts === undefined
8896
+ ? null : (sceneForm.quickPrompts || []).slice()
8291
8897
  var payload = isEdit
8292
- ? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset }
8293
- : { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset }
8898
+ ? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
8899
+ : { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
8294
8900
  apiCall(isEdit ? 'rules-update-scene' : 'rules-create-scene', payload).then(function (res) {
8295
8901
  setBusy(false)
8296
8902
  if (res && res.ok) {
@@ -8345,6 +8951,15 @@ function enabledMemoryCount(rules) {
8345
8951
  } })
8346
8952
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
8347
8953
  }
8954
+ /**
8955
+ * 快捷提示词候选(只读 op,没填令牌也拿得到)。读不到就当没有:下拉仍给
8956
+ * 「当前启用的(保持现状)」这一项,只是切到「按场景勾选」时列表是空的。
8957
+ */
8958
+ function loadQuickOptions() {
8959
+ apiCall('quickprompt-list', {}).then(function (res) {
8960
+ setQuickOptions((res && res.ok && Array.isArray(res.prompts)) ? res.prompts : [])
8961
+ }).catch(function () { setQuickOptions([]) })
8962
+ }
8348
8963
  /**
8349
8964
  * 工具表方案下拉的数据源(C1):只读调用(不带 `set:true`),
8350
8965
  * 所以没填令牌也能拿。读不到就当没有(下拉只剩「不绑定」+「出厂默认」)。
@@ -8401,8 +9016,8 @@ function enabledMemoryCount(rules) {
8401
9016
  * 取 2026-09-13 前的「预勾当前运行时状态」是错的方向:用户点「添加」只是想开始配,
8402
9017
  * 不是想把自己现有的启用状态抄进档案。代价要讲清楚——勾选集语义是「勾 = 启用」,
8403
9018
  * 空段 = 全部停用(未定义段也是同一个意思,见 archive-engine 的四域同口径),
8404
- * 所以「添加 MCP 工具集」当下就等价于「该场景下全部 MCP 停用」,
8405
- * 段脚注(`memory.archive.emptySection`)与弹窗顶部说明会同时把这句话显示出来。
9019
+ * 所以「添加 MCP 工具集」当下就等价于「该场景下全部 MCP 停用」。段脚注原先把这句话
9020
+ * 印在每一段底下,2026-09-30 用户裁定撤掉(四段同屏时同一件事说了四遍)。
8406
9021
  */
8407
9022
  function emptyMcpPreset() { return {} }
8408
9023
  /** 全选:列出全部服务器(含未运行的)并各勾「全部工具」。 */
@@ -8425,13 +9040,6 @@ function enabledMemoryCount(rules) {
8425
9040
  onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear, allChecked: allChecked,
8426
9041
  })
8427
9042
  }
8428
- /**
8429
- * 段脚注:段已定义但一项未勾必须显式提示,否则像「没保存上」。
8430
- * 各域的后果不同(MCP/技能/记忆 = 全部停用;子智能体 = 不限制),所以文案由调用方给。
8431
- */
8432
- function archiveSegFoot(defined, count, emptyLabel) {
8433
- return segFoot(defined, count, emptyLabel || t('memory.archive.emptySection'))
8434
- }
8435
9043
 
8436
9044
  // ── 段 4:记忆(P5:单一真相源 = `rules[*].enabled`)────────────────────
8437
9045
  // 这个段**没有自己的「段定义」概念**:勾选状态**就是**记忆自身的 `enabled`,
@@ -8583,7 +9191,6 @@ function enabledMemoryCount(rules) {
8583
9191
  }))
8584
9192
  })())
8585
9193
  : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mcp.hint'))),
8586
- foot: archiveSegFoot(defined, defined ? Object.keys(sections.mcp).length : 0),
8587
9194
  })
8588
9195
  }
8589
9196
  /**
@@ -8733,7 +9340,6 @@ function enabledMemoryCount(rules) {
8733
9340
  }))
8734
9341
  : React.createElement('div', { className: 'dsm-pick-empty' }, modal.skillQuery ? t('scenes.mem.noMatch') : t('scenes.skills.empty')))
8735
9342
  : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.skills.hint'))),
8736
- foot: archiveSegFoot(defined, defined ? pickedCount : 0),
8737
9343
  })
8738
9344
  }
8739
9345
  /** 段 3:子智能体绑定。 */
@@ -8808,7 +9414,6 @@ function enabledMemoryCount(rules) {
8808
9414
  }))
8809
9415
  : React.createElement('div', { className: 'dsm-pick-empty' }, modal.subQuery ? t('scenes.mem.noMatch') : t('scenes.subagents.empty')))
8810
9416
  : React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.subagents.hint'))),
8811
- foot: archiveSegFoot(defined, defined ? sections.subagents.length : 0, t('memory.archive.emptySubagents')),
8812
9417
  })
8813
9418
  }
8814
9419
  /**
@@ -8973,6 +9578,10 @@ function enabledMemoryCount(rules) {
8973
9578
  pushRow('scenes.preview.row.skillsOff', sk.off, true)
8974
9579
  pushRow('scenes.preview.row.personasOn', sub.on)
8975
9580
  pushRow('scenes.preview.row.personasOff', sub.off)
9581
+ // 快捷提示词:只有绑了才出行(没绑 = 这一域进都不进,服务端给的就是 null)。
9582
+ var qp = plan.quickPrompts || {}
9583
+ pushRow('scenes.preview.row.quickOn', qp.on)
9584
+ pushRow('scenes.preview.row.quickOff', qp.off)
8976
9585
  // 工具表方案(C1):绑了方案就是一句「会切成哪一份」,没绑不出行。
8977
9586
  // 方案已经不存在时(hiddenCount 为 null)说"这次不切换",不报一个看不懂的数字。
8978
9587
  if (plan.toolPreset) {
@@ -9095,6 +9704,9 @@ function enabledMemoryCount(rules) {
9095
9704
  if (archive.mcp) parts.push(t('scenes.profile.mcp', { count: Object.keys(archive.mcp).length }))
9096
9705
  if (Array.isArray(archive.skills)) parts.push(t('scenes.profile.skills', { count: archive.skills.length }))
9097
9706
  if (Array.isArray(archive.subagents)) parts.push(t('scenes.profile.subagents', { count: archive.subagents.length }))
9707
+ // 快捷提示词绑了才报(没绑 = 这一域不碰)。**空表也要报** —— 它说的是"进入后全部停用",
9708
+ // 与"没绑"是两件事,漏掉就等于把用户的一个真实选择显示成没有这个选择。
9709
+ if (Array.isArray(archive.quickPrompts)) parts.push(t('scenes.profile.quickPrompts', { count: archive.quickPrompts.length }))
9098
9710
  // P5:记忆**不再是档案的一部分**(开关是 `rules[*].enabled` 单一真相源),
9099
9711
  // 所以这里不再统计 memories —— 否则会显示一个已经不存在的段的数量。
9100
9712
  return parts.join(' · ')
@@ -9222,6 +9834,9 @@ function enabledMemoryCount(rules) {
9222
9834
  })),
9223
9835
  modal && (modal.type === 'scene-create' || modal.type === 'scene-edit') ? React.createElement(Modal, { key: 'screate', title: modal.type === 'scene-create' ? t('memory.scene.createTitle') : t('memory.scene.editTitle'), closeLabel: t('btn.close'), onClose: function () { setModal(null) } },
9224
9836
  React.createElement('div', { className: 'dsm-form' },
9837
+ // 弹窗里的字段分两模块:改名 / 描述是"这个场景是什么",下面三条是"进入时改什么"。
9838
+ // 一屏六行同一种字重、同一种间距时,用户读不出哪几条是一回事(用户 2026-10-01 反馈)。
9839
+ formSection(t('scenes.form.section.basic')),
9225
9840
  React.createElement('label', { className: 'dsm-field' },
9226
9841
  React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.name')),
9227
9842
  React.createElement('input', {
@@ -9247,16 +9862,77 @@ function enabledMemoryCount(rules) {
9247
9862
  onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { description: e.target.value, error: null })) },
9248
9863
  })),
9249
9864
  // 提示词预设:一个场景**只能绑一个**(单值字段天然单选);「不绑定」= 解绑。
9250
- React.createElement('label', { className: 'dsm-field' },
9865
+ formSection(t('scenes.form.section.binding')),
9866
+ // 下面三格都是「下拉 + 说明」这一类复合字段,外层用 `div` 而不是 `label`:
9867
+ // 不带 `for` 的 label 会把**第一个可标注后代**当成自己的控件 —— 点标题文字就等于
9868
+ // 点那颗下拉,而在快捷那一格里,第一个控件是勾选列表里的第一颗复选框
9869
+ // (点段头的空白会把那条勾翻掉)。纯文本输入框那两格保留 label,点标题聚焦输入框
9870
+ // 正是想要的。
9871
+ React.createElement('div', { className: 'dsm-field' },
9251
9872
  React.createElement('span', { className: 'dsm-label' }, t('scenes.field.prompt')),
9252
9873
  presetOptions.length > 0
9253
9874
  ? React.createElement(SourceSelect, { options: presetOptions, value: sceneForm.prompt || '', onChange: function (v) { setSceneForm(Object.assign({}, sceneForm, { prompt: v })) } })
9254
9875
  : React.createElement('p', { className: 'dsm-help' }, t('scenes.prompt.noPresets')),
9255
9876
  helpBullets(t, 'scenes.field.prompt.hint')),
9877
+ // 快捷提示词(0.18.0):默认「当前启用的」= 这一域**不绑**,进出不碰它;
9878
+ // 切到「按场景勾选」才给清单 —— 勾进来的进入本场景后启用、没勾的停用,
9879
+ // 退出场景按进场景前那批开关还原(与子智能体那一档同一条机制)。
9880
+ // 空表是"绑了但一条都不勾"(进入后全部停用),与"不绑"是相反的意思,
9881
+ // 所以那颗下拉只给两个方向,不让用户靠"清空"去表达"不绑"。
9882
+ React.createElement('div', { className: 'dsm-field' },
9883
+ React.createElement('span', { className: 'dsm-label' }, t('scenes.field.quickPrompts')),
9884
+ React.createElement(SourceSelect, {
9885
+ value: Array.isArray(sceneForm.quickPrompts) ? 'custom' : '',
9886
+ options: [{ value: '', label: t('scenes.field.quickPrompts.current') }, { value: 'custom', label: t('scenes.field.quickPrompts.custom') }],
9887
+ onChange: function (v) {
9888
+ setSceneForm(Object.assign({}, sceneForm, {
9889
+ quickPrompts: v === 'custom'
9890
+ // 切过来时从**当前开着的那些**起步(用户裁定:默认就是当前启用的),
9891
+ // 而不是给一张空表 —— 空表说的是"进入后全部停用"。
9892
+ ? (quickOptions || []).filter(function (p) { return p.enabled !== false }).map(function (p) { return p.id })
9893
+ : null,
9894
+ }))
9895
+ },
9896
+ }),
9897
+ // 勾选清单是一张段卡片(与档案编辑器同一套排版):段头报"已勾几条 / 共几条"并给
9898
+ // 全选,段体按内容长(超过 6 行才内滚)。原先这里是一块裸的固定 216px 滚动体,
9899
+ // 一条也占 216px —— 弹窗中间凭空一片空白(用户 2026-10-01 反馈)。
9900
+ Array.isArray(sceneForm.quickPrompts)
9901
+ ? (quickOptions || []).length
9902
+ ? seg({
9903
+ title: t('scenes.field.quickPrompts.list'),
9904
+ count: t('scenes.seg.checked', { checked: (sceneForm.quickPrompts || []).length, total: quickOptions.length }),
9905
+ actions: segActions({
9906
+ defined: true, busy: busy,
9907
+ selectAllLabel: t('bulk.selectAll'), clearLabel: t('bulk.unselectAll'),
9908
+ onAll: function () {
9909
+ setSceneForm(Object.assign({}, sceneForm, { quickPrompts: quickOptions.map(function (p) { return p.id }) }))
9910
+ },
9911
+ onClear: function () { setSceneForm(Object.assign({}, sceneForm, { quickPrompts: [] })) },
9912
+ allChecked: quickOptions.length > 0 && (sceneForm.quickPrompts || []).length === quickOptions.length,
9913
+ }),
9914
+ body: React.createElement('div', { className: 'dsm-seg-body dsm-seg-fit' }, quickOptions.map(function (p) {
9915
+ var list = sceneForm.quickPrompts || []
9916
+ return pickRow({
9917
+ key: p.id, name: p.id, desc: p.description || '',
9918
+ checked: list.indexOf(p.id) >= 0,
9919
+ onChange: function () {
9920
+ var next = list.slice()
9921
+ var at = next.indexOf(p.id)
9922
+ if (at >= 0) next.splice(at, 1)
9923
+ else next.push(p.id)
9924
+ setSceneForm(Object.assign({}, sceneForm, { quickPrompts: next }))
9925
+ },
9926
+ })
9927
+ })),
9928
+ })
9929
+ : React.createElement('p', { className: 'dsm-help' }, t('scenes.field.quickPrompts.none'))
9930
+ : null,
9931
+ helpBullets(t, 'scenes.field.quickPrompts.hint')),
9256
9932
  // 工具表方案(C1):与提示词预设同族的单值绑定字段,按用户 2026-09-28 裁定
9257
9933
  // 从档案编辑器挪进这张表单 —— 两个「场景绑谁」的字段放在一起,档案编辑器
9258
9934
  // 只留内容勾选。改的是启用中的场景时,服务端会立即把关停名单换成那份方案。
9259
- React.createElement('label', { className: 'dsm-field' },
9935
+ React.createElement('div', { className: 'dsm-field' },
9260
9936
  React.createElement('span', { className: 'dsm-label' }, t('scenes.field.toolTable')),
9261
9937
  React.createElement(SourceSelect, {
9262
9938
  value: String(sceneForm.toolTablePreset || ''),
@@ -10886,6 +11562,797 @@ function enabledMemoryCount(rules) {
10886
11562
  React.createElement('div', { className: 'dsm-modal-actions' },
10887
11563
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitTrashDelete(modal.entry) } }, t('memory.trash.purge')))) : null)
10888
11564
  }
11565
+ // ── 斜杠命令「工具」段(对话输入框里直接管六个域)────────────────────────────
11566
+ //
11567
+ // 要解决的问题:切个场景、临时停一个 MCP,都得离开当前对话去「设置 → 工具」。这一段把
11568
+ // 这些**可逆**动作搬进宿主的 `/` 菜单:打一个 `/` → 出现「工具」段 → 点一行原地进入该域
11569
+ // → 点一条直接启停,**菜单停在原地不关**,接着点下一条也行。
11570
+ //
11571
+ // 为什么走 `inputTriggers` 而不是更省事的 `commandUi`(官方弹窗壳,宿主 /model 用的那个):
11572
+ // `commandUi` 注册的命令落在哪个段由宿主写死的 `SECTION_ROWS` 表决定,表里没有的一律排到
11573
+ // 「指令」段尾(dsh-client-ui-commands 的 sectionRows),插件**拿不到自己的段标题**。
11574
+ // 用户要的就是「工具」这个独立分区,所以只能走源这一层 —— 代价是弹窗壳、右下角 ✓ 与官方
11575
+ // 确认门都拿不到,状态呈现改由图标位(绿勾 / 空心框)+ 右侧灰字承担。
11576
+ //
11577
+ // 四条纪律:
11578
+ // ① 读写一律复用面板那批 op,不新增后端通道 —— 面板能做的这里才看得到,两边不会各算一套
11579
+ // 状态(技能写完后服务端照旧走 notifyChatCatalog,官方技能目录随之重拉)。
11580
+ // ② 只放**可逆**动作(进场景 / 启停)。增删改、导入导出、改配置仍只在面板里 —— `/` 菜单
11581
+ // 里没有确认门可用,把不可逆动作放进来就是拿用户的键盘赌他不多看一眼。
11582
+ // ③ 宿主没有 `inputTriggers` 服务时**什么都不发生**:`ctx.inject` 是等依赖的,服务不出现
11583
+ // 回调就不跑(cordis 语义),不报错、不拖累设置页。
11584
+ // ④ 点完不关菜单:宿主的 `settle()` 对**非下钻**的选取一律先 `reduce({type:'close'})`
11585
+ // (input-trigger client.js),所以叶子选取除了把 token 收回本域根,还要在微任务里用
11586
+ // 官方口重新打开这一层 —— 连点多个开关是这个功能的主要用法。整段包在 try 里,任何一步
11587
+ // 取不到就退回"点完关菜单",不能把功能本身带坏。
11588
+ var SLASH_SOURCE_NAME = 'tool-management'
11589
+ /** 快捷提示词那一段自己的源名(与「工具」段分开注册,才能排到官方「技能」段之后)。 */
11590
+ var QUICK_SOURCE_NAME = 'tool-quick-prompts'
11591
+ /** 「全局」保留场景的目录名 —— 与服务端 `src/memories/constants.ts` 的 `GLOBAL_SCENE` 同值。 */
11592
+ var GLOBAL_SCENE = 'global'
11593
+ /** 域清单读到的数据用多久(界面本身 5s 轮询,这里比它短一档,够一次连续输入)。 */
11594
+ var SLASH_CACHE_TTL_MS = 1500
11595
+ /**
11596
+ * 叶子行右侧那句备注截多长。宿主那一格自己会省略号(`itemDescription` 是 nowrap + ellipsis、
11597
+ * `flex:1`),但同一行还要放名字(≤40%)与灰色 alias(≤20%),留给它的大约四成 —— 先按这个
11598
+ * 宽度裁一刀,别让一行读起来像两段话。
11599
+ */
11600
+ var LEAF_NOTE_MAX = 40
11601
+
11602
+ /** 图标位:启用态的绿色对勾。宿主按 `createElement(icon, {size})` 调用。 */
11603
+ function SlashOnIcon(props) {
11604
+ var s = props && props.size ? props.size : 14
11605
+ return React.createElement('svg', { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
11606
+ React.createElement('path', { d: 'M3.2 8.4 6.3 11.5 12.8 4.6', stroke: '#57c27d', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }))
11607
+ }
11608
+ /** 图标位:停用态的空心框(与绿勾同尺寸,两行并排时不会跳)。 */
11609
+ function SlashOffIcon(props) {
11610
+ var s = props && props.size ? props.size : 14
11611
+ return React.createElement('svg', { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
11612
+ React.createElement('rect', { x: 2.6, y: 2.6, width: 10.8, height: 10.8, rx: 3, stroke: '#6b6d75', strokeWidth: 1.6 }))
11613
+ }
11614
+
11615
+ /**
11616
+ * 六个域的读法与写法。
11617
+ *
11618
+ * `opts.quickPrompts` = 兼容页那颗「快捷提示词」开关:开着时快捷词并进「提示词」这一域
11619
+ * (用户 2026-10-01 裁定:这一域叫「提示词」,全局与快捷两类都在里面,各自一个分组标题)。
11620
+ *
11621
+ * `load` 回 `{items, summaryOf}`:items 是叶子行,summaryOf 由**当前显示的那批条目**算出摘要
11622
+ * (域行右侧那句话)。摘要不预先算成字符串,是因为乐观覆盖会改条目状态 —— 预先算好的那句
11623
+ * 会跟改完的列表自相矛盾(`0 启用 / 6 停用` 配上一排绿勾)。
11624
+ *
11625
+ * 每条 item 的 `run(next)` / `done(next)` 都**收目标状态为参数**:闭包里存着"读到的时候是什么
11626
+ * 状态",连点两次就会把同一个值发两遍(第二次点了没反应)。
11627
+ *
11628
+ * 带 `single` 的那一条是**单选且没有"全关"这一态**(全局基线):点亮它就把同组别的关掉,
11629
+ * 再点当前那条什么都不做。域级的 `exclusive` 留给"整域都是单选"的域(场景)—— 一个域里混
11630
+ * 两类时这类标记只能挂在条目上:挂在域上会把快捷词那颗开关一起管掉(关掉一条快捷词会被判成
11631
+ * "点的就是当前那条",什么都不发生)。
11632
+ *
11633
+ * **这一域里没有"贴文字"那种行**:快捷提示词的正文是用户在输入框里自己取用的,走的是
11634
+ * `quickSource` 那一段;这一域的每一行只回答"它现在算不算生效"。
11635
+ *
11636
+ * 每条 item 的 `key` 是**域内唯一身份**,`id` 只是给人看的那一段(也是 `/` 菜单里的 alias)。
11637
+ * 两者必须分开:技能名在两个目录里可以完全一样,MCP 服务器可以在用户级和项目级各有一份 ——
11638
+ * 只按 id 找目标就会点到 A 却改了 B(表现就是"点了没反应")。
11639
+ */
11640
+ function slashDomains(t, opts) {
11641
+ const quickOn = !opts || opts.quickPrompts !== false
11642
+ return [
11643
+ {
11644
+ key: 'scene', token: 'tool-scene', label: t('slash.domain.scene'), exclusive: true,
11645
+ load: async function () {
11646
+ const res = await apiCall('rules-list', {})
11647
+ if (!res || !res.ok) throw new Error(translateError(t, res))
11648
+ const active = res.activeScene || null
11649
+ // 两个保留场景都不列**这一域**:`_shared` 是公共基线(面板里恒为常开、不给开关),
11650
+ // `global` 是「全局」—— 它永远跟着生效,列出来就是一行永远点不动的"未启用"。
11651
+ // 注意别把这条判断复制到记忆域:全局不进场景区,但它的记忆照样要能启停。
11652
+ const scenes = (res.scenes || []).filter(function (s) { return s.shared !== true && s.global !== true })
11653
+ return {
11654
+ summaryOf: function (items) {
11655
+ const now = items.filter(function (x) { return x.on === true })[0]
11656
+ return now ? t('slash.sum.current', { name: now.id }) : t('slash.sum.count', { n: items.length })
11657
+ },
11658
+ items: scenes.map(function (s) {
11659
+ return {
11660
+ id: s.name, key: s.name, label: s.name, on: s.name === active, note: s.description,
11661
+ // 单选,但**关得掉**:再点当前这一项 = 退出场景(`scenes:[]` 是服务端明确支持的
11662
+ // 写法,含义是"只注入全局与公共基线"),跟面板上那颗开关同一个出口。
11663
+ // 运行时(档案里的 MCP / 技能 / 人设 / 工具表)由服务端在这一次调用里跟着切
11664
+ // —— 见 `ops/scene-sync.ts` 的「两轴恒等」,这里**不需要**再补一枪 `scene-mode-set`。
11665
+ run: function (next) { return apiCall('rules-set-active', { scenes: next ? [s.name] : [] }) },
11666
+ done: function (next) { return t(next ? 'slash.did.scene' : 'slash.did.sceneOff', { name: s.name }) },
11667
+ }
11668
+ }),
11669
+ }
11670
+ },
11671
+ },
11672
+ {
11673
+ key: 'skill', token: 'tool-skill', label: t('slash.domain.skill'),
11674
+ load: async function () {
11675
+ const res = await apiCall('skill-state', {})
11676
+ const data = (res && res.data) || {}
11677
+ const items = []
11678
+ for (const root of data.roots || []) {
11679
+ if (root.removed === true) continue
11680
+ const rootName = rootDisplayName(t, root)
11681
+ // `toggleable === false` 的目录里单条技能没有开关(面板同样不给),列出来就是死行。
11682
+ if (root.toggleable === false) continue
11683
+ // **目录本身**一行,开着的也列 —— 不然只关得开、关不掉,等于半个开关。
11684
+ // 它钉在本组最上面(`first`),读起来像"组标题带一颗开关",与技能页卡片头那颗
11685
+ // 是同一个动作(`skill-source-enable` / `skill-source-disable`)。
11686
+ // 默认来源(dsh / hub / 官方内置)与项目作用域没有来源开关,按面板口径不给。
11687
+ if (root.scope !== 'project' && root.defaultSource !== true) {
11688
+ items.push({
11689
+ id: rootName, key: '@' + root.key, label: rootName + ' · ' + t('slash.skill.sourceRow'),
11690
+ on: root.enabled !== false, group: rootName, first: true,
11691
+ run: function (next) { return apiCall(next ? 'skill-source-enable' : 'skill-source-disable', { root: root.key }) },
11692
+ done: function (next) { return t(next ? 'slash.did.sourceOn' : 'slash.did.sourceOff', { name: rootName }) },
11693
+ })
11694
+ }
11695
+ // 目录停用时**照样把里面的技能读进来**,只是由覆盖表那一层先不显示(`effective`):
11696
+ // 这样"重新启用"当场就能把原本生效的那几条摆回来,不用等下一次真读。状态按"假设来源
11697
+ // 开着"算 —— 服务端的 `managerEnabled = 可写 && 来源启用 && 单条没被关掉`,来源关着时
11698
+ // 它恒为 false,而单条自己的意图在 `managerOverride` 里,把来源那一项补成 true 就是
11699
+ // 启用之后会读到的值。
11700
+ const sourceOff = root.enabled === false
11701
+ for (const skill of root.skills || []) {
11702
+ // 被同名遮蔽的那条按了也没效果(面板不给开关,给的是「启用这个」——那是切首选,
11703
+ // 不在"可逆启停"这一类里,所以 `/` 菜单不列)。
11704
+ if (skill.shadowedBy) continue
11705
+ const view = sourceOff ? Object.assign({}, skill, { managerEnabled: true }) : skill
11706
+ items.push({
11707
+ // `key` 带来源,`id` 不带:同名技能在两个目录里各有一行时,`id` 一样而 `key` 不一样
11708
+ // —— 只按 id 找目标会改到另一条(点了没反应就是这个)。alias 那行灰字仍显示短名。
11709
+ id: skill.name, key: root.key + '/' + skill.name,
11710
+ label: skill.declaredName || skill.name, on: isSkillEnabled(view), note: skill.description,
11711
+ // 分组标题 = 来源目录(技能页那套显示名)。技能动辄几十条又分属多个目录,
11712
+ // 不分组就是一片平铺,找不到自己要改的那一条。
11713
+ group: rootName,
11714
+ run: function (next) { return apiCall(next ? 'skill-enable' : 'skill-disable', { root: root.key, name: skill.name }) },
11715
+ done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: skill.name }) },
11716
+ })
11717
+ }
11718
+ }
11719
+ // 域行那句「N 启用 / M 停用」只数技能,不数目录行 —— 混进去的话数字里会多出几条
11720
+ // 根本不是技能的东西,而这句话是拿来核对"我这批技能现在什么状况"的。
11721
+ const skillSummary = slashOnOffSummary(t)
11722
+ return {
11723
+ summaryOf: function (shown) {
11724
+ return skillSummary(shown.filter(function (x) { return x.first !== true }))
11725
+ },
11726
+ items: items,
11727
+ }
11728
+ },
11729
+ },
11730
+ {
11731
+ key: 'mcp', token: 'tool-mcp', label: t('slash.domain.mcp'),
11732
+ load: async function () {
11733
+ const res = await apiCall('mcpm-list', {})
11734
+ if (!res || !res.ok) throw new Error(translateError(t, res))
11735
+ const items = (res.rows || []).map(function (row) {
11736
+ return {
11737
+ // 同一台服务器可以在用户级与项目级各有一份(同名同 id),所以 key 带 level。
11738
+ id: row.id, key: row.level + ':' + row.id, label: row.serverName || row.id, on: row.disabled !== true, note: row.notes,
11739
+ run: function (next) { return apiCall('mcpm-set-enabled', { id: row.id, level: row.level, enabled: next }) },
11740
+ done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: row.serverName || row.id }) },
11741
+ }
11742
+ })
11743
+ return { summaryOf: slashOnOffSummary(t), items: items }
11744
+ },
11745
+ },
11746
+ {
11747
+ key: 'agent', token: 'tool-agent', label: t('slash.domain.agent'),
11748
+ load: async function () {
11749
+ const res = await apiCall('subagent-list', {})
11750
+ if (!res || !res.ok) throw new Error(translateError(t, res))
11751
+ const items = (res.subagents || []).map(function (p) {
11752
+ return {
11753
+ id: p.name, key: p.name, label: p.name, on: p.enabled !== false, note: p.description,
11754
+ run: function (next) { return apiCall('subagent-toggle', { name: p.name, enabled: next }) },
11755
+ done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: p.name }) },
11756
+ }
11757
+ })
11758
+ return { summaryOf: slashOnOffSummary(t), items: items }
11759
+ },
11760
+ },
11761
+ {
11762
+ key: 'preset', token: 'tool-preset', label: t('slash.domain.preset'),
11763
+ load: async function () {
11764
+ const res = await apiCall('agentsmd-list', {})
11765
+ if (!res || !res.ok) throw new Error(translateError(t, res))
11766
+ const items = (res.presets || []).map(function (p) {
11767
+ return {
11768
+ id: p.id, key: p.id, label: p.id, on: p.active === true, note: p.description,
11769
+ // 应用基线改的是**整轮环境**(全局 AGENTS.md)。它可逆(再应用原来那套就切回去),
11770
+ // 所以按②的口径可以进 `/`;但这里没有官方确认门可用,于是把"当前是哪套"写进域行
11771
+ // 摘要 —— 点之前屏幕上至少说清了要换掉什么。
11772
+ // 点"当前这套"不做任何事(`single`):基线没有"全部不应用"这一态,
11773
+ // 把勾去掉等于让全局 AGENTS.md 凭空消失,那不是这里的语义。
11774
+ single: true,
11775
+ run: function () { return apiCall('agentsmd-apply', { id: p.id }) },
11776
+ done: function () { return t('slash.did.preset', { name: p.id }) },
11777
+ }
11778
+ })
11779
+ const quicks = await slashQuickRows(t, quickOn)
11780
+ // 两类都在时才给全局那一组出行标题:只剩一类时,「全局提示词」这条小标题与面包屑上
11781
+ // 的域名说的是同一件事,等于把同一句话写两遍。
11782
+ if (quicks.length) for (const it of items) it.group = t('prompts.group.global')
11783
+ return {
11784
+ summaryOf: function (shown) {
11785
+ const globals = shown.filter(function (x) { return x.single === true })
11786
+ const now = globals.filter(function (x) { return x.on === true })[0]
11787
+ const quicks = shown.filter(function (x) { return x.single !== true })
11788
+ // 基线那句只数全局那一类:快捷词不是"当前生效的基线",把它算进来这句就成谎了。
11789
+ const base = now ? t('slash.sum.current', { name: now.id }) : ''
11790
+ if (!quicks.length) return base
11791
+ // 快捷那一类给个数而不是"当前是哪条":它们是多对开关,说得出"开着几条"就够了。
11792
+ const on = quicks.filter(function (x) { return x.on === true }).length
11793
+ const tail = t('slash.sum.quick', { on: on, total: quicks.length })
11794
+ return base ? base + ' · ' + tail : tail
11795
+ },
11796
+ items: items.concat(quicks),
11797
+ }
11798
+ },
11799
+ },
11800
+ {
11801
+ key: 'memory', token: 'tool-memory', label: t('slash.domain.memory'),
11802
+ load: async function () {
11803
+ const res = await apiCall('rules-list', {})
11804
+ if (!res || !res.ok) throw new Error(translateError(t, res))
11805
+ const active = res.activeScene || ''
11806
+ // 场景显示名(`global` → 「全局」):分组标题用它,别让用户在 `/` 菜单里看到磁盘名。
11807
+ const labels = {}
11808
+ for (const s of res.scenes || []) if (s && s.name) labels[s.name] = s.label || s.name
11809
+ const toRow = function (r) {
11810
+ // 记忆的场景 = group 的第一段(与服务端 `sceneOf` 同一口径,见 memories/projection.ts)。
11811
+ const scene = String(r.group || '').split('/')[0]
11812
+ return {
11813
+ id: r.id, key: r.id, label: r.name, on: r.enabled !== false, note: r.description,
11814
+ scene: scene,
11815
+ // 打了字会跨场景搜,那时按场景分组才看得清是哪里的记忆。
11816
+ group: labels[scene] || scene || t('slash.group.noScene'),
11817
+ run: function (next) { return apiCall('rules-toggle', { id: r.id, enabled: next }) },
11818
+ done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: r.name }) },
11819
+ }
11820
+ }
11821
+ const all = (res.rules || []).filter(function (r) { return r.shadowed !== true }).map(toRow)
11822
+ // 「全局」这一组排最前面(用户 2026-09-30 裁定):它恒常注入,是这一域里最常要动的那组,
11823
+ // 不该沉在当前场景后面。sort 是稳定的,组内顺序不动。
11824
+ const globalFirst = function (list) {
11825
+ return list.slice().sort(function (a, b) {
11826
+ return (a.scene === GLOBAL_SCENE ? 0 : 1) - (b.scene === GLOBAL_SCENE ? 0 : 1)
11827
+ })
11828
+ }
11829
+ // 默认列**当前场景 + 「全局」**:记忆动辄上百条,全列进来等于把记忆页搬进 `/` 菜单,
11830
+ // 而这里要改的几乎总是正在用的那个场景;全局那份虽然不是"当前场景的",却对任何对话
11831
+ // 都成立、恒常在上下文里,用户想停掉的往往正是它。(场景域不列 global 是另一回事:
11832
+ // 它没有开关可给;这里的判断对象是记忆条目,不是场景。)打了字才跨场景搜。
11833
+ const shown = active ? all.filter(function (r) { return r.scene === active || r.scene === GLOBAL_SCENE }) : all
11834
+ return {
11835
+ summaryOf: function (items) {
11836
+ return active
11837
+ ? t('slash.sum.sceneGlobal', { scene: labels[active] || active, n: items.length })
11838
+ : t('slash.sum.count', { n: all.length })
11839
+ },
11840
+ items: globalFirst(shown),
11841
+ searchPool: active ? globalFirst(all) : null,
11842
+ }
11843
+ },
11844
+ },
11845
+ ]
11846
+ }
11847
+
11848
+ /** 进入场景时被引擎跳过的档案键(服务端 `rules-set-active` 把 `scene-mode-set` 的 stale 并进来)。 */
11849
+ function slashModeStaleWarn(t, res) {
11850
+ const stale = res && res.modeStale
11851
+ return Array.isArray(stale) && stale.length ? t('memory.archive.stale', { items: stale.join('、') }) : ''
11852
+ }
11853
+
11854
+ /**
11855
+ * 「提示词」域里快捷那一组(每一行 = 面板上那颗药丸开关,同一个动作、同一份状态)。
11856
+ *
11857
+ * 读失败或开关关着一律回空数组,**不抛**:这一域的主体是全局基线,不能因为快捷那一组读不到
11858
+ * 就把整域打成"读不到"(那连切基线都做不了了)。
11859
+ *
11860
+ * 这里列**全部**快捷词,包括已经关着的那些 —— 与贴文字那一段(`quickSource`)正好相反:
11861
+ * 那一段按 `enabled` 筛过,关掉的不再出现;如果这一域也只列开着的,关掉一条之后两个入口就
11862
+ * 都没有它了,只能回设置页去开。域是"管"的那一层,管的东西不能因为被管起来就消失。
11863
+ */
11864
+ async function slashQuickRows(t, quickOn) {
11865
+ if (quickOn === false) return []
11866
+ let res
11867
+ try { res = await apiCall('quickprompt-list', {}) } catch (e) { return [] }
11868
+ if (!res || res.ok !== true) return []
11869
+ return (res.prompts || []).map(function (p) {
11870
+ return {
11871
+ // `key` 带前缀:全局预设与快捷词各存一份目录,同名完全可能(`id` 那一格仍然显示短名)。
11872
+ id: p.id, key: 'q:' + p.id, label: p.id, note: p.description,
11873
+ on: p.enabled !== false,
11874
+ group: t('prompts.group.quick'),
11875
+ // 与面板那颗开关同一条 op(`quickprompt-toggle`):只改 `meta.json` 里那一个布尔,
11876
+ // 正文与备注一个字不动。含义是"这条还出现在不在 `/` 的贴文字段里"。
11877
+ run: function (next) { return apiCall('quickprompt-toggle', { id: p.id, enabled: next }) },
11878
+ done: function (next) { return t(next ? 'slash.did.quickOn' : 'slash.did.quickOff', { name: p.id }) },
11879
+ }
11880
+ })
11881
+ }
11882
+
11883
+ /** 「N 启用 / M 停用」那句摘要(技能 / MCP / 子智能体三个域共用)。 */
11884
+ function slashOnOffSummary(t) {
11885
+ return function (items) {
11886
+ let on = 0
11887
+ for (const item of items) if (item.on === true) on += 1
11888
+ return t('slash.sum.onoff', { on: on, off: items.length - on })
11889
+ }
11890
+ }
11891
+
11892
+ /** 条目的身份:`key`(域内唯一)优先,没给就退回 `id`。覆盖表与点选定位都按它走。 */
11893
+ function slashItemKey(item) {
11894
+ return item.key === undefined ? String(item.id) : String(item.key)
11895
+ }
11896
+
11897
+ /**
11898
+ * 一次输入期间的读缓存 + 乐观覆盖,按域放在一起。
11899
+ *
11900
+ * 为什么要覆盖表:点完一条要**立刻**看到勾翻过来,而写请求是异步的、宿主快照也要下一次
11901
+ * 候选轮询才重读。覆盖只在"这个域重新真读过一次"时清掉(读回来的是权威状态,继续压着它
11902
+ * 就会把面板那边的改动盖住),写失败也立刻撤回。
11903
+ */
11904
+ function slashState() {
11905
+ const entries = new Map()
11906
+ const overrides = new Map()
11907
+ const overrideKey = function (domainKey, itemKey) { return domainKey + '/' + itemKey }
11908
+ const clearOverrides = function (domainKey) {
11909
+ const prefix = domainKey + '/'
11910
+ for (const key of Array.from(overrides.keys())) if (key.indexOf(prefix) === 0) overrides.delete(key)
11911
+ }
11912
+ const read = function (domain, force) {
11913
+ const key = domain.key
11914
+ const hit = entries.get(key)
11915
+ if (!force && hit && hit.promise) return hit.promise
11916
+ if (!force && hit && !hit.promise && Date.now() - hit.at < SLASH_CACHE_TTL_MS) return hit.value
11917
+ const promise = Promise.resolve().then(domain.load).then(function (value) {
11918
+ clearOverrides(key)
11919
+ entries.set(key, { at: Date.now(), value: value })
11920
+ return value
11921
+ }, function (error) { entries.delete(key); throw error })
11922
+ entries.set(key, { promise: promise })
11923
+ return promise
11924
+ }
11925
+ /**
11926
+ * 把覆盖表叠到读回来的条目上(返回新数组,不改缓存里那份)。
11927
+ *
11928
+ * 顺带按**叠完之后**的状态收掉整组:技能目录被(真读或乐观)判成停用时,它里面的技能行不再
11929
+ * 出现。服务端在这种组合下根本不让单条生效,留着那几行就是"看得见、点了没反应";而"目录关了
11930
+ * 勾还在"这种半截状态,用户读到的意思是"里面还有技能",跟面板与下一次真读都对不上。
11931
+ */
11932
+ const effective = function (domain, items) {
11933
+ const mapped = items.map(function (item) {
11934
+ const o = overrides.get(overrideKey(domain.key, slashItemKey(item)))
11935
+ return o === undefined ? item : Object.assign({}, item, { on: o })
11936
+ })
11937
+ const offGroup = {}
11938
+ for (const item of mapped) if (item.first === true && item.on !== true) offGroup[item.group] = true
11939
+ if (!Object.keys(offGroup).length) return mapped
11940
+ return mapped.filter(function (item) { return item.first === true || !offGroup[item.group] })
11941
+ }
11942
+ /**
11943
+ * 记一次"用户已经把它改成 next"。
11944
+ *
11945
+ * 单选的那一类(场景域的整域 `exclusive`;提示词域里全局那一组的每条 `single`)改一个就是
11946
+ * 把别的关掉,只记一条会让屏幕上同时出现两个绿勾,直到下一次真读才自己对齐 —— 那正是
11947
+ * "界面在骗人"的那几百毫秒。
11948
+ *
11949
+ * **只压同组**:提示词这一域里全局基线是单选、快捷词是多对开关,两类共处一域。不按分组收
11950
+ * 的话,点亮一条快捷词会把当前生效的那份基线的勾一起抹掉(覆盖表说的是假话,而下一次真读
11951
+ * 又把它翻回来 —— 表现就是勾自己跳了一下)。
11952
+ */
11953
+ const mark = function (domain, items, item, next) {
11954
+ const touched = [overrideKey(domain.key, slashItemKey(item))]
11955
+ overrides.set(touched[0], next)
11956
+ if ((domain.exclusive === true || item.single === true) && next === true) {
11957
+ for (const other of items) {
11958
+ if (slashItemKey(other) === slashItemKey(item) || other.on !== true) continue
11959
+ if ((other.group || '') !== (item.group || '')) continue
11960
+ const otherKey = overrideKey(domain.key, slashItemKey(other))
11961
+ overrides.set(otherKey, false)
11962
+ touched.push(otherKey)
11963
+ }
11964
+ }
11965
+ return touched
11966
+ }
11967
+ const unmark = function (keys) { for (const key of keys) overrides.delete(key) }
11968
+ const flush = function () { entries.clear() }
11969
+ return { read: read, effective: effective, mark: mark, unmark: unmark, flush: flush }
11970
+ }
11971
+
11972
+ /** 名字 / 标题的小写包含匹配(宿主不对 source 的结果二次排序,官方技能源也是自己排的)。 */
11973
+ function slashMatch(needle, item) {
11974
+ if (!needle) return true
11975
+ const q = needle.toLowerCase()
11976
+ return String(item.id).toLowerCase().indexOf(q) >= 0 || String(item.label).toLowerCase().indexOf(q) >= 0
11977
+ }
11978
+
11979
+ /**
11980
+ * 把「工具」段与「快捷提示词」挂进宿主的 `/` 菜单。
11981
+ *
11982
+ * @param ctx 客户端根上下文(apply 的那个)
11983
+ * @param t 取词函数(每次候选都重新取,所以宿主换语言后下一次打开菜单就是新词)
11984
+ * @param settings 兼容页那两颗开关 `{ tools, quickPrompts }`,缺字段一律按开处理。
11985
+ * 两段**各挂各的**:「工具」里那个「提示词」域管两类提示词的启停,
11986
+ * 「快捷提示词」那一段管把正文贴进输入框 —— 两颗开关互不代替,关掉前者不会
11987
+ * 顺带把快捷词收走,反之也一样。
11988
+ * 两颗都关掉时**什么都不做**(不注册源,`/` 菜单回到出厂样子),而不是注册一个
11989
+ * 空段 —— 空段会让人以为开关没生效。
11990
+ */
11991
+ function mountSlashCommands(ctx, t, settings) {
11992
+ const cfg = settings || {}
11993
+ // 挂载实况按段记:关着的那一段不该在 10s 后被报成"宿主没有这个服务"(那是另一回事),
11994
+ // 也不该一直停在"还没挂上" —— 它就是被关着的,照实说。
11995
+ const wanted = []
11996
+ if (cfg.tools !== false) wanted.push('tools')
11997
+ else setSlashMount('disabled', 'tools')
11998
+ if (cfg.quickPrompts !== false) wanted.push('quickPrompts')
11999
+ else setSlashMount('disabled', 'quickPrompts')
12000
+ if (!wanted.length) return
12001
+ // 「宿主压根没有这个服务」与「还没就绪」得区分开:`ctx.inject` 是等依赖的,服务不出现时
12002
+ // 回调**永远不跑**、也不报错 —— 于是兼容页那两枚标签会一直停在 pending,用户只能猜。
12003
+ // 10s 后还 pending 的段照实说"这台宿主没有 inputTriggers"(真缺服务时这是最终事实;
12004
+ // 万一它只是慢,源随后照样会挂上,状态会被 registered 覆盖)。
12005
+ try {
12006
+ setTimeout(function () {
12007
+ for (const key of wanted) if (slashMountStateOf(key) === 'pending') setSlashMount('missing', key)
12008
+ }, 10000)
12009
+ } catch (e) { /* 报不了状态不影响功能本身 */ }
12010
+ ctx.inject(['inputTriggers'], function (scope) {
12011
+ const triggers = scope.get('inputTriggers')
12012
+ const state = slashState()
12013
+ const sectionTitle = function () { return t('slash.section.tools') }
12014
+ // 每次候选都现取一份域表(域名跟着宿主语言走),但「快捷提示词要不要并进提示词域」这个
12015
+ // 决定来自启动时读到的那颗开关 —— 三处调用点(候选 / 面包屑 / 点选)必须用同一个判断,
12016
+ // 否则面包屑会指到一个候选里根本不存在的域。
12017
+ const domainOpts = { quickPrompts: cfg.quickPrompts !== false }
12018
+ const domainsOf = function () { return slashDomains(t, domainOpts) }
12019
+
12020
+ /** 会话那一份触发器控制器;拿不到就回 undefined(调用方一律静默降级)。 */
12021
+ function controllerOf(sessionId) {
12022
+ if (!sessionId) return undefined
12023
+ const scope = ctx.sessions && typeof ctx.sessions.scope === 'function' ? ctx.sessions.scope(sessionId) : undefined
12024
+ if (!scope) return undefined
12025
+ const service = ctx.get('inputTriggers')
12026
+ return service && typeof service.sessionOf === 'function' ? service.sessionOf(scope) : undefined
12027
+ }
12028
+
12029
+ /**
12030
+ * 写成功之后让菜单重取一次候选(官方口 `controller.refreshOpenMenu()`:只重取,
12031
+ * 不改 hit、不闪可见行)。
12032
+ *
12033
+ * 为什么必须有它:宿主要么按 query 变化、要么按它自己的目录失效才重取候选,而我们的
12034
+ * `state.flush()` 只是把自家缓存清空 —— 菜单没再问,屏幕上就还是**上一次那份快照**:
12035
+ * 条目多寡跟着旧状态走(目录刚关掉、里面十几条还列着;刚开、那几条又不见了),勾翻过来了
12036
+ * 列表却没换。乐观覆盖管的是"状态",管不了"这一屏该有哪些行"。菜单已经关了它就是空操作。
12037
+ */
12038
+ function refreshAfterWrite(sessionId) {
12039
+ try {
12040
+ const controller = controllerOf(sessionId)
12041
+ if (controller && typeof controller.refreshOpenMenu === 'function') controller.refreshOpenMenu()
12042
+ } catch (e) { /* 刷不动只是这一屏晚一拍,改动本身已经生效 */ }
12043
+ }
12044
+
12045
+ /**
12046
+ * 点完把菜单留在这一层(纪律④)。
12047
+ *
12048
+ * 宿主的 `settle()` 对非下钻选取一律先关菜单,所以这里在微任务里用官方口重开:
12049
+ * `sessions.scope(sessionId)` → `inputTriggers.sessionOf(scope)` → `controller.toggleSource`
12050
+ * (宿主自己那个 `/` 菜单启动按钮走的就是同一条口,见 dsh-client-ui-conversation 的
12051
+ * toggleCommandMenu)。
12052
+ *
12053
+ * 两处必须照实做,否则会以更难看的方式坏:
12054
+ * · `span.draftRev` **现取**(`conversation.input.for(scope).state`)—— 宿主拿它做 CAS,
12055
+ * 差一位下一次点击就被静默拒掉(点了没反应,还查不出原因);
12056
+ * · 取到之后核对草稿里确实是刚写回的那串,对不上就什么都不做。
12057
+ * 任何一步拿不到都只是退回"点完关菜单",不能把功能本身带坏。
12058
+ */
12059
+ function keepOpen(sessionId, query, spanStart) {
12060
+ if (!sessionId || typeof spanStart !== 'number') return
12061
+ try {
12062
+ const scope = ctx.sessions && typeof ctx.sessions.scope === 'function' ? ctx.sessions.scope(sessionId) : undefined
12063
+ if (!scope) return
12064
+ const service = ctx.get('inputTriggers')
12065
+ const controller = service && typeof service.sessionOf === 'function' ? service.sessionOf(scope) : undefined
12066
+ if (!controller || typeof controller.toggleSource !== 'function') return
12067
+ // 宿主自己已经把它重开了 —— 再 toggle 一次就是关掉,正好相反。
12068
+ if (controller.launcher && typeof controller.launcher.getSnapshot === 'function'
12069
+ && controller.launcher.getSnapshot() === SLASH_SOURCE_NAME) return
12070
+ const conversation = ctx.get('conversation')
12071
+ const input = conversation && conversation.input && typeof conversation.input.for === 'function'
12072
+ ? conversation.input.for(scope) : undefined
12073
+ const st = input && input.state && typeof input.state.getSnapshot === 'function' ? input.state.getSnapshot() : undefined
12074
+ if (!st) return
12075
+ const text = '/' + query
12076
+ const span = { start: spanStart, end: spanStart + text.length, draftRev: st.draftRev }
12077
+ if (String(st.draft || '').slice(span.start, span.end) !== text) return
12078
+ controller.toggleSource(SLASH_SOURCE_NAME, {
12079
+ trigger: '/', query: query, quoted: false, position: 'leading', span: span,
12080
+ })
12081
+ } catch (e) { /* 重开失败只是少一次连点,这次改动本身已经生效 */ }
12082
+ }
12083
+
12084
+ /** 摘要要单独等:域行不能因为一次读失败就不出现(那样整段会凭空消失,看起来像坏了)。 */
12085
+ async function domainRows(domains, query) {
12086
+ const picked = domains.filter(function (d) {
12087
+ if (!query) return true
12088
+ const q = query.toLowerCase()
12089
+ return d.token.toLowerCase().indexOf(q) >= 0 || d.label.toLowerCase().indexOf(q) >= 0
12090
+ })
12091
+ return Promise.all(picked.map(async function (d) {
12092
+ let description = ''
12093
+ try {
12094
+ const data = await state.read(d)
12095
+ description = data.summaryOf(state.effective(d, data.items)) || ''
12096
+ } catch (e) { description = '' }
12097
+ return {
12098
+ name: d.token, label: d.label, icon: SlashOffIcon, drill: true,
12099
+ ...description ? { description: description } : {},
12100
+ section: sectionTitle(), value: d.key,
12101
+ }
12102
+ }))
12103
+ }
12104
+
12105
+ /** 叶子行(下钻后的某域条目)。 */
12106
+ async function leafRows(domain, rest) {
12107
+ let data
12108
+ try {
12109
+ data = await state.read(domain)
12110
+ } catch (e) {
12111
+ // 读失败:菜单里不出假列表,直接说清楚。列一行"出错了"再让用户点它,点下去什么
12112
+ // 也不会发生 —— 那是第二条谎。
12113
+ showPluginToast(t('slash.err.load', { domain: domain.label, error: errMsg(e) }), 'err')
12114
+ return []
12115
+ }
12116
+ // 记忆域带了一份跨场景全量:只在打了字的时候用(见 load 里的注释)。
12117
+ const pool = rest && data.searchPool ? state.effective(domain, data.searchPool) : state.effective(domain, data.items)
12118
+ const hit = pool.filter(function (item) { return slashMatch(rest, item) })
12119
+ // 排序三档:先按分组(来源目录 / 场景)**首次出现的顺序**,组内把"目录"那行钉在最上,
12120
+ // 再按"已启用的在前 + 名字"。分组顺序不能打乱 —— 宿主只在 section 与上一行不同时才画一条
12121
+ // 标题,同组被拆开就会冒出一串重复标题。
12122
+ const order = new Map()
12123
+ for (const item of hit) {
12124
+ const g = item.group || domain.label
12125
+ if (!order.has(g)) order.set(g, order.size)
12126
+ }
12127
+ hit.sort(function (a, b) {
12128
+ const ga = order.get(a.group || domain.label)
12129
+ const gb = order.get(b.group || domain.label)
12130
+ if (ga !== gb) return ga - gb
12131
+ // 目录行是这一组的总开关,跟着状态跳到别处就找不着了,所以永远钉在本组第一行。
12132
+ const fa = a.first === true ? 0 : 1
12133
+ const fb = b.first === true ? 0 : 1
12134
+ if (fa !== fb) return fa - fb
12135
+ if (a.on !== b.on) return a.on ? -1 : 1
12136
+ return String(a.id).localeCompare(String(b.id))
12137
+ })
12138
+ // 全量列出,不截断:宿主的候选面板自己带滚动(MenuView 的 viewport + 溢出提示),
12139
+ // 之前那套"每域只列 6 条 + 一行继续输入以筛选"是把滚动条的活拿来让用户打字。
12140
+ return hit.map(function (item) {
12141
+ // 右侧灰字给条目自己的备注(场景说明 / 技能描述 / MCP 备注 / 人设描述 / 预设描述 /
12142
+ // 记忆描述):启用状态图标位那枚勾已经说了,不必再占一格。没有备注的才退回状态词,
12143
+ // 免得那一格空着像坏了。
12144
+ const note = clipText(item.note, LEAF_NOTE_MAX)
12145
+ return {
12146
+ name: domain.token + '/' + item.id, label: item.label,
12147
+ description: note || (item.on ? t('slash.state.on') : t('slash.state.off')),
12148
+ icon: item.on ? SlashOnIcon : SlashOffIcon,
12149
+ section: item.group || domain.label,
12150
+ value: JSON.stringify({ domain: domain.key, key: slashItemKey(item) }),
12151
+ }
12152
+ })
12153
+ }
12154
+
12155
+ const source = {
12156
+ trigger: '/',
12157
+ name: SLASH_SOURCE_NAME,
12158
+ // 段的先后按宿主的源表排序走(`roster.sources(trigger)` 以 `order ?? 0` 升序):
12159
+ // 官方「指令」源没写 `order`(=0)、技能源是 2,所以 1 把「工具」夹在两者中间
12160
+ // (用户裁定:工具在指令下面、技能上面)。
12161
+ order: 1,
12162
+ // 每行都自带 section ⇒ 宿主不再渲染源标题行(MenuView 的判据),段名由我们词典出,
12163
+ // 中英随宿主语言。源名本身(tool-management)因此不会出现在界面上,只当唯一性用。
12164
+ showGroupTitle: false,
12165
+ async candidates(session, req) {
12166
+ // 只在行首提供:`帮我看看 /tool` 这种半路打出来的 `/` 不该弹我们的东西。
12167
+ if (req.position !== 'leading') return []
12168
+ const domains = domainsOf()
12169
+ const query = req.query || ''
12170
+ const at = query.indexOf('/')
12171
+ if (at < 0) return domainRows(domains, query)
12172
+ const token = query.slice(0, at)
12173
+ const domain = domains.filter(function (d) { return d.token === token })[0]
12174
+ if (!domain) return []
12175
+ return leafRows(domain, query.slice(at + 1))
12176
+ },
12177
+ header(session, req) {
12178
+ const query = req.query || ''
12179
+ if (query.indexOf('/') < 0) return undefined
12180
+ const domain = domainsOf().filter(function (d) { return query.indexOf(d.token + '/') === 0 })[0]
12181
+ if (!domain) return undefined
12182
+ // **不看 req.drilled**:宿主在每次"非下钻"的选取后都会把这个标记清掉
12183
+ // (input-trigger client.js 的 `this.drilled = action === 'drill'`),而叶子选取正是
12184
+ // 非下钻 —— 用它当判据的话,勾第一次开关之后面包屑就没了,"返回上一级"跟着消失。
12185
+ // 官方的 @file 源可以只认 drilled(打字打出来的路径草稿里自带上下文),这里不行。
12186
+ return [
12187
+ { label: sectionTitle(), value: '/tool-' },
12188
+ { label: domain.label, value: '/' + domain.token + '/', current: true },
12189
+ ]
12190
+ },
12191
+ onPick(pick) {
12192
+ const candidate = pick.candidate
12193
+ const value = candidate.value
12194
+ if (typeof value !== 'string' || value === '') return undefined
12195
+ const sessionId = pick.session && pick.session.sessionId
12196
+ const spanStart = pick.span && typeof pick.span.start === 'number' ? pick.span.start : undefined
12197
+ /** 写回 token 并把菜单留在这一层(`inserted` 含前导 `/`,宿主要的是替换整个 token 段)。 */
12198
+ const stayAt = function (inserted) {
12199
+ const query = inserted.slice(1)
12200
+ Promise.resolve().then(function () { keepOpen(sessionId, query, spanStart) })
12201
+ return { text: inserted, continue: true }
12202
+ }
12203
+ // 面包屑:value 就是"回到那一层"要写回的 token(官方语义:回到上层与深入一层是同一个出口)。
12204
+ if (value.charAt(0) === '/') return stayAt(value)
12205
+ // 域行:原地进入该域。宿主只在点行右侧 › 或按 Tab 时发 action:'drill',点整行发的是
12206
+ // 'pick'(MenuView 的行渲染),所以这里不看 action —— 点哪都该进去。
12207
+ if (candidate.drill === true) return stayAt('/' + String(candidate.name) + '/')
12208
+ let target = null
12209
+ try { target = JSON.parse(value) } catch (e) { return undefined }
12210
+ if (!target || !target.domain || target.key === undefined) return undefined
12211
+ const domain = domainsOf().filter(function (d) { return d.key === target.domain })[0]
12212
+ if (!domain) return undefined
12213
+ // 回到本域根 + 菜单不关(纪律④):执行是异步的,所以先叠一份乐观状态再发请求。
12214
+ state.read(domain, true).then(function (data) {
12215
+ const pool = (data.searchPool || data.items)
12216
+ const shown = state.effective(domain, pool)
12217
+ // 按 key 定位,不按 id:技能/来源这类条目会重名(同名技能在两个目录里各有一行),
12218
+ // 按 id 取到的是列表里第一条同名的 —— 用户点了自己那行、改的却是别的目录里的。
12219
+ const item = shown.filter(function (x) { return slashItemKey(x) === String(target.key) })[0]
12220
+ if (!item) { showPluginToast(t('slash.err.gone'), 'err'); return }
12221
+ const next = item.on !== true
12222
+ if (item.single === true && !next) {
12223
+ // 单选又没有"全关"这一态的那一类(AGENTS.md 基线)点"当前这一项":什么都不做,
12224
+ // 只说清楚现在就是它。场景不在这儿 —— 关掉它=退出场景,是一个真实可逆的动作;
12225
+ // 快捷提示词也不在这儿 —— 那颗开关关得掉。
12226
+ showPluginToast(t('slash.did.already', { name: item.label }), 'ok')
12227
+ return
12228
+ }
12229
+ const touched = state.mark(domain, shown, item, next)
12230
+ return Promise.resolve(item.run(next)).then(function (res) {
12231
+ if (!res || res.ok === false) {
12232
+ state.unmark(touched)
12233
+ showPluginToast(res && res.error ? translateError(t, res) : t('slash.err.failed'), 'err')
12234
+ return
12235
+ }
12236
+ // 写成功后清缓存 + 让菜单重取一次:下一次候选读到真值,覆盖表随之让位(宿主的写
12237
+ // 在返回前已经把自己那份快照失效掉了,重读拿到的就是新状态)。
12238
+ state.flush()
12239
+ refreshAfterWrite(sessionId)
12240
+ const warn = joinWarn(stateSyncWarn(t, res) || sceneSyncWarn(t, res), slashModeStaleWarn(t, res))
12241
+ showPluginToast(warn ? joinWarn(item.done(next), warn) : item.done(next), warn ? 'warn' : 'ok')
12242
+ })
12243
+ }).catch(function (e) {
12244
+ showPluginToast(errMsg(e), 'err')
12245
+ })
12246
+ return stayAt('/' + domain.token + '/')
12247
+ },
12248
+ async matchEnter(session, line) {
12249
+ // 只认领"整行就是半个 token"这一种:`/tool-skill 帮我…` 后面还跟着正文时不拦,
12250
+ // 那是用户在写字,不是手滑按了回车。
12251
+ const trimmed = String(line || '').trim()
12252
+ if (trimmed.indexOf('/tool-') !== 0 || /\s/.test(trimmed)) return undefined
12253
+ showPluginToast(t('slash.err.pick'), 'err')
12254
+ return { text: '' }
12255
+ },
12256
+ }
12257
+
12258
+ // ── 快捷提示词单独成段(贴文字那一段,order 3 ⇒ 排在官方「技能」段之后)──────────────
12259
+ //
12260
+ // 与「工具 › 提示词」那一域是**两件事**,所以两处都在(用户 2026-10-01 裁定):域里那一组
12261
+ // 回答"这条要不要用"(与面板那颗药丸同一个动作),这一段回答"把这段话拿出来"——
12262
+ // 点一下把正文原样贴进输入框。同一屏里出现两种行会互相误读,所以贴文字绝不进域里。
12263
+ //
12264
+ // 为什么这一段能排到官方「技能」下面:宿主的段先后由**源**的 `order` 决定(`roster.sources`
12265
+ // 排序后一组一组渲染),同一个源里的行只能跟自己排顺序 —— 分组标题(section)只管一个源内部。
12266
+ //
12267
+ // 点一下 = 把正文原样贴进草稿,**不自动发送**(宿主要用户自己按 Enter):这一段是
12268
+ // 用户自己写的现成话,替他按下发送键就是把"贴上来看看"变成"已经发出去了"。
12269
+ const QUICK_TTL_MS = 15000
12270
+ const quickCache = { at: 0, promise: null, rows: null }
12271
+ function quickPrompts() {
12272
+ if (quickCache.promise) return quickCache.promise
12273
+ if (quickCache.rows && Date.now() - quickCache.at < QUICK_TTL_MS) return Promise.resolve(quickCache.rows)
12274
+ quickCache.promise = apiCall('quickprompt-list', {}).then(function (res) {
12275
+ quickCache.promise = null
12276
+ if (!res || res.ok !== true) throw new Error(translateError(t, res) || t('slash.err.failed'))
12277
+ quickCache.at = Date.now()
12278
+ quickCache.rows = res.prompts || []
12279
+ return quickCache.rows
12280
+ }, function (e) { quickCache.promise = null; throw e })
12281
+ return quickCache.promise
12282
+ }
12283
+ const quickSource = {
12284
+ trigger: '/',
12285
+ name: QUICK_SOURCE_NAME,
12286
+ order: 3,
12287
+ showGroupTitle: false,
12288
+ async candidates(session, req) {
12289
+ if (req.position !== 'leading') return []
12290
+ const query = String(req.query || '')
12291
+ // 只列一层:带 `/` 的草稿(`/tool-skill/…`)不是这一段的 token。
12292
+ if (query.indexOf('/') >= 0) return []
12293
+ let rows
12294
+ try { rows = await quickPrompts() } catch (e) {
12295
+ // 读不到就不出这一段。每次按键都弹一条 toast 只会变成噪声 —— 为什么没有这段,
12296
+ // 兼容页那行挂载状态说得出"挂上了",具体哪次读失败在面板里看得境。
12297
+ return []
12298
+ }
12299
+ const q = query.toLowerCase()
12300
+ return rows.filter(function (p) {
12301
+ // 面板上那颗开关关掉的不出现(与「提示词」域里那一组同一条判据,见 slashQuickRows)。
12302
+ if (p.enabled === false) return false
12303
+ if (!q) return true
12304
+ return String(p.id).toLowerCase().indexOf(q) >= 0 || String(p.description || '').toLowerCase().indexOf(q) >= 0
12305
+ }).map(function (p) {
12306
+ return {
12307
+ name: 'qp-' + p.id, label: p.id, value: p.id,
12308
+ // 右侧灰字给备注(与提示词页那一行同一个字段):正文本身点下去就整段进输入框,
12309
+ // 在这里再截一行只是把同一句话占成两处。
12310
+ description: String(p.description || ''),
12311
+ section: t('slash.section.quick'),
12312
+ // 正文随候选带着:宿主的 `settle()` 拿到 onPick 的返回值后**同步**执行插入,
12313
+ // 现场发请求读正文来不及(`quickprompt-list` 连正文一起回就是这个原因)。
12314
+ body: String(p.content || ''),
12315
+ }
12316
+ })
12317
+ },
12318
+ onPick(pick) {
12319
+ const body = String((pick.candidate && pick.candidate.body) || '')
12320
+ if (!body.trim()) { showPluginToast(t('slash.quick.empty'), 'err'); return undefined }
12321
+ // 不带 `continue`:菜单关掉、正文留在草稿里(宿主替换的是整个 `/qp-xxx` token)。
12322
+ return { text: body }
12323
+ },
12324
+ async matchEnter(session, line) {
12325
+ const trimmed = String(line || '').trim()
12326
+ if (trimmed.indexOf('/qp-') !== 0 || /\s/.test(trimmed)) return undefined
12327
+ showPluginToast(t('slash.err.pick'), 'err')
12328
+ return { text: '' }
12329
+ },
12330
+ }
12331
+ // 重名会当场抛(`registerSource` 判据是同 trigger 下源名唯一)—— 那是别的插件占了
12332
+ // `tool-management` 这个名字,不是本功能的故障,但必须说清"为什么没有这一段",
12333
+ // 否则表现就是"开关开着、菜单里却什么都没有"。
12334
+ const unregisters = []
12335
+ try {
12336
+ // 两段各按自己那颗开关注册:「工具」管六个域,「快捷提示词」管贴文字那一段 ——
12337
+ // 域里的快捷词是启停,不是贴文字,所以两处互不代替(见上面两段各自的注释)。
12338
+ if (cfg.tools !== false) unregisters.push(triggers.registerSource(source))
12339
+ if (cfg.quickPrompts !== false) unregisters.push(triggers.registerSource(quickSource))
12340
+ } catch (e) {
12341
+ // 后一个源抛了也要把前一个撤掉:只挂一半比一个都不挂更难解释。
12342
+ unregisters.forEach(function (off) { try { off() } catch (e2) { /* 已经报不了状态了 */ } })
12343
+ setSlashMount('conflict')
12344
+ return
12345
+ }
12346
+ for (const key of wanted) setSlashMount('registered', key)
12347
+ scope.effect(function () {
12348
+ return function () {
12349
+ unregisters.forEach(function (off) { try { off() } catch (e) { /* 撤不掉只是多留一个源,不影响界面 */ } })
12350
+ setSlashMount('unregistered')
12351
+ }
12352
+ })
12353
+ })
12354
+ }
12355
+
10889
12356
 
10890
12357
  // 页面组件填进 factory 作用域那个占位对象(导出对象求值时已指向它)。
10891
12358
  // 不要改回 `module.exports.X = ...`:apply 可能在本函数开头提前返回。