dsh-plugin-tool-management 0.12.1 → 0.13.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.
package/lib/client.js CHANGED
@@ -20,6 +20,17 @@ window.__ModuleLoader__.load({
20
20
 
21
21
  // 本插件自己的设计变量(宿主那套是 `--dsw-*`)。告警色此前在 12 条规则里各写一遍
22
22
  // 十六进制字面量,换主题要改 12 次 —— 收敛成 `--dsm-warn`,字面量只留下面那一处。
23
+
24
+ /**
25
+ * 取词三件套里的兜底那一件:动态键(按名字拼出来的键)缺失时返回 `fallback`,
26
+ * 而不是把键名当文案显示出来(`t` 缺键时原样返回键名)。
27
+ *
28
+ * 定义放在**文件头这一片**(2026-09-23 从那几页的共用区搬来):它要服务的是**早于它**
29
+ * 出现的分片(兼容页在 20 片,按名字拼 `compat.tools.about.<工具名>`),而分片共用一个
30
+ * factory 作用域 —— 声明在后面时早于它的调用点取不到这个名字(`npm run typecheck:client`
31
+ * 当场会红)。与 DICT 无依赖,`t` 是传进来的,所以放这儿不影响求值顺序。
32
+ */
33
+ function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
23
34
  const CSS =
24
35
  ':root{--dsm-warn:#d49245;--dsm-warn-bg:rgba(212,146,69,.12)}' +
25
36
  '.dsm-stat-row{display:flex;min-width:0;align-items:center;gap:10px}.dsm-stat-row .dsm-summary{flex:1;min-width:0}' +
@@ -104,7 +115,10 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
104
115
  .dsm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dsm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dsm-modal-sm,.dsm-modal-import{width:min(480px,100%)!important}.dsm-modal-md{width:min(560px,100%)!important}.dsm-modal-lg,.dsm-modal-wide{width:min(720px,100%)!important}.dsm-modal-list{width:min(560px,100%)!important;height:min(640px,calc(100vh - 48px))!important;max-height:none!important;overflow:hidden}.dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:10px}.dsm-modal-list .dsm-modal-body{flex:1;overflow:auto;overscroll-behavior:contain}.dsm-modal-import{padding:24px}.dsm-modal-head{display:flex;align-items:flex-start;gap:12px}
105
116
  /* 头部这一层包住「标题行 + 令牌提示」:提示挂在头部里面(而不是 head 与 body 之间)是为了
106
117
  让 body 的子节点位置稳定 —— 位置一变 React 会把 body 整棵重挂(输入框失焦、下拉归零)。 */
107
- .dsm-modal-head-wrap{display:flex;flex-direction:column;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-help-list{display:flex;margin:0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer;font:inherit;text-align:center}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid var(--dsm-warn);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-group-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
118
+ .dsm-modal-head-wrap{display:flex;flex-direction:column;gap:12px}.dsm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dsm-form,.dsm-field,.dsm-detail-section{display:flex;flex-direction:column}.dsm-form{gap:12px}.dsm-field{gap:6px}.dsm-label,.dsm-detail-title{font-size:12px}.dsm-label{color:var(--dsw-alias-label-secondary)}.dsm-detail-title{font-weight:650}.dsm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dsm-help-list{display:flex;margin:0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}
119
+ /* 两动作性质不同的弹窗底部(目前只有「进入场景」的预览卡):左边「不再显示」、右边「确认进入」。
120
+ 都靠右排会读成"两选一",而它们不是同一层意思(一个是不想被拦,一个是要这么做)。 */
121
+ .dsm-modal-actions-split{justify-content:space-between}.dsm-hidden-input{display:none}.dsm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease;cursor:pointer;font:inherit;text-align:center}.dsm-dropzone:hover,.dsm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:18px;font-weight:700}.dsm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dsm-upload-choices{display:flex;align-items:center;gap:7px}.dsm-upload-link,.dsm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dsm-upload-link{color:var(--dsw-alias-label-secondary)}.dsm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dsm-upload-link:disabled{opacity:.45;cursor:default}.dsm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dsm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dsm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dsm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dsm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dsm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dsm-upload-requirements{padding:1px 1px 0}.dsm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dsm-detail-section{gap:7px}.dsm-detail-path,.dsm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dsm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dsm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dsm-diag{padding:8px 10px;border-left:2px solid var(--dsm-warn);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dsm-trash-group-sub{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
108
122
  .dsm-modal-title-row{display:inline-flex;align-items:center;gap:9px;min-width:0}
109
123
  .dsm-pill{display:inline-flex;min-height:20px;align-items:center;padding:0 8px;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;font-size:11px;font-weight:500;white-space:nowrap}
110
124
  .dsm-pill-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
@@ -328,6 +342,25 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
328
342
  .dsm-inject-domains{display:flex;flex-wrap:wrap;gap:7px 18px;padding:0 2px}
329
343
  .dsm-inject-domain{display:inline-flex;align-items:center;gap:7px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}
330
344
  .dsm-inject-domain input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
345
+ /* 模型工具表:与「注入」同屏的第二种开关。**一行一个工具、四列固定宽度**(勾选框 / 名字 /
346
+ ≈tok / 一句话介绍)—— 列宽不固定就竖着扫不出任何一列(用户 2026-09-23 指出挤成一排很难看)。
347
+ 名字用等宽、数字用 tabular-nums 右对齐;介绍超长省略,完整那句在悬停里。 */
348
+ .dsm-tooltable{display:flex;flex-direction:column;gap:10px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}
349
+ /* 保存往返期间:整块不吃点击、略灰 —— 这一块刻意没有乐观更新(见 saveToolTable 注释)。 */
350
+ .dsm-tooltable-busy{pointer-events:none;opacity:.55}
351
+ .dsm-tooltable-summary{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}
352
+ .dsm-tooltable-group{display:flex;flex-direction:column;gap:3px}
353
+ .dsm-tooltable-ghead{display:flex;align-items:center;gap:7px;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;cursor:pointer}
354
+ .dsm-tooltable-ghead input[type=checkbox]{flex:none;width:15px;height:15px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
355
+ .dsm-tooltable-gname{font-weight:600}
356
+ .dsm-tooltable-gcount{color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}
357
+ .dsm-tooltable-tools{display:flex;flex-direction:column;padding-left:22px}
358
+ .dsm-tooltable-tool{display:grid;grid-template-columns:15px 186px 48px minmax(0,1fr);align-items:center;gap:0 8px;padding:2px 4px;border-radius:5px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:18px;cursor:pointer}
359
+ .dsm-tooltable-tool:hover{background:var(--dsw-alias-interactive-bg-hover)}
360
+ .dsm-tooltable-tool input[type=checkbox]{width:14px;height:14px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
361
+ .dsm-tooltable-name{overflow:hidden;color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
362
+ .dsm-tooltable-tok{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap}
363
+ .dsm-tooltable-about{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
331
364
  /* 注入实况(只读):与「注入」设置同屏。行 = 域 + 体积 + 状态胶囊,点域名展开正文。 */
332
365
  .dsm-inject-live-actions{display:flex;flex:none;align-items:center;gap:6px;margin-left:auto}
333
366
  .dsm-inject-live-toggle{padding:0;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer}
@@ -839,7 +872,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
839
872
  // 注入设置(本插件五个注入域的开关;见 src/context-inject.ts)。它和可达性矩阵是一体两面:
840
873
  // 矩阵说"到不到得了",这里决定"要不要"。读不到时这一节不显示。
841
874
  const [inject, setInject] = React.useState(null)
842
- // 注入实况(只读):最近活跃会话里模型**真正看到**的五域文本 + 本次运行的投递统计。
875
+ // 模型工具表(哪些工具整份不发给模型;见 src/tools/table.ts)。与「注入」是两种省法:
876
+ // 注入关掉省的是正文,这里关掉省的是工具 schema(工具表按每个请求付钱)。
877
+ const [toolTable, setToolTable] = React.useState(null)
878
+ // 保存往返期间把整块置灰(见 saveToolTable 的注释:不做乐观更新)。
879
+ const [toolTableBusy, setToolTableBusy] = React.useState(false)
880
+ // 注入实况(只读):最近活跃会话里模型**真正看到**的五域文本 + 那一段对话的投递统计。
843
881
  // 与「注入」设置是一体两面:设置说"要送什么",这里说"实际送到了什么"。
844
882
  const [live, setLive] = React.useState(null)
845
883
  const [liveOpen, setLiveOpen] = React.useState('')
@@ -1078,6 +1116,53 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1078
1116
  .then(function (r) { if (alive !== false && r && r.ok && r.settings) setInject(r.settings) })
1079
1117
  .catch(function () { /* 读不到 → 不渲染这一节 */ })
1080
1118
  }
1119
+ // 工具表实况(注册时量到的体积 + 关掉了哪些)。服务端返回的 `report` 自带分组与
1120
+ // ≈token 合计 —— 界面不自己算,也不缓存第二份数字。
1121
+ const applyToolTable = function (r, alive) {
1122
+ if (alive === false) return
1123
+ if (r && r.ok && r.report) setToolTable({ hidden: r.hidden || [], report: r.report })
1124
+ }
1125
+ const loadToolTable = function (alive) {
1126
+ apiCall('tool-table', {})
1127
+ .then(function (r) { applyToolTable(r, alive) })
1128
+ .catch(function () { /* 读不到 → 不渲染这一节 */ })
1129
+ }
1130
+ /**
1131
+ * 保存工具表开关。
1132
+ *
1133
+ * 刻意**不做乐观更新**(与上面的注入设置不同):勾选框状态与"关掉后每轮少发多少"
1134
+ * 这两个数字必须来自同一份服务端数据(注册时量到的体积),本地先改一个就会在
1135
+ * 往返的这几毫秒里出现"框已经勾了、合计还没变"的两种真相。这里是本机 HTTP,
1136
+ * 往返可以忽略,所以选择在等待期间把整块变灰。
1137
+ */
1138
+ const saveToolTable = function (names) {
1139
+ if (!toolTable || toolTableBusy) return
1140
+ setToolTableBusy(true)
1141
+ apiCall('tool-table', { set: true, hidden: names.slice() })
1142
+ .then(function (r) {
1143
+ if (r && r.ok && r.report) setToolTable({ hidden: r.hidden || names, report: r.report })
1144
+ else loadToolTable(true)
1145
+ // 功能总览那一行写着"一轮发出去多少",它得跟着变。
1146
+ loadFeatures(true)
1147
+ })
1148
+ .catch(function () { loadToolTable(true) })
1149
+ .then(function () { setToolTableBusy(false) })
1150
+ }
1151
+ /** 单个工具的勾选:在 hidden 名单里加 / 删一个名字。 */
1152
+ const toggleToolTableTool = function (name, on) {
1153
+ if (!toolTable) return
1154
+ const hid = (toolTable.hidden || []).filter(function (n) { return n !== name })
1155
+ if (!on) hid.push(name)
1156
+ saveToolTable(hid)
1157
+ }
1158
+ /** 整个域一起开 / 关(分组勾选框)。 */
1159
+ const toggleToolTableGroup = function (group, on) {
1160
+ if (!toolTable) return
1161
+ const names = group.tools.map(function (tool) { return tool.name })
1162
+ const hid = (toolTable.hidden || []).filter(function (n) { return names.indexOf(n) < 0 })
1163
+ if (!on) Array.prototype.push.apply(hid, names)
1164
+ saveToolTable(hid)
1165
+ }
1081
1166
  const loadLive = function (alive) {
1082
1167
  apiCall('injection-live', {})
1083
1168
  .then(function (r) { if (alive !== false && r && r.ok) setLive(r) })
@@ -1098,6 +1183,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1098
1183
  .catch(function (e) { if (alive) { setError(errMsg(e)); setBusy(false) } })
1099
1184
  loadReach(alive)
1100
1185
  loadInject(alive)
1186
+ loadToolTable(alive)
1101
1187
  loadLive(alive)
1102
1188
  loadFeatures(alive)
1103
1189
  loadToken(alive)
@@ -1111,6 +1197,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1111
1197
  .catch(function (e) { setError(errMsg(e)); setBusy(false) })
1112
1198
  loadReach(true)
1113
1199
  loadInject(true)
1200
+ loadToolTable(true)
1114
1201
  loadLive(true)
1115
1202
  loadFeatures(true)
1116
1203
  loadToken(true)
@@ -1472,6 +1559,57 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1472
1559
  React.createElement('span', null, t('compat.inject.domain.' + key)))
1473
1560
  })))))
1474
1561
  }
1562
+ // 模型工具表:与「注入」并列的第二种省法 —— 那边省正文,这边省工具 schema。
1563
+ // 工具表**每个请求都发一遍**(哪怕这一轮用不上),所以关掉的工具是整份不进请求,
1564
+ // 这是本插件省 token 最实在的一处;代价是模型调不到它(面板不受影响)。
1565
+ // 排版(用户 2026-09-23 指出太乱):**一行一个工具**、四列固定宽度 —— 勾选框 / 名字 /
1566
+ // ≈tok / 一句话介绍。挤成一排的小块每行宽度都不一样,竖着扫不出任何一列。
1567
+ // 文字两档:行内那句是"干什么用的"(统一动词开头),悬停给稍详细的一句(含边界与代价)。
1568
+ // 两档都按工具名去词典取,取不到就让那一格空着(`translateOrFallback`),
1569
+ // 绝不把 `compat.tools.about.x` 这种键名当文案显示出来。
1570
+ if (toolTable) {
1571
+ const report = toolTable.report
1572
+ const hidden = toolTable.hidden || []
1573
+ const groups = report.groups || []
1574
+ const groupLabel = function (key) { return key === 'other' ? t('compat.tools.other') : t('compat.inject.domain.' + key) }
1575
+ const rows = []
1576
+ groups.forEach(function (group) {
1577
+ const onCount = group.tools.filter(function (tool) { return !tool.hidden }).length
1578
+ rows.push(React.createElement('div', { className: 'dsm-tooltable-group', key: 'g-' + group.key },
1579
+ React.createElement('label', { className: 'dsm-tooltable-ghead' },
1580
+ React.createElement('input', {
1581
+ type: 'checkbox',
1582
+ checked: onCount === group.tools.length,
1583
+ // 半开半关时显示成"未全选",点一下 = 全开(与其余全选控件同款:勾选框只表达
1584
+ // 两种意思,第三种状态靠计数说清)。
1585
+ onChange: function () { toggleToolTableGroup(group, onCount !== group.tools.length) },
1586
+ }),
1587
+ React.createElement('span', { className: 'dsm-tooltable-gname' }, groupLabel(group.key)),
1588
+ React.createElement('span', { className: 'dsm-tooltable-gcount' }, t('compat.tools.groupCount', { on: onCount, total: group.tools.length, tok: group.tok }))),
1589
+ React.createElement('div', { className: 'dsm-tooltable-tools' },
1590
+ group.tools.map(function (tool) {
1591
+ const about = translateOrFallback(t, 'compat.tools.about.' + tool.name, '')
1592
+ // 悬停 = 稍详细那句 + 空行 + 当前状态。取不到详细那句时退回行内那句,
1593
+ // 于是"悬停什么也没有"不会发生。
1594
+ const hover = translateOrFallback(t, 'compat.tools.hover.' + tool.name, about)
1595
+ return React.createElement('label', {
1596
+ className: 'dsm-tooltable-tool',
1597
+ key: tool.name,
1598
+ title: (hover ? hover + '\n\n' : '') + (tool.hidden ? t('compat.tools.offTitle') : t('compat.tools.onTitle')),
1599
+ },
1600
+ React.createElement('input', { type: 'checkbox', checked: !tool.hidden, onChange: function () { toggleToolTableTool(tool.name, !!tool.hidden) } }),
1601
+ React.createElement('span', { className: 'dsm-tooltable-name' }, tool.name),
1602
+ React.createElement('span', { className: 'dsm-tooltable-tok' }, '≈' + tool.tok),
1603
+ React.createElement('span', { className: 'dsm-tooltable-about' }, about))
1604
+ }))))
1605
+ })
1606
+ push(section(t('compat.tools'), t('compat.tools.hint'),
1607
+ React.createElement('div', { className: 'dsm-tooltable' + (toolTableBusy ? ' dsm-tooltable-busy' : '') },
1608
+ React.createElement('div', { className: 'dsm-tooltable-summary' }, hidden.length === 0
1609
+ ? t('compat.tools.summaryAll', { total: report.totalCount, tok: report.totalTok })
1610
+ : t('compat.tools.summary', { on: report.visibleCount, total: report.totalCount, tok: report.visibleTok, saved: report.hiddenTok })),
1611
+ rows)))
1612
+ }
1475
1613
  // 注入实况:紧挨「注入」设置块下方。数据来自 `injection-live`(宿主读最近活跃会话的
1476
1614
  // 可见表面),是"回放"不是"重算"—— 界面显示已注入而模型实际没收到时(场景接管、
1477
1615
  // 压制型预设、开关被关、压缩后还没补发),这里一眼可见。
@@ -1537,17 +1675,30 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
1537
1675
  const deliveredText = delivered.count > 0
1538
1676
  ? t('compat.live.delivered', { count: delivered.count, time: new Date(delivered.lastAt).toTimeString().slice(0, 5) })
1539
1677
  : t('compat.live.never')
1540
- // 观测面:本进程到底看到过几次本插件的工具调用。必须显示 —— 采纳全是 0 时,
1678
+ // 观测面:这段对话里到底看到过几次本插件的工具调用。必须显示 —— 采纳全是 0 时,
1541
1679
  // 要能分清"模型真的没用"(结论)和"遥测没接上"(故障)。
1542
1680
  const observedCalls = Number((live.observed || {}).toolCalls) || 0
1543
1681
  const observedText = observedCalls > 0
1544
1682
  ? t('compat.live.observe.some', { count: observedCalls })
1545
1683
  : t('compat.live.observe.none')
1684
+ // 成本一行:总量 / 最费的域 / 重发最多的域。只在**真投过东西**时出现 ——
1685
+ // 全 0 时摆一行「累计约 0 B」是噪声(与上面 adoptText 的取向一致:没投过的域不报统计)。
1686
+ // 字节数是「当前正文 × 投递次数」的近似(服务端已注明),所以句子带「约」。
1687
+ const cost = live.cost || {}
1688
+ const costName = function (entry) { return entry ? (domainLabel(entry.key) || String(entry.label || '')) : '' }
1689
+ const costBytes = Number(cost.injectedBytes) || 0
1690
+ const costText = costBytes > 0 && cost.topDomain
1691
+ ? t('compat.live.cost', {
1692
+ size: fmtSize(costBytes),
1693
+ domain: costName(cost.topDomain),
1694
+ resent: costName(cost.mostDelivered),
1695
+ })
1696
+ : ''
1546
1697
  const canCopy = (live.domains || []).some(hasText)
1547
1698
  push(React.createElement('section', { className: 'dsm-compat-section' },
1548
1699
  React.createElement('div', { className: 'dsm-compat-section-head' },
1549
1700
  React.createElement('h3', { className: 'dsm-compat-section-title' }, t('compat.live.title')),
1550
- React.createElement('span', { className: 'dsm-compat-section-hint' }, t('compat.live.hint') + ' · ' + deliveredText + ' · ' + observedText),
1701
+ React.createElement('span', { className: 'dsm-compat-section-hint' }, t('compat.live.hint') + ' · ' + deliveredText + ' · ' + observedText + (costText ? ' · ' + costText : '')),
1551
1702
  React.createElement('span', { className: 'dsm-inject-live-actions' },
1552
1703
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: !canCopy, onClick: copyAll },
1553
1704
  liveCopied ? t('compat.live.copied') : t('compat.live.copy')))),
@@ -2131,6 +2282,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2131
2282
  "mcp.form.err.serverName": "服务名需为 1-32 位字母、数字、下划线或短横线(不能有空格与中文)", "mcp.form.err.url": "地址需以 http:// 或 https:// 开头", "mcp.form.err.command": "命令不能为空",
2132
2283
  "mcp.form.maskedHint": "密钥框里的打码值(如 ••••••)不是原文:直接保存不会改动它们,插件会保留补丁文件里的原值。要看原文请点「显示密钥」(本插件配了访问令牌的话,先填对令牌)。",
2133
2284
  "mcp.field.serverName": "服务名称 serverName", "mcp.field.serverName.hint": "1-32 位 [A-Za-z0-9_-],补丁里按此名注册",
2285
+ "mcp.field.note": "备注(可选)", "mcp.field.note.placeholder": "给模型看的决策提示,比如「这台要 sudo 免密」;留空不写",
2134
2286
  "mcp.field.transport": "传输方式", "mcp.field.level": "级别",
2135
2287
  "mcp.field.level.project": "应用级(仅当前应用:{path})", "mcp.field.level.global": "全局(跨应用:{path})",
2136
2288
  "mcp.field.url": "服务 URL", "mcp.field.url.hint": "需以 http(s):// 开头",
@@ -2236,8 +2388,58 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2236
2388
  "compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "提示词",
2237
2389
  "compat.inject.domain.skills": "技能",
2238
2390
  "compat.inject.domain.carrier.title": "关掉后,系统自带的那份也会一并停掉",
2391
+ "compat.tools": "模型工具表", "compat.tools.hint": "哪些工具发给模型:工具表每一轮都随请求发一遍,关掉的整份不发(≈ 是估算)",
2392
+ "compat.tools.summaryAll": "{total} 个工具全部下发 · ≈{tok} tok/轮",
2393
+ "compat.tools.summary": "下发 {on}/{total} 个 · ≈{tok} tok/轮(关掉的省下 ≈{saved})",
2394
+ "compat.tools.groupCount": "{on}/{total} 个 · ≈{tok}",
2395
+ "compat.tools.other": "其它",
2396
+ "compat.tools.onTitle": "已下发:模型能看到并调用它",
2397
+ "compat.tools.offTitle": "已关掉:整份不进请求,模型看不到也调不到(面板不受影响)",
2398
+ // 每个工具两档文案(行内一句 / 悬停稍详细)。格式统一:行内一律动词开头、不带句号;
2399
+ // 悬停补"什么时候用、会改到什么、边界在哪"。键按工具名拼(`compat.tools.about.<工具名>`),
2400
+ // 工具改名 = 换身份,这两组键要跟着改(取不到时界面留空、不显示键名)。
2401
+ "compat.tools.about.memory_manager_list": "列出记忆(默认只列会注入的)",
2402
+ "compat.tools.about.memory_manager_read": "按 id 读一条记忆的正文",
2403
+ "compat.tools.about.memory_manager_set_enabled": "启用或停用一条记忆",
2404
+ "compat.tools.about.memory_manager_write": "新建一条记忆",
2405
+ "compat.tools.about.memory_manager_update": "改记忆的正文、描述或名字",
2406
+ "compat.tools.about.mcp_manager_list": "列出服务器、状态与工具",
2407
+ "compat.tools.about.mcp_manager_set_enabled": "启用或停用服务器 / 单个工具",
2408
+ "compat.tools.about.mcp_manager_restart": "重启一台服务器",
2409
+ "compat.tools.about.mcp_manager_add": "新增一台 MCP 服务器",
2410
+ "compat.tools.about.skill_manager_list": "列出技能、启停与路径",
2411
+ "compat.tools.about.skill_manager_read": "按名字读技能正文",
2412
+ "compat.tools.about.skill_manager_set_enabled": "启用或停用技能 / 技能目录",
2413
+ "compat.tools.about.skill_manager_create": "新建一个技能",
2414
+ "compat.tools.about.subagent_manager_list": "列出人设与描述",
2415
+ "compat.tools.about.subagent_manager_run": "按人设跑一个子智能体",
2416
+ "compat.tools.about.subagent_manager_set_enabled": "启用或停用人设",
2417
+ "compat.tools.about.subagent_manager_create": "新建一个人设",
2418
+ "compat.tools.about.subagent_manager_update": "改人设(含改名)",
2419
+ "compat.tools.about.prompt_manager_list": "列出提示词预设",
2420
+ "compat.tools.about.prompt_manager_apply": "应用一份提示词预设",
2421
+ "compat.tools.hover.memory_manager_list": "列出记忆的 id、所属场景、启停与一句话描述。默认只列这次真的会注入的;all=true 会把停用、场景未启用、被同名覆盖的一起列出来。只看不改。",
2422
+ "compat.tools.hover.memory_manager_read": "按 id(<场景>/<名字>)读一条记忆的完整正文。清单里只有一句话描述,要看正文就用它 —— 尤其是被停用、场景没启用、或被体积预算挤掉的记忆。",
2423
+ "compat.tools.hover.memory_manager_set_enabled": "只拨索引里的开关,.md 文件一个字不动,随时能拨回来。停用的记忆不再进上下文,等于把注入预算让给别人 —— 想省 token 优先用它,而不是删。",
2424
+ "compat.tools.hover.memory_manager_write": "在某个场景下建一条记忆(场景要先存在;global 是常驻那个)。记忆会进之后每一轮请求,所以只记从代码、配置、git 推不出来的事;建之前先看清单里有没有已经讲了同一件事的。",
2425
+ "compat.tools.hover.memory_manager_update": "改一条现有记忆:正文、描述或改名(旧文件会留在回收站)。没传的字段保持原样;改之前先读一遍、只改要改的那部分,不要把整份重述一遍。",
2426
+ "compat.tools.hover.mcp_manager_list": "列出配好的服务器、连没连上、各自有哪些工具,以及你写给这台服务器的备注。曾经连上过、现在连不上的会标出来 —— 那说的是「这台有问题」,不是「没配」。",
2427
+ "compat.tools.hover.mcp_manager_set_enabled": "停用整台服务器,或只停它其中一个工具。停用的工具会从模型可见的工具表里消失,执行侧也拦住;重启 DSH 后仍然生效。",
2428
+ "compat.tools.hover.mcp_manager_restart": "把服务器停用、等它断开、再按原来的状态启用 —— 改了配置或服务器卡住时用它。过程中这台服务器的工具会短暂消失。",
2429
+ "compat.tools.hover.mcp_manager_add": "新增一台服务器(streamable-http 或 stdio),可带 URL、命令、参数、请求头、环境变量与备注,写到项目层或全局层。只在用户明确要求时用。",
2430
+ "compat.tools.hover.skill_manager_list": "列出技能的启停、生效还是被同名覆盖、源文件路径。常驻目录只带名字和摘要,所以「我怎么关了它」「同名哪份生效」要看这个;all=true 连停用与影子副本一起列。",
2431
+ "compat.tools.hover.skill_manager_read": "按名字读某个技能的 SKILL.md 正文(目录里只有名字和摘要)。同名多份时取真正生效的那一份;要特意看某个被覆盖的副本就传 root。会话里已有官方 skill 工具时这条不下发。",
2432
+ "compat.tools.hover.skill_manager_set_enabled": "按技能名或按来源目录启停。停用的技能不进目录也不能被加载;同名冲突时只有生效的那份有用 —— 启错了副本等于没启。",
2433
+ "compat.tools.hover.skill_manager_create": "在指定来源目录下新建技能(名字、描述、SKILL.md 正文)。技能目录会进每一轮上下文,描述写清「什么时候用它」,模型才找得到。",
2434
+ "compat.tools.hover.subagent_manager_list": "列出可用人设(子智能体的角色定义)与各自的描述。常驻目录给的是同一份清单;这个工具拿到的永远是当前完整版,包括目录没注入到的会话。",
2435
+ "compat.tools.hover.subagent_manager_run": "把任务交给某个具名人设:它拿到人设当自己的系统提示词、在自己的上下文里做完、只回最终结果。开 inherit 它会看到本次会话已完成的对话。花真 token,默认要用户确认。",
2436
+ "compat.tools.hover.subagent_manager_set_enabled": "停用的人设不进目录也不能被委派;被场景绑定的人设,在进入该场景时会自动启用。",
2437
+ "compat.tools.hover.subagent_manager_create": "新建人设:名字、描述、正文(会作为子代理的系统提示词),可带 provider、模型与工具限制。描述决定模型什么时候会选它。",
2438
+ "compat.tools.hover.subagent_manager_update": "改一个人设:正文、描述、provider、模型、工具限制,或改名(场景与档案里的绑定会跟着改)。没传的字段保持原样。",
2439
+ "compat.tools.hover.prompt_manager_list": "列出提示词预设与摘要,标明当前生效的是哪一份 —— 同时只有一份生效。要换用哪份之前先用它确认名字。",
2440
+ "compat.tools.hover.prompt_manager_apply": "把某份预设应用到用户级 AGENTS.md,之后的对话立刻按它走。一次只有一份在生效,换一份就是替换上一份。",
2239
2441
  "compat.live.title": "注入实况", "compat.live.hint": "模型实际看到的内容(只读)",
2240
- "compat.live.never": "本次运行未投递", "compat.live.delivered": "已投递 {count} 条 · 最近 {time}",
2442
+ "compat.live.never": "这段对话还没投过", "compat.live.delivered": "本段对话已投递 {count} 条 · 最近 {time}",
2241
2443
  "compat.live.copy": "复制全文", "compat.live.copied": "已复制", "compat.live.noAgent": "还没有会话:发一条消息后再来看",
2242
2444
  "compat.live.state.inContext": "已注入", "compat.live.state.absent": "未投递", "compat.live.state.cleared": "已清空",
2243
2445
  "compat.live.state.off": "已关闭", "compat.live.state.unknown": "没有会话",
@@ -2245,9 +2447,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2245
2447
  "compat.live.state.child.title": "本会话的深度超过了人设的目录注入深度(catalogDepth),所以常驻目录不在这里注入。要让目录出现在子会话,把对应人设的「目录注入」调大(或选「不限制嵌套」)。注意:这不影响委派 —— 子代理始终可以继续委派。",
2246
2448
  "compat.live.adopt": "注入 {injected} 次 · 调用 {used} 次 · 采纳 {adopted} 次",
2247
2449
  "compat.live.adopt.none": "注入 {injected} 次 · 从未调用",
2248
- "compat.live.adopt.title": "本进程运行以来的计数。采纳 = 调用发生时该域正文正在这个会话的上下文里;调用次数不区分成功失败",
2249
- "compat.live.observe.some": "本进程观测到 {count} 次调用",
2250
- "compat.live.observe.none": "本进程未观测到调用",
2450
+ "compat.live.adopt.title": "只数**最近这一段对话**(换一段重新数,重启归零)。采纳 = 调用发生时该域正文正在这个会话的上下文里;调用次数不区分成功失败",
2451
+ "compat.live.observe.some": "这段对话观测到 {count} 次调用",
2452
+ "compat.live.observe.none": "这段对话还没观测到调用",
2453
+ "compat.live.cost": "本段对话累计约 {size} · 最费 {domain} · 重发最多 {resent}",
2251
2454
  "compat.live.state.official": "官方注入", "compat.live.state.official.title": "提示词与技能目录在标准类预设下由官方通道送达,本插件不重复注入(极简这类官方没挂的预设才由本插件兜底)",
2252
2455
  "compat.live.state.empty": "无内容", "compat.live.est": "≈{count} token",
2253
2456
  "compat.live.state.absent.title": "本插件负责这个域,但当前不在上下文里(可能刚被压缩,下一步会自动补发)",
@@ -2264,7 +2467,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2264
2467
  "scenes.noDesc": "还没有描述",
2265
2468
  "scenes.mode.active": "当前模式:{name}", "scenes.mode.profile": "档案:{parts}", "scenes.mode.noProfile": "这个场景还没有档案",
2266
2469
  "scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
2267
- "scenes.field.desc.limit": "最多 {count} 字;超出部分在卡片上省略。",
2470
+ "scenes.search.placeholder": "搜索场景名或描述", "scenes.empty.search": "没有匹配的场景",
2268
2471
  "scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
2269
2472
  "scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。没勾的、以及整段没建的,进场景时一律停用(含停掉正在运行的进程)。",
2270
2473
  "scenes.seg.checked": "{checked}/{total} 已勾选",
@@ -2328,6 +2531,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2328
2531
  "memory.attach.count.one": "{count} 个附件", "memory.attach.count.other": "{count} 个附件",
2329
2532
  "memory.attach.tip": "点击管理附件:{count} 个 · 共 {size} —— {names}",
2330
2533
  "memory.attach.tip.empty": "还没有附件——点击添加",
2534
+ "memory.size.tip": "正文 {size}:启用后每一轮对话都要占这么多上下文",
2331
2535
  "memory.attach.more": ",另 {count} 个", "memory.attach.sep": "、", "memory.attach.flat": "flat 是单文件,不能带附件", "memory.attach.tooLarge": "附件过大:{name}(上限 {limit} MB)",
2332
2536
  "memory.delete.title": "移入回收站?", "memory.delete.desc": "「{name}」将移入回收站,可恢复",
2333
2537
  "memory.result.created": "已创建记忆:{name}", "memory.result.updated": "已保存记忆:{name}", "memory.result.removed": "已移入回收站:{name}", "memory.result.toggled": "已更新记忆状态:{name}", "memory.result.active": "已更新启用场景",
@@ -2387,7 +2591,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2387
2591
  "import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
2388
2592
  "error.import.noFiles": "没有选择要导入的文件",
2389
2593
  "nav.title": "工具",
2390
- "agm.stat.total": "个提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "agm.field.desc": "描述(只给使用者看)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用场景即以它为全局基线。\n启用期间在提示词页「应用」别的预设 = 改绑到那一份(同步写进档案)。", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "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": "未知错误",
2594
+ "agm.stat.total": "个提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.btn.reapply": "重新应用", "agm.btn.apply": "应用", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.copyFrom": "或从现有预设复制", "agm.field.copyFrom.none": "不复制(空白模板)", "agm.field.desc": "描述(只给使用者看)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.field.prompt.hint": "启用场景即以它为全局基线。\n启用期间在提示词页「应用」别的预设 = 改绑到那一份(同步写进档案)。", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按它恢复——删了就没法恢复了", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点「重新应用」把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "项目目录(可留空)", "hist.import.cwd.placeholder": "绝对路径,可留空(会话归入未分组)", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.result.archived": "已归档 {count} 个会话到 History,可到 History 页继续管理", "hist.result.archiveFailed": ";其中 {count} 个未能归档", "hist.btn.archiveSelected": "归档所选", "hist.btn.archiveSelected.title": "把选中的会话收进 History,纳入保留期管理", "hist.export.busy": "处理中…", "hist.btn.exportSelected": "导出 {count} 个会话", "hist.error.title": "操作失败", "hist.error.unknown": "未知错误",
2391
2595
  },
2392
2596
  en: {
2393
2597
  "title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
@@ -2478,6 +2682,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2478
2682
  "mcp.form.err.serverName": "Server name must be 1-32 chars of letters, digits, underscore or hyphen (no spaces, no CJK)", "mcp.form.err.url": "URL must start with http:// or https://", "mcp.form.err.command": "Command cannot be empty",
2479
2683
  "mcp.form.maskedHint": "A masked value in a secret field (e.g. ••••••) is not the real value: saving leaves those entries untouched, because the plugin writes back the value found in the patch file. To read a secret in plain text, click Reveal secrets (if this plugin has an access token configured, enter the matching token first).",
2480
2684
  "mcp.field.serverName": "Server name (serverName)", "mcp.field.serverName.hint": "1-32 chars of [A-Za-z0-9_-]; the patch registers it under this name",
2685
+ "mcp.field.note": "Note (optional)", "mcp.field.note.placeholder": "A hint for the model, e.g. \“this one needs passwordless sudo\”; empty means nothing is stored",
2481
2686
  "mcp.field.transport": "Transport", "mcp.field.level": "Level",
2482
2687
  "mcp.field.level.project": "Profile level (this app: {path})", "mcp.field.level.global": "Global (across profiles: {path})",
2483
2688
  "mcp.field.url": "Server URL", "mcp.field.url.hint": "Must start with http(s)://",
@@ -2581,8 +2786,58 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2581
2786
  "compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Prompt",
2582
2787
  "compat.inject.domain.skills": "Skills",
2583
2788
  "compat.inject.domain.carrier.title": "Turning this off also stops the copy the system sends by itself",
2789
+ "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)",
2790
+ "compat.tools.summaryAll": "all {total} tools sent · ≈{tok} tok per turn",
2791
+ "compat.tools.summary": "{on}/{total} sent · ≈{tok} tok per turn ({saved} tok saved)",
2792
+ "compat.tools.groupCount": "{on}/{total} · ≈{tok}",
2793
+ "compat.tools.other": "Other",
2794
+ "compat.tools.onTitle": "Sent: the model can see and call it",
2795
+ "compat.tools.offTitle": "Switched off: not sent at all, so the model cannot see or call it (the panel is unaffected)",
2796
+ // Two tiers per tool (inline one-liner / slightly longer hover). Same shape everywhere:
2797
+ // the inline line starts with a verb and has no full stop; the hover adds when to use it,
2798
+ // what it changes and where the limit is. Keys are built from the tool name.
2799
+ "compat.tools.about.memory_manager_list": "List memories (only the injected ones by default)",
2800
+ "compat.tools.about.memory_manager_read": "Read one memory's full body by id",
2801
+ "compat.tools.about.memory_manager_set_enabled": "Enable or disable one memory",
2802
+ "compat.tools.about.memory_manager_write": "Create a memory",
2803
+ "compat.tools.about.memory_manager_update": "Update a memory (body, description, name)",
2804
+ "compat.tools.about.mcp_manager_list": "List servers, state and tools",
2805
+ "compat.tools.about.mcp_manager_set_enabled": "Enable or disable a server or one tool",
2806
+ "compat.tools.about.mcp_manager_restart": "Restart a server",
2807
+ "compat.tools.about.mcp_manager_add": "Add an MCP server",
2808
+ "compat.tools.about.skill_manager_list": "List skills with state and path",
2809
+ "compat.tools.about.skill_manager_read": "Read a skill body by name",
2810
+ "compat.tools.about.skill_manager_set_enabled": "Enable or disable a skill or source",
2811
+ "compat.tools.about.skill_manager_create": "Create a skill",
2812
+ "compat.tools.about.subagent_manager_list": "List personas and descriptions",
2813
+ "compat.tools.about.subagent_manager_run": "Run a persona as a subagent",
2814
+ "compat.tools.about.subagent_manager_set_enabled": "Enable or disable a persona",
2815
+ "compat.tools.about.subagent_manager_create": "Create a persona",
2816
+ "compat.tools.about.subagent_manager_update": "Update a persona (can rename)",
2817
+ "compat.tools.about.prompt_manager_list": "List prompt presets",
2818
+ "compat.tools.about.prompt_manager_apply": "Apply a prompt preset",
2819
+ "compat.tools.hover.memory_manager_list": "Lists each memory's id, scene, on/off state and one-line description. Only the ones that would actually be injected by default; all=true adds disabled, inactive-scene and shadowed entries. Read-only.",
2820
+ "compat.tools.hover.memory_manager_read": "Reads the full body of one memory by id (<scene>/<name>). The list carries a one-line description only — use this for the body, especially for memories that are off, in an inactive scene, or dropped by the byte budget.",
2821
+ "compat.tools.hover.memory_manager_set_enabled": "Flips the index switch only — the .md file is untouched and it is reversible. A disabled memory stops being injected, which frees byte budget for the others; prefer this over deleting when you want to spend fewer tokens.",
2822
+ "compat.tools.hover.memory_manager_write": "Creates a memory in a scene (the scene must exist; global is the always-on one). Memories enter every later request, so record only what cannot be re-derived from code, config or git — and check the list first for an entry that already says this.",
2823
+ "compat.tools.hover.memory_manager_update": "Updates an existing memory: body, description, or rename (the old file stays in the trash). Omitted fields are kept as they are; read it first and change only the part that needs changing.",
2824
+ "compat.tools.hover.mcp_manager_list": "Lists the configured servers, whether each is connected, which tools it exposes, and the note you wrote for it. Servers that connected before but not now are marked — that means this one is broken, not unconfigured.",
2825
+ "compat.tools.hover.mcp_manager_set_enabled": "Disables a whole server, or a single tool on it. A disabled tool disappears from the model-visible tool table and is blocked at execution; this survives a DSH restart.",
2826
+ "compat.tools.hover.mcp_manager_restart": "Disables the server, waits for it to drop, then enables it back the way it was — use it after a config change or when a server is stuck. Its tools disappear briefly during the restart.",
2827
+ "compat.tools.hover.mcp_manager_add": "Adds a server (streamable-http or stdio) with URL, command, args, headers, env and a note, at project or global level. Only on the user's explicit request.",
2828
+ "compat.tools.hover.skill_manager_list": "Lists each skill's on/off state, whether it wins or is shadowed, and its source path. The always-on catalog carries names and summaries only — use this to see what you turned off and which copy of a name wins; all=true includes disabled and shadowed copies.",
2829
+ "compat.tools.hover.skill_manager_read": "Reads one skill's SKILL.md body by name (the catalog has names and summaries only). Name collisions resolve to the copy actually in effect; pass root to inspect a shadowed copy instead. Not sent in conversations where the official skill tool is mounted.",
2830
+ "compat.tools.hover.skill_manager_set_enabled": "Toggles by skill name or by source directory. Disabled skills leave the catalog and cannot be loaded; when a name exists in several roots only the winning copy counts, so enabling the wrong one changes nothing.",
2831
+ "compat.tools.hover.skill_manager_create": "Creates a skill in the given source (name, description, SKILL.md body). The catalog is injected into every request, so make the description say when to use it — that is how the model finds it.",
2832
+ "compat.tools.hover.subagent_manager_list": "Lists the available personas (pre-configured subagent roles) with their descriptions. The always-on catalog carries the same list; this tool always returns the current, complete version — including conversations the catalog does not reach.",
2833
+ "compat.tools.hover.subagent_manager_run": "Hands a task to a named persona: it gets the persona as its own system prompt, works in its own context and returns only the final result. With inherit it also sees this conversation's finished turns. It spends real tokens and asks the user by default.",
2834
+ "compat.tools.hover.subagent_manager_set_enabled": "A disabled persona leaves the catalog and cannot be delegated to; personas bound to a scene are enabled automatically when that scene is entered.",
2835
+ "compat.tools.hover.subagent_manager_create": "Creates a persona: name, description and body (the body becomes the subagent's system prompt), plus optional provider, model and tool limits. The description decides when the model picks it.",
2836
+ "compat.tools.hover.subagent_manager_update": "Updates a persona: body, description, provider, model, tool limits, or a rename (scene and archive bindings follow the rename). Omitted fields are kept as they are.",
2837
+ "compat.tools.hover.prompt_manager_list": "Lists the prompt presets with their summaries and marks the one currently in effect — only one is active at a time. Use it to confirm the name before switching.",
2838
+ "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.",
2584
2839
  "compat.live.title": "Live injection", "compat.live.hint": "What the model actually sees (read-only)",
2585
- "compat.live.never": "nothing delivered yet this run", "compat.live.delivered": "{count} delivered · last {time}",
2840
+ "compat.live.never": "nothing delivered in this conversation yet", "compat.live.delivered": "{count} delivered in this conversation · last {time}",
2586
2841
  "compat.live.copy": "Copy all", "compat.live.copied": "Copied", "compat.live.noAgent": "No session yet — send a message first",
2587
2842
  "compat.live.state.inContext": "Injected", "compat.live.state.absent": "Not delivered", "compat.live.state.cleared": "Cleared",
2588
2843
  "compat.live.state.off": "Off", "compat.live.state.unknown": "No session",
@@ -2590,9 +2845,10 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2590
2845
  "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.",
2591
2846
  "compat.live.adopt": "injected {injected} · called {used} · adopted {adopted}",
2592
2847
  "compat.live.adopt.none": "injected {injected} · never called",
2593
- "compat.live.adopt.title": "Counts since this process started. Adopted = the domain text was in context when the call happened; calls are counted whether or not they succeeded",
2594
- "compat.live.observe.some": "{count} tool calls observed this run",
2595
- "compat.live.observe.none": "no tool calls observed yet this run",
2848
+ "compat.live.adopt.title": "Counts the most recent conversation only (it restarts when you switch, and on every launch). Adopted = the domain text was in context when the call happened; calls are counted whether or not they succeeded",
2849
+ "compat.live.observe.some": "{count} tool calls observed in this conversation",
2850
+ "compat.live.observe.none": "no tool calls observed in this conversation yet",
2851
+ "compat.live.cost": "≈{size} this conversation · biggest: {domain} · most resent: {resent}",
2596
2852
  "compat.live.state.official": "Official", "compat.live.state.official.title": "The official carrier delivers these under standard presets; the plugin does not duplicate (it only fills in for Minimal-like presets)",
2597
2853
  "compat.live.state.empty": "Nothing to send", "compat.live.est": "≈{count} tokens",
2598
2854
  "compat.live.state.absent.title": "This plugin owns this domain but it is not in context right now (it may have just been compacted; the next step re-sends it)",
@@ -2609,7 +2865,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2609
2865
  "scenes.noDesc": "No description yet",
2610
2866
  "scenes.mode.active": "Active mode: {name}", "scenes.mode.profile": "Profile: {parts}", "scenes.mode.noProfile": "This scene has no profile yet",
2611
2867
  "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.",
2612
- "scenes.field.desc.limit": "Up to {count} characters; longer text is clipped on the card.",
2868
+ "scenes.search.placeholder": "Search scene names or descriptions", "scenes.empty.search": "No matching scenes",
2613
2869
  "scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
2614
2870
  "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.",
2615
2871
  "scenes.seg.checked": "{checked}/{total} selected",
@@ -2673,6 +2929,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2673
2929
  "memory.attach.count.one": "{count} attachment", "memory.attach.count.other": "{count} attachments",
2674
2930
  "memory.attach.tip": "Click to manage attachments: {count} · {size} total — {names}",
2675
2931
  "memory.attach.tip.empty": "No files yet — click to add",
2932
+ "memory.size.tip": "{size} of body: once enabled it costs this much context on every turn",
2676
2933
  "memory.attach.more": ", +{count} more", "memory.attach.sep": ", ", "memory.attach.flat": "The flat form is a single file and cannot carry attachments", "memory.attach.tooLarge": "File too large: {name} (max {limit} MB)",
2677
2934
  "memory.delete.title": "Move to trash?", "memory.delete.desc": "“{name}” moves to trash and can be restored",
2678
2935
  "memory.result.created": "Created memory: {name}", "memory.result.updated": "Saved memory: {name}", "memory.result.removed": "Moved to trash: {name}", "memory.result.toggled": "Memory state updated: {name}", "memory.result.active": "Enabled scenes updated",
@@ -2731,7 +2988,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
2731
2988
  "import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
2732
2989
  "error.import.noFiles": "No files selected",
2733
2990
  "nav.title": "Tools",
2734
- "agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (for humans only)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.field.prompt.hint": "While the scene is on, this preset is the global baseline.\nApplying another preset on the Prompts page rebinds the scene to it (written into the profile too).", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "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",
2991
+ "agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (for humans only)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.field.prompt.hint": "While the scene is on, this preset is the global baseline.\nApplying another preset on the Prompts page rebinds the scene to it (written into the profile too).", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); click “Apply again” to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
2735
2992
  }
2736
2993
  };
2737
2994
 
@@ -3084,7 +3341,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3084
3341
  ? { kind: 'warn', text: mt('mcp.msg.warn', { warning: res.warning }) }
3085
3342
  : { kind: 'ok', text: mt('mcp.msg.ok') }
3086
3343
  )
3087
- const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'global' })
3344
+ const emptyForm = () => ({ serverName: '', transport: 'streamable-http', url: '', command: '', args: '', headers: '', env: '', level: 'global', note: '' })
3088
3345
  const kvToLines = (obj) => (obj ? Object.keys(obj).map((k) => k + '=' + obj[k]).join('\n') : '')
3089
3346
  // 级别筛选选项:必须在**渲染时**取词。原来写成模块级常量,apply() 早于 locale
3090
3347
  // 注册,导致英文界面下这四项永远显示中文(且再也翻不过来)。
@@ -3311,6 +3568,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3311
3568
  env: payload.env,
3312
3569
  }
3313
3570
  if (payload.mode === 'edit') args.id = payload.id
3571
+ // 备注只在新增时带:服务端在写补丁的同一次锁里落进侧车(见 mcpmAdd)。
3572
+ else args.note = payload.note
3314
3573
  run(payload.mode === 'edit' ? 'mcpm-edit' : 'mcpm-add', args, 'form', closeForm)
3315
3574
  }
3316
3575
 
@@ -3642,7 +3901,15 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3642
3901
  ? React.createElement('label', { className: 'dsm-field' },
3643
3902
  React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.headers')),
3644
3903
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: formModal.headers, placeholder: 'Authorization=Bearer xxx', onChange: setFormField('headers') }))
3645
- : null),
3904
+ : null,
3905
+ // 备注放在表单**最下面**:它是可选项,说的又是"这台怎么用",不该插在必填的连接参数中间。
3906
+ // 默认不填 = 侧车里不产生条目;只在新增时出现(`mcpm-edit` 不碰侧车,改备注走详情里那枚
3907
+ // 输入框)。上限就是注入段的截断长度 `MCP_NOTE_MAX` —— 写多少模型就能读到多少。
3908
+ formModal.mode === 'edit' ? null : React.createElement('label', { className: 'dsm-field' },
3909
+ React.createElement('div', { className: 'dsm-budget-meta' },
3910
+ React.createElement('span', { className: 'dsm-label' }, mt('mcp.field.note')),
3911
+ React.createElement('span', { className: 'dsm-char-count' }, String(formModal.note || '').length + ' / ' + MCP_NOTE_MAX)),
3912
+ React.createElement('input', { className: 'dsm-control', value: formModal.note || '', maxLength: MCP_NOTE_MAX, placeholder: mt('mcp.field.note.placeholder'), onChange: setFormField('note') }))),
3646
3913
  React.createElement('div', { className: 'dsm-modal-actions' },
3647
3914
  React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy === 'form' || !formModal.serverName.trim(), onClick: submitForm }, formModal.mode === 'edit' ? mt('mcp.btn.save') : mt('mcp.btn.add'))))
3648
3915
 
@@ -3684,8 +3951,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
3684
3951
  React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.status')),
3685
3952
  React.createElement('div', { className: 'dsm-feedback' + (detailStatus.cls === 'dsm-failed' ? ' dsm-error' : ' dsm-warning') }, detailHint)) : null,
3686
3953
  React.createElement('div', { className: 'dsm-detail-section' },
3687
- React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
3688
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, maxLength: 200, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
3954
+ // 字数计数摆在小标题同一行右侧:备注的上限就是注入段的截断长度(`MCP_NOTE_MAX`)。
3955
+ // 超限标红沿用记忆页描述那套(`dsm-char-over`)—— 服务端不校验备注长度,
3956
+ // 这条之前存的长备注会整条存着、注入时被截掉,得让人看得见超了多少。
3957
+ React.createElement('div', { className: 'dsm-budget-meta' },
3958
+ React.createElement('div', { className: 'dsm-detail-title' }, mt('mcp.detail.note')),
3959
+ React.createElement('span', { className: 'dsm-char-count' + (String(noteDraft || '').length > MCP_NOTE_MAX ? ' dsm-char-over' : '') }, String(noteDraft || '').length + ' / ' + MCP_NOTE_MAX)),
3960
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: noteDraft, maxLength: MCP_NOTE_MAX, placeholder: mt('mcp.detail.note.placeholder'), onChange: (ev) => setNoteDraft(ev.target.value) }),
3689
3961
  React.createElement('div', { className: 'dsm-modal-actions' },
3690
3962
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || noteDraft.trim() === String(detail.row.notes || ''), onClick: saveNote }, mt('mcp.detail.note.save')))),
3691
3963
  React.createElement('div', { className: 'dsm-detail-section' },
@@ -4203,10 +4475,44 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4203
4475
  var el = document.getElementById('dsm-tab-' + TABS[next][0])
4204
4476
  if (el && el.focus) el.focus()
4205
4477
  }
4478
+ // 键盘导航第一步:`/` 聚焦当前页的搜索框、Esc 清空它 —— 只做这两个键。
4479
+ //
4480
+ // 为什么挂在 `section` 上而不是 `window`:本插件只是宿主设置面板里的一个块,
4481
+ // 全局监听会抢走宿主自己的键。挂在根上则只覆盖我们自己的 UI(焦点落在哪一页
4482
+ // 就筛哪一页),也不需要在 effect 里管监听器的增删。
4483
+ var onSectionKeyDown = function (e) {
4484
+ var el = e.target
4485
+ if (!el || typeof el.closest !== 'function') return
4486
+ // 弹窗开着时整个放行:Esc 关窗与 Tab 焦点陷阱由 Modal 自己处理(见 trapModalFocus)。
4487
+ if (el.closest('.dsm-modal')) return
4488
+ if (e.key === '/') {
4489
+ // 焦点已在任意文本控件里 → `/` 是用户要打的字符,不是快捷键。
4490
+ if (el.closest('input, textarea, select, [contenteditable]')) return
4491
+ var boxes = sectionRef.current ? sectionRef.current.querySelectorAll('input.dsm-search') : []
4492
+ for (var i = 0; i < boxes.length; i += 1) {
4493
+ // 只挑真正可见的那个:MCP 页的弹窗里还藏着一枚搜索框,聚焦到
4494
+ // `display:none` 的子树里等于把焦点弄丢(用户只会觉得"按了没反应")。
4495
+ var box = /** @type {HTMLInputElement} */ (boxes[i])
4496
+ if (box.offsetParent !== null) { e.preventDefault(); box.focus(); return }
4497
+ }
4498
+ return
4499
+ }
4500
+ if (e.key !== 'Escape') return
4501
+ var search = /** @type {HTMLInputElement} */ (el.closest('input.dsm-search'))
4502
+ // 已经是空的就不拦:让这次 Esc 继续往宿主冒(关设置窗口),别让快捷键吃掉别的行为。
4503
+ if (!search || !search.value) return
4504
+ // 清空要走原生 setter 再复演一次 input 事件:直接改 `search.value` 不触发 React 的
4505
+ // onChange,界面上看着清了、过滤条件还留着(列表仍是筛过的)。
4506
+ var desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
4507
+ if (!desc || !desc.set) return
4508
+ e.preventDefault()
4509
+ desc.set.call(search, '')
4510
+ search.dispatchEvent(new Event('input', { bubbles: true }))
4511
+ }
4206
4512
  // 子节点用**带 key 的数组**给(而不是并列的三个实参):并列实参会退化成按位置
4207
4513
  // 认子节点,令牌提示一出现/消失就把 `page` 换一个位置 → React 把整页卸载重挂
4208
4514
  // (页面状态与列表全丢,看起来像"页面自己刷新了")。带 key 之后位置永远稳定。
4209
- return React.createElement('section', { ref: sectionRef, className: 'dsm-section' }, [
4515
+ return React.createElement('section', { ref: sectionRef, className: 'dsm-section', onKeyDown: onSectionKeyDown }, [
4210
4516
  React.createElement('div', { key: 'tabs', className: 'dsm-tabs-shell' },
4211
4517
  React.createElement('div', { className: 'dsm-tabs', role: 'tablist', 'aria-label': t('nav.title'), onKeyDown: onTabKeyDown },
4212
4518
  TABS.map(function (entry) {
@@ -4269,7 +4575,6 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
4269
4575
 
4270
4576
 
4271
4577
 
4272
- function translateOrFallback(t, key, fallback, params) { var value = t(key, params); return typeof value === "string" && value !== key ? value : fallback; }
4273
4578
  /**
4274
4579
  * 字段下的多行说明 → 条目清单(与导入弹窗的「文件要求」同款样式,见 dsm-help-list)。
4275
4580
  * 词典值按 `\n` 分段,一段一条:一整句话里塞三件事时,条目比句子好扫。
@@ -4486,6 +4791,15 @@ function enabledMemoryCount(rules) {
4486
4791
  /** 已移除的来源(不在主列表里,但要让用户看得到并恢复)。 */
4487
4792
  function removedSkillRoots(roots) { return (roots || []).filter(function (root) { return root.removed === true; }); }
4488
4793
  var MAX_UPLOAD_ARCHIVE_BYTES = 32 << 20, MAX_UPLOAD_ENTRY_BYTES = 32 << 20, MAX_UPLOAD_TOTAL_BYTES = 64 << 20, MAX_UPLOAD_ENTRIES = 1000;
4794
+ /**
4795
+ * 注入段里"模型实际能看到多长"的两个截断长度,同时当输入框的 `maxLength` 与字数计数的分母
4796
+ * —— 写多少就能被看到多少,不会出现"界面让你写 500 字、模型只读到前 300"。
4797
+ *
4798
+ * ⚠️ 宿主侧各有一份真相:`src/mcp/state-section.ts` 的 `DEFAULT_MCP_NOTE_MAX_LENGTH`、
4799
+ * `src/skills/catalog.ts` 与 `src/subagents/catalog.ts` 的 `..._DESCRIPTION_MAX_LENGTH`。
4800
+ * 两边没有编译期约束(计划文档 A3 说的"同一事实两处写"),改一处必须改另一处。
4801
+ */
4802
+ var MCP_NOTE_MAX = 300, CATALOG_DESC_MAX = 500;
4489
4803
  function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
4490
4804
  function inspectUploadSelection(files) {
4491
4805
  var list = Array.prototype.slice.call(files || []); if (!list.length) return null;
@@ -5373,7 +5687,7 @@ function enabledMemoryCount(rules) {
5373
5687
  (!anyLocked && sceneName) ? h("div", { key: "scenebanner", className: "dsm-feedback dsm-warning", role: "status" }, t("scene.switch.banner", { scene: sceneName })) : null,
5374
5688
  h("div", { key: "summary", className: "dsm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.issues, "summary.issues"]].map(function (item) { return h("div", { key: item[1], className: "dsm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dsm-filters" }, h("input", { className: "dsm-control dsm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dsm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), undo && undo.scope == null ? h(UndoBar, { key: "undo", undo: undo, busy: busy, t: t, onUndo: undoBulk, onClose: dismissUndo }) : null, result && modal !== "import" ? h(Notice, { key: "result", kind: result.warning ? "warn" : result.ok ? "ok" : "err", text: result.text }) : null].concat((data.warnings || []).map(function (warning, index) { return h(Notice, { key: "warning-" + index, kind: "warn", text: translateError(t, warning) }); }), [snapshot.error ? h(Notice, { key: "error", kind: "err", text: snapshot.error }) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dsm-empty" }, t("loading")) : h("div", { key: "sources", className: "dsm-sources", ref: flipRef }, roots.map(renderRoot))]);
5375
5689
 
5376
- if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dsm-field" }, h("span", { className: "dsm-label" }, t(field[1])), h("input", { className: "dsm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.body")), h("textarea", { className: "dsm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dsm-help" }, t("create.chat.note"))), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
5690
+ if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dsm-form" }, h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dsm-field" }, h("div", { className: "dsm-budget-meta" }, h("span", { className: "dsm-label" }, t(field[1])), field[0] === "description" ? h("span", { className: "dsm-char-count" }, String(form.description || "").length + " / " + CATALOG_DESC_MAX) : null), h("input", { className: "dsm-control", value: form[field[0]], placeholder: t(field[2]), maxLength: field[0] === "description" ? CATALOG_DESC_MAX : undefined, onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dsm-field" }, h("span", { className: "dsm-label" }, t("create.body")), h("textarea", { className: "dsm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dsm-help" }, t("create.chat.note"))), h("div", { className: "dsm-modal-actions" }, h("button", { className: "dsm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
5377
5691
  // D13:导入弹窗统一用共享 ImportModal(能力取三者并集:拖拽区 + 已选文件行 +
5378
5692
  // 文件要求清单 + 结果反馈)。技能页只保留自己的校验与提交逻辑。
5379
5693
  if (modal === "import") content.push(h(ImportModal, {
@@ -6066,13 +6380,12 @@ function enabledMemoryCount(rules) {
6066
6380
  }
6067
6381
 
6068
6382
  /**
6069
- * 场景描述在卡片上最多显示多少个字。
6383
+ * 场景描述最多存多少个字(新建/编辑表单的 `maxLength` 与字数计数都用它)。
6070
6384
  *
6071
- * 描述是自由文本,卡片只留一行:不裁的话一个长描述会把卡片撑成纵向(整页高度失控),
6072
- * 保留场景「全局」尤其明显——它的名字只有两个字,剩下的宽度全被描述占满。
6073
- * 上限同时写在新建/编辑表单的 `maxLength` 上(见场景表单),显示层再裁一次兜住历史数据。
6385
+ * 卡片那一行**不靠这个数撑腰**:`.dsm-scene-tile-desc` 是 `nowrap` + 省略号,
6386
+ * 再长也只占一行、不会把卡片撑成纵向。显示层仍按它裁一次,兜住历史数据。
6074
6387
  */
6075
- var SCENE_DESC_MAX = 60
6388
+ var SCENE_DESC_MAX = 300
6076
6389
  /** 记忆描述在勾选行里最多显示多少个字:记忆正文可能很长,行内只留一行,超长截断(全文进 title)。 */
6077
6390
  var MEM_DESC_MAX = 80
6078
6391
  /** 压平空白并裁到 max 个字符(含省略号);返回空串 = 没有描述。 */
@@ -6151,6 +6464,19 @@ function enabledMemoryCount(rules) {
6151
6464
  var trash = sts[0], setTrash = sts[1]
6152
6465
  var stb = React.useState(false)
6153
6466
  var trashBusy = stb[0], setTrashBusy = stb[1]
6467
+ // 「进入场景」前的改动预览卡(null = 关闭;{name, loading, error, plan})。
6468
+ // 只有**进入**这条路走它:退出是按快照回原位,不是新做一次改动,再问一遍纯添摩擦。
6469
+ var pvs = React.useState(null)
6470
+ var preview = pvs[0], setPreview = pvs[1]
6471
+ // 要不要弹那张卡(侧车 `scene-settings.json`)。默认开;卡上「不再显示」与右上那颗
6472
+ // 按钮改的都是它。读不到时按默认(弹)—— 少弹一次卡是"我没被告知就改了环境",
6473
+ // 多弹一次只是多按一下。
6474
+ var eps = React.useState(true)
6475
+ var enterPreview = eps[0], setEnterPreview = eps[1]
6476
+ // 场景页搜索:与 MCP / 技能 / 提示词 / 子智能体 / 记忆页同一形态(防抖 + `matchByText`)。
6477
+ var qs = React.useState('')
6478
+ var query = qs[0], setQuery = qs[1]
6479
+ var dq = useDebouncedValue(query)
6154
6480
  var flipRef = React.useRef(null)
6155
6481
  useFlipReorder(flipRef)
6156
6482
  function loadTrash(keepOpen) {
@@ -6182,6 +6508,13 @@ function enabledMemoryCount(rules) {
6182
6508
  apiCall('scene-mode-get', {}).then(function (m) {
6183
6509
  if (m && m.ok) setData(function (prev) { return Object.assign({}, prev, { mode: m.mode || EMPTY_MODE, archives: m.archives || {} }) })
6184
6510
  }).catch(function () { /* 模式信息拿不到按空模式渲染;主数据 rules-list 的错误有自己的通道 */ })
6511
+ // 悬空引用体检:跟着列表一起读一次(这一页就是档案与绑定的所在地)。
6512
+ // **只在首次加载与手动刷新时跑** —— 它要把四个域各枚举一遍(服务器 / 技能 /
6513
+ // 人设 / 预设),而页内那些"操作完顺手重读"的 silent 刷新一次点开关就来一趟,
6514
+ // 为一个横幅把列表拖慢不值。横幅因此可能滞后一次操作,可它本来就是提示,不是控件。
6515
+ if (!silent) apiCall('state-doctor', {}).then(function (d) {
6516
+ setData(function (prev) { return Object.assign({}, prev, { doctor: d && d.ok ? { findings: d.findings || [], skipped: d.skipped || [] } : null }) })
6517
+ }).catch(function () { setData(function (prev) { return Object.assign({}, prev, { doctor: null }) }) })
6185
6518
  apiCall('rules-list', {}).then(function (r) {
6186
6519
  // rules 也留下来:档案弹窗「记忆」段的默认勾选要用每条记忆的启用状态
6187
6520
  //(scene-inventory 的 memories 里没有 enabled,客户端自己算,省一次宿主改动)。
@@ -6190,6 +6523,24 @@ function enabledMemoryCount(rules) {
6190
6523
  }).catch(function (e) { setData(function (prev) { return Object.assign({}, prev, { loading: false, error: errMsg(e) }) }) })
6191
6524
  }
6192
6525
  React.useEffect(function () { refresh() }, [])
6526
+ /** 读一次界面设置(读不到就保持默认 = 弹卡,本页其它功能不受影响)。 */
6527
+ function loadSceneSettings() {
6528
+ apiCall('scene-settings', {}).then(function (res) {
6529
+ if (res && res.ok && res.settings) setEnterPreview(res.settings.enterPreview !== false)
6530
+ }).catch(function () { /* 读不到 → 保持默认(弹卡) */ })
6531
+ }
6532
+ React.useEffect(function () { loadSceneSettings() }, [])
6533
+ /**
6534
+ * 改「进入场景前弹不弹卡」。乐观更新(点一下就该翻转),失败回读一次并弹结果条 ——
6535
+ * 这颗按钮的状态必须与侧车一致,否则会出现"以为关了、下次还弹"。
6536
+ */
6537
+ function saveEnterPreview(next) {
6538
+ setEnterPreview(next)
6539
+ apiCall('scene-settings', { set: true, enterPreview: next }).then(function (res) {
6540
+ if (res && res.ok && res.settings) setEnterPreview(res.settings.enterPreview !== false)
6541
+ else { loadSceneSettings(); setResult({ ok: false, text: translateError(t, res) }) }
6542
+ }).catch(function (e) { loadSceneSettings(); setResult({ ok: false, text: errMsg(e) }) })
6543
+ }
6193
6544
  /**
6194
6545
  * 切场景的结果提示:宿主会把场景绑定的提示词**写进 `~/.dsh/AGENTS.md`**,
6195
6546
  * 因此必须如实说清写没写、写的是哪份、或者为什么没写成。
@@ -6226,6 +6577,7 @@ function enabledMemoryCount(rules) {
6226
6577
  function toggleScene(scene) {
6227
6578
  if (busy || scene.shared || scene.global) return
6228
6579
  if (scene.active === true) exitMode()
6580
+ else if (enterPreview) askEnter(scene.name)
6229
6581
  else enterMode(scene.name)
6230
6582
  }
6231
6583
  /** 场景锁定(v0.8):锁上后五个管理域整体只读(场景页 + 各功能页);启停不受影响。 */
@@ -6574,8 +6926,12 @@ function enabledMemoryCount(rules) {
6574
6926
  },
6575
6927
  React.createElement('div', { className: 'dsm-modal-body' },
6576
6928
  React.createElement('div', { className: 'dsm-field' },
6577
- React.createElement('label', { className: 'dsm-label' }, t('scenes.mcp.noteLabel')),
6578
- React.createElement('input', { className: 'dsm-control', type: 'text', value: noteVal, maxLength: 200, placeholder: t('scenes.mcp.notePlaceholder'), onChange: function (e) { setMcpNote(server, e.target.value) } }),
6929
+ // 上限 = 注入段的截断长度(`MCP_NOTE_MAX`),计数与标签同行(与场景描述框同款);
6930
+ // 存量的超长备注标红(服务端不校验长度,只有注入时截)。
6931
+ React.createElement('div', { className: 'dsm-budget-meta' },
6932
+ React.createElement('span', { className: 'dsm-label' }, t('scenes.mcp.noteLabel')),
6933
+ React.createElement('span', { className: 'dsm-char-count' + (String(noteVal || '').length > MCP_NOTE_MAX ? ' dsm-char-over' : '') }, String(noteVal || '').length + ' / ' + MCP_NOTE_MAX)),
6934
+ React.createElement('input', { className: 'dsm-control', type: 'text', value: noteVal, maxLength: MCP_NOTE_MAX, placeholder: t('scenes.mcp.notePlaceholder'), onChange: function (e) { setMcpNote(server, e.target.value) } }),
6579
6935
  React.createElement('p', { className: 'dsm-help' }, t('scenes.mcp.noteHint'))),
6580
6936
  React.createElement('div', { className: 'dsm-field' },
6581
6937
  React.createElement('div', { className: 'dsm-mcp-edit-tools-head' },
@@ -6837,6 +7193,118 @@ function enabledMemoryCount(rules) {
6837
7193
  } else setResult({ ok: false, text: translateError(t, res) })
6838
7194
  }).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
6839
7195
  }
7196
+ /**
7197
+ * 「进入场景」先问一次:拉 `scene-mode-preview` 把这次会改的东西列出来。
7198
+ *
7199
+ * 为什么值得多这一步:进入场景是本页**唯一一次改运行时环境**的动作(服务器级 +
7200
+ * 工具级 + 来源级 + 技能级 + 人设 + 备注六处),而它此前的反馈只有事后那句结果条。
7201
+ * 用户实测的用法是"点进去看看",出来才发现十几个技能被停了 —— 只能靠退出还原,
7202
+ * 而还原读的是快照,快照记的是"进场景前",不是"你以为你改之前的样子"。
7203
+ * 预览失败**不拦**:卡上留一条错误 + 「仍然进入」,别让一个附属读数变成进入场景的
7204
+ * 新前置条件。
7205
+ */
7206
+ function askEnter(name) {
7207
+ setPreview({ name: name, loading: true, error: null, plan: null })
7208
+ apiCall('scene-mode-preview', { scene: name }).then(function (res) {
7209
+ if (res && res.ok) setPreview({ name: name, loading: false, error: null, plan: res })
7210
+ else setPreview({ name: name, loading: false, error: translateError(t, res), plan: null })
7211
+ }).catch(function (e) { setPreview({ name: name, loading: false, error: errMsg(e), plan: null }) })
7212
+ }
7213
+ function confirmEnter() {
7214
+ if (!preview) return
7215
+ var name = preview.name
7216
+ setPreview(null)
7217
+ enterMode(name)
7218
+ }
7219
+ /**
7220
+ * 名单只铺前若干个:一行塞 60 个名字,卡就变成另一份档案编辑器了。
7221
+ *
7222
+ * `nameOnly`:技能键是 `<来源>/<名字>`,而自定义来源的键是一串机器名
7223
+ * (`custom-d38da02b873bdb5c`)—— 铺在卡上没人认得出那是哪个技能。技能只取名字
7224
+ * (最后一段),来源与人设本来就是短名(用户 2026-09-23 裁定)。
7225
+ */
7226
+ function previewNames(list, nameOnly) {
7227
+ var names = (list || []).map(function (item) {
7228
+ var text = String(item)
7229
+ if (!nameOnly) return text
7230
+ var parts = text.split('/')
7231
+ return parts[parts.length - 1] || text
7232
+ })
7233
+ var shown = names.slice(0, 12).join('、')
7234
+ return names.length > 12 ? shown + t('scenes.preview.more') : shown
7235
+ }
7236
+ /** 预览卡正文:`scene-mode-preview` 算出的每个方向一行,没有改动的方向不出行。 */
7237
+ function previewRows(plan) {
7238
+ var out = []
7239
+ if (plan.hasArchive !== true) out.push(React.createElement('div', { key: 'noarch', className: 'dsm-feedback dsm-warning' }, t('scenes.preview.noArchive')))
7240
+ if (plan.exit && plan.exit.scene) out.push(React.createElement('p', { key: 'exit', className: 'dsm-help' }, t(plan.exit.snapshotMissing ? 'scenes.preview.exitNoSnapshot' : 'scenes.preview.exitFirst', { scene: plan.exit.scene })))
7241
+ var mcp = plan.mcp || {}
7242
+ var sk = plan.skills || {}
7243
+ var sub = plan.subagents || {}
7244
+ var changed = 0
7245
+ /**
7246
+ * 进入 `target` 之后**会注入**的记忆:该场景自己 + 恒常启用的那两个(服务端给
7247
+ * `_shared`(公共基线)与 `global`(全局)的 `active` 恒为 true,所以不必在客户端
7248
+ * 写死场景名)。逐条判据与记忆页那一行同源(`!shadowed && enabled !== false && 场景 active`),
7249
+ * 差别只有一处:目标场景按"进入后它就是活动场景"算 —— 它现在的 `meta.active` 还是 false。
7250
+ */
7251
+ function memoriesInScopeAfterEnter(target) {
7252
+ var meta = {}
7253
+ ;(data.scenes || []).forEach(function (s) { meta[String(s.name)] = s })
7254
+ return (data.rules || []).filter(function (r) {
7255
+ if (!r || r.shadowed === true || r.enabled === false) return false
7256
+ var scene = sceneOfGroup(r.group)
7257
+ if (scene === target) return true
7258
+ var m = meta[scene]
7259
+ return !!(m && m.active !== false)
7260
+ })
7261
+ }
7262
+ /** 一行改动:`names` 为空就不出行(卡片只报真的会变的方向)。 */
7263
+ function pushRow(key, list, nameOnly) {
7264
+ var names = list || []
7265
+ if (!names.length) return
7266
+ changed += 1
7267
+ out.push(React.createElement('p', { key: key, className: 'dsm-help' }, t(key, { count: names.length, names: previewNames(names, nameOnly === true) })))
7268
+ }
7269
+ // 按域分组:MCP 改动 → MCP 结果 → 技能 → 人设 → 记忆(改动 + 结果)。
7270
+ pushRow('scenes.preview.row.serversOn', mcp.serversOn)
7271
+ pushRow('scenes.preview.row.serversOff', mcp.serversOff)
7272
+ pushRow('scenes.preview.row.toolsOn', mcp.toolsOn)
7273
+ pushRow('scenes.preview.row.toolsOff', mcp.toolsOff)
7274
+ // 「进入后启用 N 台 MCP 服务器」:**结果**口径,与上面四行的"会改什么"互补 ——
7275
+ // 上面全空时这一段就整段消失,用户读起来像缺了信息(2026-09-23 裁定)。
7276
+ var enterServers = (plan.enter && plan.enter.servers) || []
7277
+ if (enterServers.length && plan.noChange !== true) {
7278
+ out.push(React.createElement('p', { key: 'enter-servers', className: 'dsm-help' }, t('scenes.preview.enter.servers', { count: enterServers.length, names: previewNames(enterServers) })))
7279
+ }
7280
+ pushRow('scenes.preview.row.sourcesOn', sk.sourcesOn)
7281
+ pushRow('scenes.preview.row.sourcesOff', sk.sourcesOff)
7282
+ pushRow('scenes.preview.row.skillsOn', sk.on, true)
7283
+ pushRow('scenes.preview.row.skillsOff', sk.off, true)
7284
+ pushRow('scenes.preview.row.personasOn', sub.on)
7285
+ pushRow('scenes.preview.row.personasOff', sub.off)
7286
+ if (mcp.notes) {
7287
+ changed += 1
7288
+ out.push(React.createElement('p', { key: 'notes', className: 'dsm-help' }, t('scenes.preview.row.notes', { count: mcp.notes })))
7289
+ }
7290
+ // 「进入后启用 N 条记忆」:同上,结果口径(含公共基线与全局,它们也在范围里)。
7291
+ // 此前这里还有一行"记忆注入范围收窄到「X」(公共基线与全局不受影响)"—— 用户
7292
+ // 2026-09-23 裁定删掉:下面这句把"有几条、是哪些"说全了,那一行只是把同一件事
7293
+ // 换个说法再说一遍。
7294
+ if (plan.target && plan.noChange !== true) {
7295
+ var memScope = memoriesInScopeAfterEnter(plan.target)
7296
+ if (memScope.length) {
7297
+ out.push(React.createElement('p', { key: 'enter-memories', className: 'dsm-help' }, t('scenes.preview.enter.memories', {
7298
+ count: memScope.length,
7299
+ names: previewNames(memScope.map(function (r) { return String(r.name || r.id) })),
7300
+ })))
7301
+ }
7302
+ }
7303
+ if (plan.truncated) out.push(React.createElement('p', { key: 'more', className: 'dsm-help' }, t('scenes.preview.truncated')))
7304
+ if ((plan.stale || []).length) out.push(React.createElement('p', { key: 'stale', className: 'dsm-help' }, t('scenes.preview.stale', { count: plan.stale.length, items: previewNames(plan.stale) })))
7305
+ if (!changed && !out.length) out.push(React.createElement('p', { key: 'none', className: 'dsm-help' }, t('scenes.preview.nochange')))
7306
+ return out
7307
+ }
6840
7308
  // 「进入某个场景」= 应用它的档案 + 启用它(单选),于是它的记忆与绑定的提示词
6841
7309
  // 一起生效——用户裁定:「进入其中一个场景,提示词就启动成场景的设置的」。
6842
7310
  function enterMode(name) {
@@ -6896,17 +7364,27 @@ function enabledMemoryCount(rules) {
6896
7364
  active: presets.filter(function (s) { return s.active === true }).length,
6897
7365
  archives: presets.filter(function (s) { return !!data.archives[s.name] }).length,
6898
7366
  }
7367
+ /**
7368
+ * 搜索只过滤列表,不动三格统计 —— 与子智能体页同口径:统计答"这台机器上有几个",
7369
+ * 列表答"当前筛选下看得见几个"。匹配范围 = 卡片上真会显示的四样:目录名、显示名、
7370
+ * 一行描述、绑定的提示词预设 id(`_shared` / `global` 这些保留名不在卡片上,不匹配)。
7371
+ */
7372
+ var visible = presets.filter(function (s) {
7373
+ return matchByText([s.name, sceneLabel(data.scenes, s.name), sceneTileDesc(s), s.prompt], dq)
7374
+ })
6899
7375
  return {
6900
7376
  legacyAllScenes: legacy,
6901
7377
  presetScenes: presets,
6902
7378
  sceneStats: stats,
6903
- orderedScenes: enabledFirst(presets, function (scene) { return scene.active === true; }),
7379
+ visibleScenes: visible,
7380
+ orderedScenes: enabledFirst(visible, function (scene) { return scene.active === true; }),
6904
7381
  }
6905
- }, [data.scenes, data.archives])
7382
+ }, [data.scenes, data.archives, dq])
6906
7383
  var legacyAllScenes = sceneView.legacyAllScenes
6907
7384
  var presetScenes = sceneView.presetScenes
6908
7385
  var sceneStats = sceneView.sceneStats
6909
7386
  var orderedScenes = sceneView.orderedScenes
7387
+ var visibleScenes = sceneView.visibleScenes
6910
7388
  /**
6911
7389
  * 档案里已配的东西,一行摘要(没绑的域不出现)。
6912
7390
  *
@@ -6935,12 +7413,25 @@ function enabledMemoryCount(rules) {
6935
7413
  React.createElement('div', { className: 'dsm-actions' },
6936
7414
  refreshButton(t, busy || data.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: busy || data.loading, onClick: function () { refresh() } }),
6937
7415
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, onClick: openCreateScene }, t('memory.btn.newScene')),
7416
+ // 进入场景前的提醒开关(用户 2026-09-23 裁定放在「回收站」左边)。按钮文字是
7417
+ // **动作**:现在会弹 → 写「关闭提醒」;已经关掉 → 写「开启提醒」。
7418
+ React.createElement('button', {
7419
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', title: t('scenes.preview.toggle.title'),
7420
+ onClick: function () { saveEnterPreview(!enterPreview) },
7421
+ }, t(enterPreview ? 'scenes.preview.toggle.off' : 'scenes.preview.toggle.on')),
6938
7422
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
6939
7423
  React.createElement('div', { key: 'stats', className: 'dsm-summary' },
6940
7424
  [[sceneStats.total, t('scenes.stat.total')], [sceneStats.active, t('scenes.stat.active')], [sceneStats.archives, t('scenes.stat.archives')]].map(function (item) {
6941
7425
  return React.createElement('div', { key: item[1], className: 'dsm-stat' },
6942
7426
  React.createElement('strong', null, item[0]), item[1])
6943
7427
  })),
7428
+ // 搜索框:统计条之后(与 MCP / 技能 / 记忆 / 提示词 / 子智能体页同一位置与样式)。
7429
+ React.createElement('div', { key: 'filters', className: 'dsm-filters' },
7430
+ React.createElement('input', {
7431
+ className: 'dsm-control dsm-search', value: query, 'aria-label': t('search'),
7432
+ placeholder: t('scenes.search.placeholder'),
7433
+ onChange: function (e) { setQuery(e.target.value) },
7434
+ })),
6944
7435
  // 当前模式条:**只在真的进入了模式时才出现**。
6945
7436
  // 以前没有模式时也常驻一条「自由模式 + 一句解释」,用户反馈「这个是干什么的,感觉没什么用」——
6946
7437
  // 静态解释占一整条,而「没有条 = 没有模式」本来就不言自明。现在它只承载有状态的信息:
@@ -6960,9 +7451,29 @@ function enabledMemoryCount(rules) {
6960
7451
  legacyAllScenes.length > 1 ? React.createElement('div', { key: 'legacyall', className: 'dsm-feedback dsm-warning' },
6961
7452
  React.createElement('span', null, t('scenes.legacyAll', { count: legacyAllScenes.length })),
6962
7453
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { enterMode(legacyAllScenes[0].name) } }, t('scenes.legacyAll.fix'))) : null,
7454
+ // 改名 / 删除留下的悬空引用:这一条列表上看着还绑着,运行时按那个名字已经找不到了。
7455
+ // 只说"哪一条指向谁、那个谁不在了",不代做修复 —— 猜着改名字比留着更危险。
7456
+ (function () {
7457
+ var doctor = data.doctor
7458
+ if (!doctor) return null
7459
+ var findings = doctor.findings || []
7460
+ if (!findings.length && !(doctor.skipped || []).length) return null
7461
+ var shown = findings.slice(0, 6)
7462
+ return React.createElement('div', { key: 'doctor', className: 'dsm-feedback dsm-warning' },
7463
+ React.createElement('div', { key: 'head' }, findings.length
7464
+ ? t('scenes.doctor.title', { count: findings.length })
7465
+ : t('scenes.doctor.noneButSkipped'), React.createElement('span', null, ' ' + t('scenes.doctor.hint'))),
7466
+ shown.map(function (row, i) {
7467
+ return React.createElement('div', { key: 'd' + i, className: 'dsm-help' }, t('scenes.doctor.row', { where: row.where, name: row.name }))
7468
+ }),
7469
+ findings.length > shown.length ? React.createElement('div', { key: 'more', className: 'dsm-help' }, t('scenes.doctor.more', { count: findings.length - shown.length })) : null,
7470
+ (doctor.skipped || []).length ? React.createElement('div', { key: 'skip', className: 'dsm-help' }, t('scenes.doctor.skipped', { domains: doctor.skipped.join('、') })) : null)
7471
+ })(),
6963
7472
  data.error ? React.createElement(Notice, { key: 'gerr', kind: 'err', text: String(data.error) }) : null,
6964
7473
  data.loading && !presetScenes.length ? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
6965
- : presetScenes.length ? React.createElement('div', { key: 'scenes', className: 'dsm-scenes', ref: flipRef }, orderedScenes.map(function (scene) {
7474
+ : !presetScenes.length ? React.createElement('div', { key: 'empty', className: 'dsm-empty' }, t('scenes.empty'))
7475
+ : !visibleScenes.length ? React.createElement('div', { key: 'emptysearch', className: 'dsm-empty' }, t('scenes.empty.search'))
7476
+ : React.createElement('div', { key: 'scenes', className: 'dsm-scenes', ref: flipRef }, orderedScenes.map(function (scene) {
6966
7477
  var name = scene.name
6967
7478
  var label = sceneLabel(data.scenes, name) || name
6968
7479
  var archive = data.archives[name]
@@ -7010,7 +7521,7 @@ function enabledMemoryCount(rules) {
7010
7521
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy || sceneLocked, title: sceneLocked ? t('scenes.lock.blockedEdit') : '', onClick: function () { openArchive(name) } }, t('memory.archive.edit')),
7011
7522
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy, onClick: function () { openEditScene(scene) } }, t('memory.scene.edit')),
7012
7523
  locked ? null : React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy || sceneLocked, title: sceneLocked ? t('scenes.lock.blockedEdit') : '', onClick: function () { setModal({ type: 'scene-delete', name: name }) } }, t('memory.btn.deleteScene')))))
7013
- })) : React.createElement('div', { key: 'empty', className: 'dsm-empty' }, t('scenes.empty')),
7524
+ })),
7014
7525
  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) } },
7015
7526
  React.createElement('div', { className: 'dsm-form' },
7016
7527
  React.createElement('label', { className: 'dsm-field' },
@@ -7025,7 +7536,7 @@ function enabledMemoryCount(rules) {
7025
7536
  React.createElement('label', { className: 'dsm-field' },
7026
7537
  React.createElement('div', { className: 'dsm-budget-meta' },
7027
7538
  React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.desc')),
7028
- // 与描述行一一对应的字数上限:卡片只显示一行,超长的描述会把卡片撑成纵向。
7539
+ // 字数上限 = 存多少(`SCENE_DESC_MAX`);卡片那一行靠 CSS 省略号截,不靠这个数。
7029
7540
  React.createElement('span', { className: 'dsm-char-count' }, String(String(sceneForm.description || '').length) + '/' + SCENE_DESC_MAX)),
7030
7541
  React.createElement('input', {
7031
7542
  className: 'dsm-control',
@@ -7033,8 +7544,7 @@ function enabledMemoryCount(rules) {
7033
7544
  maxLength: SCENE_DESC_MAX,
7034
7545
  placeholder: t('memory.scene.field.desc.placeholder'),
7035
7546
  onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { description: e.target.value, error: null })) },
7036
- }),
7037
- React.createElement('p', { className: 'dsm-help' }, t('scenes.field.desc.limit', { count: SCENE_DESC_MAX }))),
7547
+ })),
7038
7548
  // 提示词预设:一个场景**只能绑一个**(单值字段天然单选);「不绑定」= 解绑。
7039
7549
  React.createElement('label', { className: 'dsm-field' },
7040
7550
  React.createElement('span', { className: 'dsm-label' }, t('scenes.field.prompt')),
@@ -7048,6 +7558,22 @@ function enabledMemoryCount(rules) {
7048
7558
  React.createElement('p', { className: 'dsm-help' }, t('memory.deleteScene.desc', { name: modal.name })),
7049
7559
  React.createElement('div', { className: 'dsm-modal-actions' },
7050
7560
  React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitDeleteScene(modal.name) } }, t('memory.btn.deleteScene')))) : null,
7561
+ // 「进入场景」的改动预览卡(askEnter)。预览拿不到时不拦进入这条路 ——
7562
+ // 这是一张说明卡,不是一道新门禁;把它做成前置条件就等于新增了"进不去场景"。
7563
+ //
7564
+ // 按钮(用户 2026-09-23 裁定):**只有「确认进入」与「不再显示」**。原先那颗
7565
+ // 「先不进入」与右上角的「关闭」是同一个动作的两颗按钮,删掉那颗;「不再显示」
7566
+ // = 关掉卡片 + 以后不再弹(也不进入),想恢复用右上那颗「开启提醒」。
7567
+ preview ? React.createElement(Modal, { key: 'spreview', title: t('scenes.preview.title', { name: preview.name }), closeLabel: t('btn.close'), onClose: function () { setPreview(null) } },
7568
+ preview.loading ? React.createElement('p', { className: 'dsm-help' }, t('scenes.preview.loading'))
7569
+ : preview.error ? React.createElement('div', { className: 'dsm-feedback dsm-error', role: 'alert' }, t('scenes.preview.failed', { error: preview.error }))
7570
+ : previewRows(preview.plan || { target: preview.name }),
7571
+ React.createElement('div', { className: 'dsm-modal-actions dsm-modal-actions-split' },
7572
+ React.createElement('button', {
7573
+ type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy, title: t('scenes.preview.dismiss.title'),
7574
+ onClick: function () { setPreview(null); saveEnterPreview(false) },
7575
+ }, t('scenes.preview.dismiss')),
7576
+ React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy || preview.loading, onClick: confirmEnter }, t('scenes.preview.enter')))) : null,
7051
7577
  // 档案编辑器(见 archiveNode):三段共用「段卡片 + 勾选行」排版。
7052
7578
  archiveNode(),
7053
7579
  trash ? React.createElement(TrashModal, {
@@ -7580,8 +8106,12 @@ function enabledMemoryCount(rules) {
7580
8106
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.name')),
7581
8107
  React.createElement('input', { className: 'dsm-control', value: modal.form.name || '', placeholder: 'code-review', onChange: function (e) { setForm({ name: e.target.value }) } })),
7582
8108
  React.createElement('label', { className: 'dsm-field' },
7583
- React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
7584
- React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
8109
+ // 描述会整段进「可委派的子智能体」目录,上限就是注入侧的截断长度(`CATALOG_DESC_MAX`);
8110
+ // 存量人设的超长描述标红(服务端不校验这个长度,只有注入时截)。
8111
+ React.createElement('div', { className: 'dsm-budget-meta' },
8112
+ React.createElement('span', { className: 'dsm-label' }, t('subagents.field.description')),
8113
+ React.createElement('span', { className: 'dsm-char-count' + (String(modal.form.description || '').length > CATALOG_DESC_MAX ? ' dsm-char-over' : '') }, String(modal.form.description || '').length + ' / ' + CATALOG_DESC_MAX)),
8114
+ React.createElement('input', { className: 'dsm-control', value: modal.form.description || '', maxLength: CATALOG_DESC_MAX, placeholder: t('subagents.field.description.placeholder'), onChange: function (e) { setForm({ description: e.target.value }) } })),
7585
8115
  React.createElement('label', { className: 'dsm-field' },
7586
8116
  React.createElement('span', { className: 'dsm-label' }, t('subagents.field.body')),
7587
8117
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: modal.form.body || '', placeholder: t('subagents.field.body.placeholder'), onChange: function (e) { setForm({ body: e.target.value }) } })),
@@ -8242,6 +8772,10 @@ function enabledMemoryCount(rules) {
8242
8772
  React.createElement('div', { className: 'dsm-note' }, r.description || ''),
8243
8773
  shadowed ? React.createElement('div', { className: 'dsm-rule-shadow-hint' }, t('memory.shadowed.hint')) : null),
8244
8774
  React.createElement('div', { className: 'dsm-tags' },
8775
+ // 体积:记忆是唯一**直接吃注入预算**的域(场景段 128 KiB),此前页面上却
8776
+ // 看不出哪条大 —— 一列字节数就是那一域的成本读数。宿主读不到文件时不带
8777
+ // `bytes`,这里也就什么都不显示(而不是显示 0 B,那是个假数字)。
8778
+ typeof r.bytes === 'number' ? React.createElement('span', { className: 'dsm-tag', title: t('memory.size.tip', { size: fmtBytes(r.bytes) }) }, fmtBytes(r.bytes)) : null,
8245
8779
  r.group && r.group.indexOf('/') >= 0 ? React.createElement('span', { className: 'dsm-tag' }, r.group) : null,
8246
8780
  r.form === 'bundle' ? React.createElement('span', { className: 'dsm-tag dsm-tag-on' }, t('memory.form.bundle')) : null,
8247
8781
  attachTag(r, shadowed),
@@ -8419,9 +8953,12 @@ function enabledMemoryCount(rules) {
8419
8953
  React.createElement('input', { className: 'dsm-control' + (editorNameInvalid ? ' dsm-rule-invalid' : ''), value: editor.name || '', placeholder: t('memory.field.name.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { name: e.target.value })) } }),
8420
8954
  editorNameInvalid ? React.createElement('p', { className: 'dsm-rule-hint' }, t('memory.name.invalid')) : null),
8421
8955
  React.createElement('label', { className: 'dsm-field' },
8422
- React.createElement('span', { className: 'dsm-label' }, t('memory.field.description')),
8423
- React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: editor.description || '', placeholder: t('memory.field.description.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { description: e.target.value })) } }),
8424
- React.createElement('div', { className: 'dsm-char-count' + (editorDescLen > 500 ? ' dsm-char-over' : '') }, editorDescLen + ' / 500')),
8956
+ // 字数计数与「描述」同一行、贴右缘(与 MCP 备注 / 人设与技能简介同款排版)——
8957
+ // 以前它单起一行落在输入框下面,看着像是另一个字段。
8958
+ React.createElement('div', { className: 'dsm-budget-meta' },
8959
+ React.createElement('span', { className: 'dsm-label' }, t('memory.field.description')),
8960
+ React.createElement('span', { className: 'dsm-char-count' + (editorDescLen > 500 ? ' dsm-char-over' : '') }, editorDescLen + ' / 500')),
8961
+ React.createElement('textarea', { className: 'dsm-control dsm-textarea-sm', value: editor.description || '', placeholder: t('memory.field.description.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { description: e.target.value })) } })),
8425
8962
  React.createElement('label', { className: 'dsm-field' },
8426
8963
  React.createElement('span', { className: 'dsm-label' }, t('memory.field.body')),
8427
8964
  React.createElement('textarea', { className: 'dsm-control dsm-textarea-md', value: editor.body || '', placeholder: t('memory.field.body.placeholder'), onChange: function (e) { setEditor(Object.assign({}, editor, { body: e.target.value })) } })),