dsh-plugin-tool-management 0.17.0 → 0.18.5
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/CHANGELOG.md +70 -0
- package/README.md +19 -4
- package/README_EN.md +19 -4
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +29 -0
- package/lib/client.js +1662 -161
- package/lib/compat/capability-specs.js +321 -0
- package/lib/compat/host-probe.js +261 -0
- package/lib/compat/host-roots.js +168 -0
- package/lib/compat/probe-types.js +6 -0
- package/lib/compat/probe.js +9 -720
- package/lib/context-inject-contract.js +114 -0
- package/lib/context-inject-frame.js +147 -0
- package/lib/context-inject-select.js +127 -0
- package/lib/context-inject-surface.js +97 -0
- package/lib/context-inject.js +10 -478
- package/lib/exports/transcript.js +223 -0
- package/lib/http-api.js +229 -0
- package/lib/hub.js +2 -0
- package/lib/index.js +138 -868
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/manager.js +30 -548
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +142 -5
- package/lib/memories/archive.js +10 -1
- package/lib/memories/discovery.js +261 -0
- package/lib/memories/export-plan.js +42 -0
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +4 -38
- package/lib/memories/snapshot.js +10 -252
- package/lib/op-registry.js +21 -0
- package/lib/ops/compat.js +11 -0
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +40 -0
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +34 -14
- package/lib/ops/snapshot.js +18 -12
- package/lib/prompts/quick-service.js +211 -0
- package/lib/scene-linkage.js +184 -0
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +29 -166
- package/lib/settings-sidecars.js +143 -0
- package/lib/skills/core-types.js +8 -0
- package/lib/skills/core-util.js +81 -0
- package/lib/skills/core.js +56 -2326
- package/lib/skills/dir-watch.js +71 -0
- package/lib/skills/frontmatter.js +96 -0
- package/lib/skills/homes.js +14 -0
- package/lib/skills/import-pipeline.js +231 -0
- package/lib/skills/manager-state.js +800 -0
- package/lib/skills/official.js +269 -0
- package/lib/skills/provider-registration.js +89 -0
- package/lib/skills/service.js +5 -240
- package/lib/skills/skill-roots.js +287 -0
- package/lib/skills/skill-trash.js +265 -0
- package/lib/skills/system-trash.js +97 -0
- package/lib/skills/upload-pipeline.js +374 -0
- package/lib/slash-settings.js +25 -0
- package/lib/subagents/persona-parse.js +312 -0
- package/lib/subagents/persona-types.js +5 -0
- package/lib/subagents/persona-write.js +93 -0
- package/lib/subagents/run-provider.js +159 -0
- package/lib/subagents/service.js +14 -638
- package/lib/subagents/tool-filter.js +98 -0
- package/lib/token-config.js +240 -0
- package/lib/tool-table-switch.js +163 -0
- package/lib/tools/scene.js +40 -9
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -119,6 +119,12 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
|
|
|
119
119
|
改成纵向两行:**名字独占一行、仍单行省略**(全名靠 title),描述另起一行、单行省略。
|
|
120
120
|
只作用于本页:.dsm-source-head-main 还被技能 / 记忆 / MCP / 会话四页的分组头共用,
|
|
121
121
|
那些行没有描述,不动。同权重靠书写顺序取胜,所以必须排在上面那条 .dsm-source 规则之后。 */
|
|
122
|
+
/* 提示词页的两张分组卡片:条目本身仍是 .dsm-source 卡,直接套进组卡的卡片体里就成了
|
|
123
|
+
"卡中卡"(一层边框套一层边框)。这里把它们压平成带分隔线的行 —— 与技能 / MCP 那两页
|
|
124
|
+
的卡片体同一个观感(用户 2026-10-01 裁定:做成技能那样可以折叠的样式)。 */
|
|
125
|
+
.dsm-source-body .dsm-sources{gap:0}
|
|
126
|
+
.dsm-source-body .dsm-source{border:0;border-radius:0;border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
127
|
+
.dsm-source-body .dsm-source:last-child{border-bottom:0}
|
|
122
128
|
.dsm-persona-main{box-sizing:border-box;min-height:48px;padding:6px 0;flex-direction:column;align-items:flex-start;justify-content:center;gap:0}
|
|
123
129
|
.dsm-persona-name-row{display:flex;width:100%;min-width:0;align-items:center;gap:7px}
|
|
124
130
|
.dsm-persona-name-row .dsm-tag{flex:none}
|
|
@@ -211,6 +217,12 @@ button.dsm-tag:disabled{cursor:default;opacity:.6}
|
|
|
211
217
|
.dsm-seg-body{display:flex;height:216px;padding:6px;overflow:auto;flex-direction:column}
|
|
212
218
|
/* 滚动容器里的子项不许被压缩:否则内容一多,筛选框会被压到比 input 还矮、与下面的条目叠在一起。 */
|
|
213
219
|
.dsm-seg-body>*{flex:none}
|
|
220
|
+
/* 条目本来就少的那一段(场景表单里的快捷提示词):跟着内容长,超过 6 行才内滚。
|
|
221
|
+
固定 216px 的段体放在一张按内容高的表单弹窗里,就是"一个勾 + 一片空白"(用户实测)。 */
|
|
222
|
+
.dsm-seg-fit{height:auto;max-height:172px}
|
|
223
|
+
/* 表单分段标题:把一长串字段切成「基本信息 / 场景绑定」这类模块,右侧一道发丝线跟着撑满。 */
|
|
224
|
+
.dsm-form-section{display:flex;align-items:center;gap:10px;margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:620;letter-spacing:.02em}
|
|
225
|
+
.dsm-form-section::after{content:"";height:1px;flex:1;background:var(--dsw-alias-border-l1)}
|
|
214
226
|
.dsm-seg-foot{display:flex;align-items:center;gap:8px;padding:7px 12px;border-top:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
|
|
215
227
|
.dsm-pick{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px}.dsm-mcp-edit-tools-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.dsm-mcp-edit-tools-actions{display:flex;align-items:center;gap:8px}.dsm-seg-empty-actions{display:flex;justify-content:center;padding:4px 0 12px}.dsm-seg-empty-error{padding:0 10px 10px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;text-align:center}
|
|
216
228
|
.dsm-pick:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
@@ -447,11 +459,25 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
447
459
|
/* 动作可用性表:表头列宽与 dsm-compat-mod-row 一致;窄容器下隐藏表头。 */
|
|
448
460
|
.dsm-compat-table-head{display:grid;grid-template-columns:minmax(0,1fr) max-content;gap:10px;padding:6px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}
|
|
449
461
|
.dsm-compat-cards .dsm-compat-table-head{display:none}
|
|
462
|
+
/* 新增提示词弹窗定高(用户 2026-09-30):切「全局 / 快捷」时「从现有预设复制」那一行会出现/
|
|
463
|
+
消失,让整颗框跟着长缩。高的一屏为准,多出来的高度给正文文本框吃掉;窗口太矮时弹窗自己滚。 */
|
|
464
|
+
.dsm-modal-fixed{height:min(760px,calc(100vh - 48px));max-height:none}
|
|
465
|
+
.dsm-modal-fixed .dsm-field-grow{min-height:0;flex:1}
|
|
466
|
+
.dsm-modal-fixed .dsm-field-grow textarea{flex:1}
|
|
450
467
|
/* 导出弹窗(D14):宽高都固定;只有「条目列表」这一层滚动,动作条常驻底部。 */
|
|
451
468
|
.dsm-export-modal .dsm-modal-body{display:flex;min-height:0;flex-direction:column;gap:14px;overflow:hidden}
|
|
452
469
|
.dsm-export-modal .dsm-export-form{display:flex;min-height:0;flex:1;flex-direction:column;gap:10px}
|
|
453
470
|
.dsm-export-modal .dsm-pick-list{flex:1;min-height:0;max-height:none;overflow:auto}
|
|
454
471
|
.dsm-export-modal .dsm-export-body{display:flex;min-height:0;flex:1;flex-direction:column;gap:6px}
|
|
472
|
+
/* 会话导出弹窗(0.18.5):字段压成三行之后,560×640 里会话列表只剩几行(用户反馈)。
|
|
473
|
+
处方照 .dsm-modal-archive / .dsm-modal-fixed:固定高、只有列表内滚。宽度取 720 —— 与
|
|
474
|
+
.dsm-modal-lg / .dsm-modal-wide 同一档,两列字段各约 340px 刚好放得下(880 试过,标题行
|
|
475
|
+
右边空成一片,用户说太宽)。高度取 760 而不是视口百分比 —— 760 既是 .dsm-modal 自己的
|
|
476
|
+
max-height 也是 .dsm-modal-fixed 那个数,窗口矮时 calc(100vh - 48px) 照常接管。
|
|
477
|
+
写成两段类名(.dsm-modal.dsm-modal-export-lg)是为了**不靠书写顺序**压过 .dsm-modal-list
|
|
478
|
+
的 !important(.dsm-modal-lg 排在 .dsm-modal-list 前面,直接借用会被它盖掉)。只加在这一颗
|
|
479
|
+
弹窗上:.dsm-modal-list 还被四个页面的导出弹窗和回收站共用,动它就是它们一起变大。 */
|
|
480
|
+
.dsm-modal.dsm-modal-export-lg{width:min(720px,100%)!important;height:min(760px,calc(100vh - 48px))!important;max-height:none!important}
|
|
455
481
|
.dsm-pick-group{display:flex;flex-direction:column}
|
|
456
482
|
.dsm-pick-group-head{box-sizing:border-box;display:flex;width:100%;min-width:0;align-items:center;gap:7px;padding:7px 2px;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;text-align:left;cursor:pointer}
|
|
457
483
|
.dsm-pick-group-head:hover{color:var(--dsw-alias-label-primary)}
|
|
@@ -497,6 +523,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
497
523
|
.dsm-token-banner-close{pointer-events:auto;flex:none;width:20px;height:20px;padding:0;border:0;border-radius:5px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:20px;cursor:pointer;opacity:.7}
|
|
498
524
|
.dsm-token-banner-close:hover{opacity:1}
|
|
499
525
|
|
|
526
|
+
/* 全局轻提示(斜杠命令的执行反馈)。位置在右下角:对话页的输入框居底居中,顶部那条又是
|
|
527
|
+
令牌横幅,两处都会被挡。z-index 比横幅低一档 —— 令牌没过时那句解释比"改成功了"更要紧。 */
|
|
528
|
+
.dsm-toast{position:fixed;right:18px;bottom:18px;z-index:998;max-width:min(420px,80vw);padding:9px 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-text-primary);font-size:12.5px;line-height:1.55;box-shadow:0 6px 22px rgba(0,0,0,.16);pointer-events:none;backdrop-filter:blur(6px)}
|
|
529
|
+
.dsm-toast-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}
|
|
530
|
+
.dsm-toast-warn{border-color:var(--dsm-warn);color:var(--dsm-warn)}
|
|
531
|
+
.dsm-toast-err{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
532
|
+
|
|
500
533
|
/* MCP JSON 导入预览(0.15.0 A1):勾选 | 主体 | 状态三列。非法行只把「问题」那一行标红,
|
|
501
534
|
名字净化 / 字段忽略这类是配置事实,走中性灰 —— 黄色在这里留给不了任何东西(琥珀语义)。 */
|
|
502
535
|
.dsm-imp-head,.dsm-imp-row{display:grid;grid-template-columns:15px minmax(0,1fr) auto;align-items:start;gap:10px}
|
|
@@ -547,9 +580,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
547
580
|
.dsm-snap{display:flex;flex-direction:column;gap:8px}
|
|
548
581
|
.dsm-snap-row{display:flex;gap:8px}
|
|
549
582
|
.dsm-snap-row .dsm-control{flex:1;min-width:0}
|
|
550
|
-
.dsm-snap-picks{display:flex;flex-wrap:wrap;gap:6px 14px}
|
|
551
|
-
.dsm-snap-pick{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
|
|
552
|
-
.dsm-snap-pick input{width:14px;height:14px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
|
|
583
|
+
.dsm-snap-picks,.dsm-kind-picks{display:flex;flex-wrap:wrap;gap:6px 14px}
|
|
584
|
+
.dsm-snap-pick,.dsm-kind-pick{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer}
|
|
585
|
+
.dsm-snap-pick input,.dsm-kind-pick input{width:14px;height:14px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-state-success-primary)}
|
|
553
586
|
.dsm-snap-preview{display:flex;flex-direction:column;gap:6px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1)}
|
|
554
587
|
/* 兼容页三块弹窗:**开合不改尺寸**(列表长短、预览有没有读回来都不该让框跳)。
|
|
555
588
|
「最近改动」(唯一 fixed 弹窗)的滚动层**只给列表**:正文是纯 flex 容器不滚动,
|
|
@@ -944,6 +977,60 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
944
977
|
return pending
|
|
945
978
|
}
|
|
946
979
|
|
|
980
|
+
/**
|
|
981
|
+
* 斜杠命令两段的挂载实况(由 48-slash.js 在注册时写入,兼容页那两枚标签读它)。
|
|
982
|
+
* **按段记**:两段各自是一个源,各自挂没挂上可以不同(只注册了一个、或第二个源重名)。
|
|
983
|
+
*
|
|
984
|
+
* 为什么声明放在这里而不是 48 片:消费它的 CompatPage 在 20 片,比 48 片**早** —— 同一个
|
|
985
|
+
* factory 作用域里函数声明提升没问题,但 `checkJs` 对这条跨片的反向引用会报 TS2304
|
|
986
|
+
* (`npm run typecheck:client` 实测)。共享状态放在两个使用方的**共同上游**这一档。
|
|
987
|
+
*
|
|
988
|
+
* 服务端**不知道**挂没挂上(那是浏览器那半的事),所以这一句只能由客户端说;feature-overview
|
|
989
|
+
* 那一行报的是开关状态,两者不是一回事,别拿一个冒充另一个(见 ops/compat.ts 同一处注释)。
|
|
990
|
+
*/
|
|
991
|
+
let slashMountStates = { tools: 'pending', quickPrompts: 'pending' }
|
|
992
|
+
function slashMountStateOf(key) { return slashMountStates[key] || 'pending' }
|
|
993
|
+
/** 传 key 只改那一段;不传是"两段一起"(都关掉了、或整批注册失败)。 */
|
|
994
|
+
function setSlashMount(state, key) {
|
|
995
|
+
if (key) slashMountStates[key] = state
|
|
996
|
+
else slashMountStates = { tools: state, quickPrompts: state }
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* 一条全局轻提示(body 级,与页面/弹窗无关)。
|
|
1001
|
+
*
|
|
1002
|
+
* 为什么不是 `Notice`:`Notice` 长在页签里,而斜杠命令是在**对话页**按下的 —— 那时设置页
|
|
1003
|
+
* 根本没挂载,面板内的提示条无处可渲染。它与「令牌没填」那条横幅同源同位(40-apply-head.js
|
|
1004
|
+
* 的 body 级 append),差别只是这条会自动收。
|
|
1005
|
+
*
|
|
1006
|
+
* 同时只留一条:后发的直接覆写前一条(连点两下开关不该叠出两层提示)。
|
|
1007
|
+
*/
|
|
1008
|
+
let toastEl = null
|
|
1009
|
+
let toastText = null
|
|
1010
|
+
let toastTimer = null
|
|
1011
|
+
function showPluginToast(text, kind) {
|
|
1012
|
+
try {
|
|
1013
|
+
if (typeof document === 'undefined' || !document.body) return
|
|
1014
|
+
const tone = kind === 'err' ? 'err' : kind === 'warn' ? 'warn' : 'ok'
|
|
1015
|
+
if (!toastEl) {
|
|
1016
|
+
toastEl = document.createElement('div')
|
|
1017
|
+
toastEl.setAttribute('role', 'status')
|
|
1018
|
+
toastText = document.createElement('span')
|
|
1019
|
+
toastEl.appendChild(toastText)
|
|
1020
|
+
document.body.appendChild(toastEl)
|
|
1021
|
+
}
|
|
1022
|
+
toastText.textContent = String(text == null ? '' : text)
|
|
1023
|
+
toastEl.className = 'dsm-toast dsm-toast-' + tone
|
|
1024
|
+
toastEl.style.display = ''
|
|
1025
|
+
if (toastTimer) clearTimeout(toastTimer)
|
|
1026
|
+
// 报错那条留久一点:它常常是"得去设置页填令牌"这类要照着做的话。
|
|
1027
|
+
toastTimer = setTimeout(function () {
|
|
1028
|
+
if (toastEl) toastEl.style.display = 'none'
|
|
1029
|
+
toastTimer = null
|
|
1030
|
+
}, tone === 'err' ? 9000 : 4000)
|
|
1031
|
+
} catch (e) { /* DOM 不可用 → 只是少一条反馈,不影响已经生效的改动 */ }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
947
1034
|
// Small version badge shown next to each settings-page title; reads the
|
|
948
1035
|
// package version from the host (plugin-version op) so it always matches
|
|
949
1036
|
// the installed release.
|
|
@@ -1046,6 +1133,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1046
1133
|
'agentsmd-apply': 'compat.audit.op.presetApply',
|
|
1047
1134
|
'agentsmd-trash-restore': 'compat.audit.op.trashRestore',
|
|
1048
1135
|
'agentsmd-trash-delete': 'compat.audit.op.trashDelete',
|
|
1136
|
+
// 快捷提示词:与预设分开两套文案(「保存提示词预设」在流水里等于"改了 AGENTS.md 的来源",
|
|
1137
|
+
// 而快捷词一个字节都不碰宿主状态,混着记会让流水读起来不像事实)。
|
|
1138
|
+
'quickprompt-create': 'compat.audit.op.quickSave',
|
|
1139
|
+
'quickprompt-update': 'compat.audit.op.quickSave',
|
|
1140
|
+
'quickprompt-toggle': 'compat.audit.op.quickSwitch',
|
|
1141
|
+
'quickprompt-remove': 'compat.audit.op.quickDelete',
|
|
1142
|
+
'quickprompt-trash-restore': 'compat.audit.op.trashRestore',
|
|
1143
|
+
'quickprompt-trash-delete': 'compat.audit.op.trashDelete',
|
|
1049
1144
|
'tool-table': 'compat.audit.op.toolTable',
|
|
1050
1145
|
'inject-settings': 'compat.audit.op.injectSettings',
|
|
1051
1146
|
'scene-settings': 'compat.audit.op.sceneSettings',
|
|
@@ -1090,6 +1185,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1090
1185
|
// 注入设置(本插件各注入域的开关,清单见 context-inject 的 INJECT_DOMAIN_KEYS)。它和可达性矩阵是一体两面:
|
|
1091
1186
|
// 矩阵说"到不到得了",这里决定"要不要"。读不到时这一节不显示。
|
|
1092
1187
|
const [inject, setInject] = React.useState(null)
|
|
1188
|
+
// 斜杠命令入口(`/` 菜单里那个「工具」段)的开关。与 inject 同一类:读不到就不渲染这一节。
|
|
1189
|
+
const [slash, setSlash] = React.useState(null)
|
|
1093
1190
|
// 模型工具表(哪些工具整份不发给模型;见 src/tools/table.ts)。与「注入」是两种省法:
|
|
1094
1191
|
// 注入关掉省的是正文,这里关掉省的是工具 schema(工具表按每个请求付钱)。
|
|
1095
1192
|
const [toolTable, setToolTable] = React.useState(null)
|
|
@@ -1450,6 +1547,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1450
1547
|
.then(function (r) { if (alive !== false && r && r.ok && r.settings) setInject(r.settings) })
|
|
1451
1548
|
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
1452
1549
|
}
|
|
1550
|
+
const loadSlash = function (alive) {
|
|
1551
|
+
apiCall('slash-settings', {})
|
|
1552
|
+
.then(function (r) { if (alive !== false && r && r.ok && r.settings) setSlash(r.settings) })
|
|
1553
|
+
.catch(function () { /* 读不到 → 不渲染这一节 */ })
|
|
1554
|
+
}
|
|
1453
1555
|
// 工具表实况(注册时量到的体积 + 关掉了哪些)。服务端返回的 `report` 自带分组与
|
|
1454
1556
|
// ≈token 合计 —— 界面不自己算,也不缓存第二份数字。
|
|
1455
1557
|
const applyToolTable = function (r, alive) {
|
|
@@ -1587,6 +1689,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1587
1689
|
.catch(function (e) { if (alive) { setError(errMsg(e)); setBusy(false) } })
|
|
1588
1690
|
loadReach(alive)
|
|
1589
1691
|
loadInject(alive)
|
|
1692
|
+
loadSlash(alive)
|
|
1590
1693
|
loadToolTable(alive)
|
|
1591
1694
|
loadLive(alive)
|
|
1592
1695
|
loadFeatures(alive)
|
|
@@ -1601,6 +1704,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1601
1704
|
.catch(function (e) { setError(errMsg(e)); setBusy(false) })
|
|
1602
1705
|
loadReach(true)
|
|
1603
1706
|
loadInject(true)
|
|
1707
|
+
loadSlash(true)
|
|
1604
1708
|
loadToolTable(true)
|
|
1605
1709
|
loadLive(true)
|
|
1606
1710
|
loadFeatures(true)
|
|
@@ -1620,6 +1724,21 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1620
1724
|
})
|
|
1621
1725
|
.catch(function () { loadInject(true); loadReach(true) })
|
|
1622
1726
|
}
|
|
1727
|
+
/**
|
|
1728
|
+
* 保存斜杠命令入口的两颗开关(「工具」段 / 「快捷提示词」段)。与 saveInject 同一条口径:
|
|
1729
|
+
* 先乐观更新(点一下就该有反馈),服务端返回值到达后以其为准,失败重拉。
|
|
1730
|
+
*
|
|
1731
|
+
* **本次启动不会立刻挂上 / 撤下** —— 源注册是启动期一次性的事,所以说明里写清"重启或
|
|
1732
|
+
* 重开页面后生效",别让"关掉了但菜单还在"变成一次报障。
|
|
1733
|
+
*/
|
|
1734
|
+
const saveSlash = function (patch) {
|
|
1735
|
+
if (!slash) return
|
|
1736
|
+
const next = Object.assign({}, slash, patch)
|
|
1737
|
+
setSlash(next)
|
|
1738
|
+
apiCall('slash-settings', Object.assign({ set: true }, next))
|
|
1739
|
+
.then(function (r) { if (r && r.ok && r.settings) setSlash(r.settings); loadFeatures(true) })
|
|
1740
|
+
.catch(function () { loadSlash(true) })
|
|
1741
|
+
}
|
|
1623
1742
|
|
|
1624
1743
|
const findings = (data && data.findings) || []
|
|
1625
1744
|
const degraded = (data && data.degraded) || []
|
|
@@ -1697,6 +1816,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1697
1816
|
case 'scene-lock': return t('compat.ov.sceneLock')
|
|
1698
1817
|
case 'mcp-tools': return t('compat.ov.mcpTools')
|
|
1699
1818
|
case 'tool-table': return t('compat.ov.toolTable')
|
|
1819
|
+
case 'slash-commands': return t('compat.ov.slashCommands')
|
|
1700
1820
|
case 'native-delete': return t('compat.ov.nativeDelete')
|
|
1701
1821
|
case 'host-identity': return t('compat.ov.hostIdentity')
|
|
1702
1822
|
case 'memory': return t('compat.ov.memory')
|
|
@@ -1774,6 +1894,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
1774
1894
|
: t('compat.ov.toolTable.mixed', { hidden: hidden, total: row.total, defaults: row.defaults })
|
|
1775
1895
|
}
|
|
1776
1896
|
if (row.key === 'native-delete' && row.state === 'ok') return t('compat.ov.nativeDelete.ok')
|
|
1897
|
+
if (row.key === 'slash-commands') {
|
|
1898
|
+
// 两段各自一颗开关:只开一段时不能沿用"两段都在"那句(服务端把两个布尔一起带回来了)。
|
|
1899
|
+
if (row.state === 'disabled') return t('compat.ov.slashCommands.off')
|
|
1900
|
+
if (row.slashTools && row.slashQuickPrompts) return t('compat.ov.slashCommands.on')
|
|
1901
|
+
return t(row.slashTools ? 'compat.ov.slashCommands.onTools' : 'compat.ov.slashCommands.onQuick')
|
|
1902
|
+
}
|
|
1777
1903
|
if (row.key === 'host-identity') {
|
|
1778
1904
|
if (row.state === 'unknown') return t('compat.ov.route.unknown')
|
|
1779
1905
|
if (row.state === 'degraded') return t('compat.ov.hostIdentity.degraded', { list: list(row.blockers) })
|
|
@@ -2111,24 +2237,67 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2111
2237
|
}, t('compat.token.edit.cancel'))]))))
|
|
2112
2238
|
}
|
|
2113
2239
|
|
|
2114
|
-
//
|
|
2115
|
-
// "极简这类预设下要不要破例"
|
|
2240
|
+
// 注入设置块:本插件注入给模型哪些内容的开关 + 对话输入框的斜杠命令入口(`/` 菜单里的
|
|
2241
|
+
// 「工具」与「快捷提示词」两段)。总开关只管"极简这类预设下要不要破例",各域勾选在任何
|
|
2242
|
+
// 预设下都生效。
|
|
2116
2243
|
//
|
|
2117
2244
|
// 排版口径(用户裁定 2026-09-16):勾选**横排**(短标签一行放得下,不再一人一行竖着排),
|
|
2118
2245
|
// 文案只说到"有这个内容"为止 —— 用户是普通使用者,不解释 persona/载体/路径这些内部机制。
|
|
2246
|
+
//
|
|
2247
|
+
// 斜杠命令入口并进这一块(用户 2026-09-30 裁定):它原先自己成一节,那一节的说明又长又和
|
|
2248
|
+
// 勾选框自己的标签重复。现在只剩一条分组标题 + 两段各自一颗勾选框(「工具」/「快捷提示词」,
|
|
2249
|
+
// 各自就是一个源,所以能各挂各的)+ 一句"重启才生效"。
|
|
2250
|
+
const slashParts = []
|
|
2251
|
+
if (slash) {
|
|
2252
|
+
// 行末那枚标签是"本次启动真挂上了没":开关值说的是"要不要",挂没挂上只有浏览器知道
|
|
2253
|
+
// (服务端看不到源注册的结果),两件事不能合成一句 —— 合了必有一句是假的。标签只说
|
|
2254
|
+
// 已生效 / 未生效,**为什么**没生效挂在悬停那句里(重名抢占 / 宿主没这个服务 / 还没就绪,
|
|
2255
|
+
// 三种的处置完全不同)—— 见 48-slash.js 的 setSlashMount。
|
|
2256
|
+
const liveTag = function (key) {
|
|
2257
|
+
const state = slashMountStateOf(key)
|
|
2258
|
+
const on = state === 'registered'
|
|
2259
|
+
return React.createElement('span', { className: 'dsm-pick-meta' },
|
|
2260
|
+
React.createElement('span', {
|
|
2261
|
+
className: 'dsm-tag' + (on ? ' dsm-tag-on' : ''), title: t('compat.slash.mount.' + state),
|
|
2262
|
+
}, t(on ? 'compat.slash.live.on' : 'compat.slash.live.off')))
|
|
2263
|
+
}
|
|
2264
|
+
slashParts.push(
|
|
2265
|
+
React.createElement('div', { key: 'slash-head', className: 'dsm-seg-group' },
|
|
2266
|
+
React.createElement('span', null, t('compat.slash.title')),
|
|
2267
|
+
React.createElement('span', { className: 'dsm-seg-group-line' })),
|
|
2268
|
+
React.createElement('label', { key: 'slash-tools', className: 'dsm-pick' },
|
|
2269
|
+
React.createElement('input', {
|
|
2270
|
+
type: 'checkbox', checked: slash.tools !== false,
|
|
2271
|
+
onChange: function (e) { saveSlash({ tools: e.target.checked === true }) },
|
|
2272
|
+
}),
|
|
2273
|
+
React.createElement('span', { className: 'dsm-pick-main' },
|
|
2274
|
+
React.createElement('span', { className: 'dsm-pick-name' }, t('compat.slash.tools')),
|
|
2275
|
+
React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.slash.tools.desc'))),
|
|
2276
|
+
liveTag('tools')),
|
|
2277
|
+
React.createElement('label', { key: 'slash-quick', className: 'dsm-pick' },
|
|
2278
|
+
React.createElement('input', {
|
|
2279
|
+
type: 'checkbox', checked: slash.quickPrompts !== false,
|
|
2280
|
+
onChange: function (e) { saveSlash({ quickPrompts: e.target.checked === true }) },
|
|
2281
|
+
}),
|
|
2282
|
+
React.createElement('span', { className: 'dsm-pick-main' },
|
|
2283
|
+
React.createElement('span', { className: 'dsm-pick-name' }, t('compat.slash.quick')),
|
|
2284
|
+
React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.slash.quick.desc'))),
|
|
2285
|
+
liveTag('quickPrompts')),
|
|
2286
|
+
React.createElement('p', { key: 'slash-restart', className: 'dsm-help' }, t('compat.slash.restartHint')))
|
|
2287
|
+
}
|
|
2288
|
+
const injectParts = []
|
|
2119
2289
|
if (inject) {
|
|
2120
2290
|
const domains = (inject && inject.domains) || {}
|
|
2121
|
-
push(
|
|
2122
|
-
|
|
2123
|
-
React.createElement('label', { className: 'dsm-pick' },
|
|
2291
|
+
injectParts.push(
|
|
2292
|
+
React.createElement('label', { key: 'force', className: 'dsm-pick' },
|
|
2124
2293
|
React.createElement('input', { type: 'checkbox', checked: inject.underSuppressingPresets === true, onChange: function () { saveInject({ underSuppressingPresets: inject.underSuppressingPresets !== true }) } }),
|
|
2125
2294
|
React.createElement('span', { className: 'dsm-pick-main' },
|
|
2126
2295
|
React.createElement('span', { className: 'dsm-pick-name' }, t('compat.inject.force')),
|
|
2127
2296
|
React.createElement('span', { className: 'dsm-pick-desc' }, t('compat.inject.force.desc')))),
|
|
2128
|
-
React.createElement('div', { className: 'dsm-seg-group' },
|
|
2297
|
+
React.createElement('div', { key: 'domains-head', className: 'dsm-seg-group' },
|
|
2129
2298
|
React.createElement('span', null, t('compat.inject.domains')),
|
|
2130
2299
|
React.createElement('span', { className: 'dsm-seg-group-line' })),
|
|
2131
|
-
React.createElement('div', { className: 'dsm-inject-domains' },
|
|
2300
|
+
React.createElement('div', { key: 'domains', className: 'dsm-inject-domains' },
|
|
2132
2301
|
['scene', 'memory', 'mcp', 'skills', 'subagents', 'prompt'].map(function (key) {
|
|
2133
2302
|
// 技能与提示词两域在标准类预设下由宿主送、本插件让位 —— 关掉它们时本插件会连
|
|
2134
2303
|
// 宿主那条一起拦下(见 context-inject.ts 的 officialKindsToSuppress)。这里给
|
|
@@ -2141,7 +2310,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
2141
2310
|
saveInject({ domains: nextDomains })
|
|
2142
2311
|
} }),
|
|
2143
2312
|
React.createElement('span', null, t('compat.inject.domain.' + key)))
|
|
2144
|
-
})))
|
|
2313
|
+
})))
|
|
2314
|
+
}
|
|
2315
|
+
// 两块数据各自独立加载:inject 读不到时斜杠入口照样要出现,反之也一样,所以整块的条件是
|
|
2316
|
+
// 「任一有值」—— 缺的那半只是不出现,不把另一半一起吞掉。
|
|
2317
|
+
if (inject || slash) {
|
|
2318
|
+
push(section(inject ? t('compat.inject') : t('compat.slash.title'), inject ? t('compat.inject.hint') : null,
|
|
2319
|
+
React.createElement('div', { className: 'dsm-inject-settings' }, injectParts.concat(slashParts))))
|
|
2145
2320
|
}
|
|
2146
2321
|
// 模型工具表:与「注入」并列的第二种省法 —— 那边省正文,这边省工具 schema。
|
|
2147
2322
|
// 工具表**每个请求都发一遍**(哪怕这一轮用不上),所以关掉的工具是整份不进请求,
|
|
@@ -3193,7 +3368,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3193
3368
|
"source.external.note": "只读接入,启停不改写源文件", "source.dsh.note": "可创建、导入、移到回收站;默认来源必须读取,不能移除或停用",
|
|
3194
3369
|
"detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "元数据", "detail.noFrontmatter": "该技能未提供元数据。", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
|
|
3195
3370
|
"create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话里 skill_manager_save 建的是用户级技能",
|
|
3196
|
-
"import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名", "upload.requirement.memory.3": "带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.4": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "
|
|
3371
|
+
"import.title": "导入技能", "upload.drop.title": "点击或拖入此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.requirement.persona.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.persona.2": "一个 .md = 一个人设,文件名即人设名", "upload.requirement.persona.3": "同名自动跳过,绝不覆盖原有文件", "upload.requirement.memory.1": "支持 .md / .zip(可多选、可拖入)", "upload.requirement.memory.2": "一个 .md = 一条记忆,文件名即记忆名", "upload.requirement.memory.3": "带 SKILL.md 的目录 = bundle 记忆(附件一起进来)", "upload.requirement.memory.4": "zip 里的目录名即场景;没有目录时用下面「导入到场景」的值(留空 = 全局)", "upload.requirement.prompt.1": "支持 .md(可多选、可拖入)", "upload.requirement.prompt.2": "一个 .md = 一份预设,文件名即 id(目录名)", "upload.requirement.prompt.3": "同名已存在则跳过,绝不覆盖", "upload.requirement.session.1": "支持 .jsonl / .json / .md / .txt(一次一个)", "upload.requirement.session.2": "Claude Code / Cursor / Codex 的转录文件,或任意文本", "upload.requirement.session.3": "工作区可留空 = 归入未分组;留空不影响导入", "upload.importing": "正在导入…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md", "select.folder.invalid": "该文件夹里没有 SKILL.md", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
|
|
3197
3372
|
// F-029:服务端错误码补词条(zh 逐字复现服务端文案,命中词条前后中文显示不变;EN 不再回退中文原文)。
|
|
3198
3373
|
"error.custom.absolute": "目录路径必须是绝对路径: {path}", "error.custom.notDirectory": "不是目录: {path}", "error.custom.unreadable": "无法读取目录: {path}", "error.custom.overlap": "目录与已有技能来源重叠: {path} ↔ {other}", "error.custom.invalidKey": "非法的自定义来源 key: {key}", "error.custom.notFound": "自定义来源不存在: {key}",
|
|
3199
3374
|
// 与宿主 http-fence.ts 的 TOKEN_MSG **逐字相同**:同一件事在哪个页面出现都必须说同一句话,
|
|
@@ -3330,7 +3505,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3330
3505
|
"snapshot.export.dir": "导出目录", "snapshot.export.btn": "导出", "snapshot.export.busy": "导出中…", "snapshot.export.done": "快照已写到:{dir}", "snapshot.export.failed": "导出失败",
|
|
3331
3506
|
"snapshot.export.secrets": "完整导出(含明文凭据)", "snapshot.export.secrets.warn": "会把密钥原文一起写进去。这份目录要拷去别的机器或网盘,想清楚再勾。",
|
|
3332
3507
|
"snapshot.counts": "{items}", "snapshot.notes": "说明:{items}",
|
|
3333
|
-
"snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "记忆", "snapshot.domain.skills": "技能", "snapshot.domain.subagents": "子智能体", "snapshot.domain.prompts": "
|
|
3508
|
+
"snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "记忆", "snapshot.domain.skills": "技能", "snapshot.domain.subagents": "子智能体", "snapshot.domain.prompts": "全局提示词", "snapshot.domain.quick-prompts": "快捷提示词", "snapshot.domain.settings": "侧车配置", "snapshot.domain.scenes": "场景档案",
|
|
3334
3509
|
"snapshot.import.dir": "快照位置", "snapshot.import.dir.placeholder": "例如 D:\\backup\\dsh-tool-management-snapshot-…",
|
|
3335
3510
|
"snapshot.import.preview": "导入", "snapshot.import.busy": "处理中…",
|
|
3336
3511
|
"snapshot.import.row": "{domain} {count} 项 · 重名 {clashes}",
|
|
@@ -3355,7 +3530,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3355
3530
|
"compat.audit.op.ruleSave": "保存记忆", "compat.audit.op.ruleDelete": "删除记忆", "compat.audit.op.ruleSwitch": "启停记忆", "compat.audit.op.ruleImport": "导入记忆", "compat.audit.op.ruleAttach": "挂到场景 / 摘下", "compat.audit.op.ruleIndex": "重建记忆索引", "compat.audit.op.memorySwitch": "切换生效记忆集",
|
|
3356
3531
|
"compat.audit.op.sceneSave": "保存场景档案", "compat.audit.op.sceneDelete": "删除场景", "compat.audit.op.sceneRebind": "改绑提示词", "compat.audit.op.sceneLock": "场景锁定开关", "compat.audit.op.sceneEnter": "进入 / 退出场景", "compat.audit.op.engineApply": "场景引擎应用档案", "compat.audit.op.engineRestore": "场景引擎还原档案",
|
|
3357
3532
|
"compat.audit.op.personaSave": "保存人设", "compat.audit.op.personaDelete": "删除人设", "compat.audit.op.personaImport": "导入人设", "compat.audit.op.personaSwitch": "启停人设",
|
|
3358
|
-
"compat.audit.op.presetSave": "保存提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
|
|
3533
|
+
"compat.audit.op.presetSave": "保存提示词", "compat.audit.op.quickSave": "保存快捷提示词", "compat.audit.op.quickSwitch": "启停快捷提示词", "compat.audit.op.quickDelete": "删除快捷提示词", "compat.audit.op.presetDelete": "删除提示词", "compat.audit.op.presetImport": "导入提示词", "compat.audit.op.presetApply": "应用提示词",
|
|
3359
3534
|
"compat.audit.op.trashRestore": "从回收站恢复", "compat.audit.op.trashDelete": "永久删除",
|
|
3360
3535
|
"compat.audit.op.toolTable": "模型工具表", "compat.audit.op.injectSettings": "注入设置", "compat.audit.op.sceneSettings": "场景页设置", "compat.audit.op.backupsClean": "清理旧备份",
|
|
3361
3536
|
"compat.audit.op.sessionArchive": "归档会话", "compat.audit.op.sessionUnarchive": "取消归档", "compat.audit.op.sessionDelete": "删除会话", "compat.audit.op.sessionRetention": "改保留策略", "compat.audit.op.sessionImport": "导入会话", "compat.audit.op.sessionExport": "导出会话", "compat.audit.op.sessionRegister": "登记工作区",
|
|
@@ -3413,6 +3588,63 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3413
3588
|
"compat.ov.hostIdentity": "宿主身份与模块", "compat.ov.hostIdentity.ok": "模块与宿主同源(宿主 DSH {version})", "compat.ov.hostIdentity.asar": "官方包与宿主同版本(DSH {version}),但宿主跑在 {name} 里、插件在归档外", "compat.ov.hostIdentity.degraded": "存在阻塞项:{list}",
|
|
3414
3589
|
"compat.ov.memory": "记忆", "compat.ov.prompts": "提示词", "compat.ov.subagents": "子智能体", "compat.ov.scenes": "场景档案",
|
|
3415
3590
|
"compat.ov.plain.ok": "无降级上报(装配失败会出现在这里)",
|
|
3591
|
+
// ── 斜杠命令「工具」段(对话输入框里直接管六个域;实现见 48-slash.js)──────
|
|
3592
|
+
// 段标题与六个域名出现在宿主 `/` 菜单里;域名要短 —— 菜单行右侧还要留位置给摘要与 › 箭头。
|
|
3593
|
+
"slash.section.tools": "工具",
|
|
3594
|
+
"slash.domain.scene": "场景", "slash.domain.skill": "技能", "slash.domain.mcp": "MCP 服务器",
|
|
3595
|
+
"slash.domain.agent": "子智能体", "slash.domain.preset": "提示词", "slash.domain.memory": "记忆",
|
|
3596
|
+
"slash.sum.current": "当前:{name}", "slash.sum.count": "{n} 项",
|
|
3597
|
+
"slash.sum.onoff": "{on} 启用 / {off} 停用", "slash.sum.sceneGlobal": "场景「{scene}」+ 全局 · {n} 条",
|
|
3598
|
+
"slash.sum.quick": "快捷 {on}/{total}",
|
|
3599
|
+
"slash.group.noScene": "未归场景",
|
|
3600
|
+
"slash.state.on": "已启用", "slash.state.off": "未启用",
|
|
3601
|
+
"slash.skill.sourceRow": "目录",
|
|
3602
|
+
"slash.did.on": "已启用 {name}", "slash.did.off": "已停用 {name}",
|
|
3603
|
+
"slash.did.scene": "已进入场景「{name}」", "slash.did.sceneOff": "已退出场景「{name}」(只注入全局与公共基线)",
|
|
3604
|
+
"slash.did.preset": "已应用基线「{name}」",
|
|
3605
|
+
"slash.did.quickOn": "已启用快捷提示词「{name}」(在 / 的「快捷提示词」段里可见)",
|
|
3606
|
+
"slash.did.quickOff": "已停用快捷提示词「{name}」(不再出现在 / 的「快捷提示词」段,正文留着)",
|
|
3607
|
+
"slash.did.sourceOn": "已启用目录「{name}」,其中原本启用的技能随之生效",
|
|
3608
|
+
"slash.did.sourceOff": "已停用目录「{name}」(整个文件夹不再读取)",
|
|
3609
|
+
"slash.did.already": "当前就是「{name}」",
|
|
3610
|
+
"slash.err.load": "读不到{domain}列表:{error}", "slash.err.gone": "这一项已经不在了,请重新打开列表",
|
|
3611
|
+
"slash.err.failed": "改动没有生效", "slash.err.pick": "请用 ↑↓ 选中一行后按 Enter(这半截命令没有发出去)",
|
|
3612
|
+
// ── 快捷提示词(提示词页的第二段;对话框 `/` 菜单里另起一段)────────────────
|
|
3613
|
+
// 两类提示词的差别全在这几句里说清:一份会写进 AGENTS.md 常驻上下文,一份只是贴进
|
|
3614
|
+
// 输入框的文字。这句话必须出现在**选它的那一步**(弹窗顶部的类型行),不然用户要到
|
|
3615
|
+
// 点完"应用"才知道自己存的是一段会被原样注入的字。
|
|
3616
|
+
"prompts.kind.label": "提示词类型",
|
|
3617
|
+
"prompts.kind.global": "全局提示词", "prompts.kind.quick": "快捷提示词",
|
|
3618
|
+
"prompts.kind.global.hint": "应用后写入 AGENTS.md,下一轮对话生效;可被场景绑定。",
|
|
3619
|
+
"prompts.kind.quick.hint": "对话框使用:将提示词输入对话框。",
|
|
3620
|
+
"prompts.group.global": "全局提示词", "prompts.group.quick": "快捷提示词", "prompts.bulk.quick": "把这一组里当前可见的快捷提示词全部打开 / 全部关闭", "prompts.bulk.failed": "{done} 条已改,{failed} 条没改成",
|
|
3621
|
+
"quickprompt.stat": "条快捷提示词",
|
|
3622
|
+
"quickprompt.toggle": "在 / 里显示",
|
|
3623
|
+
"quickprompt.toggle.hint": "关掉后这条不再出现在对话框 / 的「快捷提示词」段里;正文与备注都留着,随时可以再打开(「工具 › 提示词」那一域里也列着,在那里同样拨得动)",
|
|
3624
|
+
"quickprompt.empty": "还没有快捷提示词。点「新增提示词」,类型选「快捷提示词」。",
|
|
3625
|
+
"quickprompt.edit.title": "编辑快捷提示词",
|
|
3626
|
+
"quickprompt.field.id.hint": "也是 / 菜单里那一行的名字,可以中文。",
|
|
3627
|
+
"quickprompt.field.desc.placeholder": "一句话记住它是干什么的(只给你自己看)",
|
|
3628
|
+
"quickprompt.field.content": "正文",
|
|
3629
|
+
"quickprompt.field.content.placeholder": "这段文字会在你点的时候原样贴进对话框",
|
|
3630
|
+
"quickprompt.remove.title": "删除快捷提示词",
|
|
3631
|
+
"quickprompt.remove.hint": "删除后进入回收站,可以从本页的「回收站」恢复。",
|
|
3632
|
+
"trash.group.quick": "快捷提示词",
|
|
3633
|
+
"trash.section.quick.sub": "快捷提示词目录已移走:恢复能还原,永久删除会删掉文件",
|
|
3634
|
+
"slash.section.quick": "快捷提示词",
|
|
3635
|
+
"slash.quick.empty": "这条快捷提示词没有正文,先到「设置 → 工具 → 提示词」补上",
|
|
3636
|
+
"compat.ov.slashCommands": "斜杠命令入口", "compat.ov.slashCommands.on": "两段都开(打 / 唤出「工具」与「快捷提示词」两段)",
|
|
3637
|
+
"compat.ov.slashCommands.onTools": "只开「工具」段", "compat.ov.slashCommands.onQuick": "只开「快捷提示词」段",
|
|
3638
|
+
"compat.ov.slashCommands.off": "两段都已关闭(本页可重新打开)",
|
|
3639
|
+
"compat.slash.title": "斜杠命令入口",
|
|
3640
|
+
"compat.slash.tools": "「工具」", "compat.slash.tools.desc": "进场景、启停技能 / MCP / 子智能体 / 提示词(全局与快捷两类)/ 记忆",
|
|
3641
|
+
"compat.slash.quick": "「快捷提示词」", "compat.slash.quick.desc": "取用常用话术、一键贴进输入框(开关在「工具 › 提示词」里)",
|
|
3642
|
+
"compat.slash.restartHint": "斜杠命令修改后重启 DSH 即可生效",
|
|
3643
|
+
"compat.slash.live.on": "已生效", "compat.slash.live.off": "未生效",
|
|
3644
|
+
"compat.slash.mount.registered": "本次启动已挂上", "compat.slash.mount.pending": "还没挂上(宿主服务未就绪)", "compat.slash.mount.unregistered": "本次启动已撤下",
|
|
3645
|
+
"compat.slash.mount.disabled": "这一段的开关关着",
|
|
3646
|
+
"compat.slash.mount.missing": "宿主没有 inputTriggers 服务,这个入口不会出现",
|
|
3647
|
+
"compat.slash.mount.conflict": "源名与另一个插件重名,本次没挂上",
|
|
3416
3648
|
// 降级 note 的模板(detailKey 见 src/compat/runtime-notes.ts;占位符与英文版一一对应)。
|
|
3417
3649
|
"compat.note.skillsProvider": "技能 provider 装配失败({reason}):技能启用/停用与 agent 技能目录这一轮不生效。",
|
|
3418
3650
|
"compat.note.contextInjection": "注入通道装配失败({reason}):全部注入域的内容这一轮不会被送进模型。",
|
|
@@ -3485,7 +3717,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3485
3717
|
"compat.token.failed": "读取令牌状态失败,请刷新后重试。",
|
|
3486
3718
|
"compat.inject.domains": "注入内容",
|
|
3487
3719
|
"compat.inject.domain.scene": "场景", "compat.inject.domain.memory": "记忆", "compat.inject.domain.mcp": "MCP",
|
|
3488
|
-
"compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "
|
|
3720
|
+
"compat.inject.domain.subagents": "子智能体", "compat.inject.domain.prompt": "全局提示词",
|
|
3489
3721
|
"compat.inject.domain.skills": "技能",
|
|
3490
3722
|
"compat.inject.domain.carrier.title": "关掉后,系统自带的那份也会一并停掉",
|
|
3491
3723
|
"compat.tools": "模型工具表", "compat.tools.hint": "哪些工具发给模型:工具表每一轮都随请求发一遍,关掉的整份不发(≈ 是估算)",
|
|
@@ -3545,8 +3777,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3545
3777
|
"compat.tools.about.subagent_manager_run": "按人设派子智能体",
|
|
3546
3778
|
"compat.tools.about.subagent_manager_switch": "启用/停用人设",
|
|
3547
3779
|
"compat.tools.about.subagent_manager_save": "新建/修改人设",
|
|
3548
|
-
"compat.tools.about.prompt_manager_list": "
|
|
3549
|
-
"compat.tools.about.prompt_manager_apply": "
|
|
3780
|
+
"compat.tools.about.prompt_manager_list": "列出全局提示词预设",
|
|
3781
|
+
"compat.tools.about.prompt_manager_apply": "应用一份全局提示词预设",
|
|
3550
3782
|
"compat.tools.hover.memory_manager_list": "列出记忆的 id、所属场景、启停与一句话描述,并标明注入按哪个场景走。默认只列这次真的会注入的记忆;all=true 会把停用、场景未启用、被同名覆盖的一起列出来。场景本身(档案、绑定的预设、锁没锁)在 scene_manager_list。只看不改。",
|
|
3551
3783
|
"compat.tools.hover.memory_manager_read": "按 id(<场景>/<名字>)读一条记忆的完整正文。清单里只有一句话描述,要看正文就用它 —— 尤其是被停用、场景没启用、或被体积预算挤掉的记忆。",
|
|
3552
3784
|
"compat.tools.hover.memory_manager_switch": "只拨索引里的开关,.md 文件一个字不动,随时能拨回来。停用的记忆不再进上下文,等于把注入预算让给别人 —— 想省 token 优先用它,而不是删。",
|
|
@@ -3565,7 +3797,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3565
3797
|
"compat.tools.hover.subagent_manager_run": "把任务交给某个具名人设:它拿到人设当自己的系统提示词、在自己的上下文里做完、只回最终结果。开 inherit 它会看到本次会话已完成的对话。花真 token,默认要用户确认。",
|
|
3566
3798
|
"compat.tools.hover.subagent_manager_switch": "停用的人设不进目录也不能被委派;被场景绑定的人设,在进入该场景时会自动启用。",
|
|
3567
3799
|
"compat.tools.hover.subagent_manager_save": "新建人设,或改掉已存在的那份 —— 回执会说清走了哪条。可改名字、描述、正文(正文会作为子代理的系统提示词)与输出要求,没传的字段保持原样;改名会连带改场景与档案里的绑定。provider / 模型 / 工具限制只留给面板(模型看不到自己改对了没有,所以不给)。描述决定模型什么时候会选它。",
|
|
3568
|
-
"compat.tools.hover.prompt_manager_list": "
|
|
3800
|
+
"compat.tools.hover.prompt_manager_list": "列出全局提示词预设(写进 AGENTS.md 的那一类)与摘要,标明当前生效的是哪一份 —— 同时只有一份生效。要换用哪份之前先用它确认名字。快捷提示词不在这个域里:那是用户自己在输入框里点出来的一段话,不进上下文,模型这边没有可做的动作。",
|
|
3569
3801
|
"compat.tools.hover.prompt_manager_apply": "把某份预设应用到用户级 AGENTS.md,之后的对话立刻按它走。一次只有一份在生效,换一份就是替换上一份。",
|
|
3570
3802
|
"compat.live.title": "注入实况", "compat.live.hint": "模型实际看到的内容(只读)",
|
|
3571
3803
|
"compat.live.never": "这段对话还没投过", "compat.live.delivered": "本段对话已投递 {count} 条 · 最近 {time}",
|
|
@@ -3600,7 +3832,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3600
3832
|
"scenes.mode.active": "当前模式:{name}", "scenes.mode.profile": "档案:{parts}", "scenes.mode.noProfile": "这个场景还没有档案",
|
|
3601
3833
|
"scenes.empty": "还没有专门设置的场景;全局记忆不需要场景就能注入,需要切换 MCP / 技能 / 人设时再新建。",
|
|
3602
3834
|
"scenes.search.placeholder": "搜索场景名或描述", "scenes.empty.search": "没有匹配的场景",
|
|
3603
|
-
"scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.memories": "记忆 {count} 条",
|
|
3835
|
+
"scenes.profile.mcp": "MCP {count} 台", "scenes.profile.skills": "技能 {count} 个", "scenes.profile.subagents": "子智能体 {count} 个", "scenes.profile.quickPrompts": "快捷提示词 {count} 条", "scenes.profile.memories": "记忆 {count} 条",
|
|
3604
3836
|
"scenes.mcp.hint": "添加「MCP 工具集」后,勾选要启用的服务器(含未运行的);「选工具」可细化到具体工具。没勾的、以及整段没建的,进场景时一律停用(含停掉正在运行的进程)。",
|
|
3605
3837
|
"scenes.seg.checked": "{checked}/{total} 已勾选",
|
|
3606
3838
|
"bulk.selectAll": "全选", "bulk.unselectAll": "取消全选",
|
|
@@ -3679,9 +3911,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3679
3911
|
"memory.result.sceneCreated": "已创建场景:{name}", "memory.result.sceneUpdated": "已保存场景:{name}", "memory.result.sceneRemoved": "已删除场景:{name}", "memory.result.sceneRemoved.withMemories": "已删除场景:{name}(连同 {count} 项记忆,可在回收站整条恢复)",
|
|
3680
3912
|
"memory.table.name": "记忆名称与描述", "memory.table.tags": "标记",
|
|
3681
3913
|
"error.rules.invalidArgs": "缺少参数:需要 all:true(回到全部启用)或 scenes:[...](显式收窄)", "error.rules.invalidGroup": "场景/分组名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.invalidName": "记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : \" | ? *、不以 . 开头)", "error.rules.descriptionRequired": "描述不能为空", "error.rules.descriptionTooLong": "描述过长(不能超过 500 字符)", "error.rules.bodyRequired": "正文不能为空", "error.rules.tooLarge": "规则内容过大", "error.rules.shadowed": "规则被同名 bundle 遮蔽,无法写入", "error.rules.exists": "同名记忆已存在,无法写入", "error.rules.notFound": "规则不存在", "error.rules.budgetExceeded": "场景记忆段超出预算", "error.rules.ioFailed": "文件操作失败", "error.rules.outsideRoot": "拒绝操作:解析后的落点不在记忆根目录内(可能存在指向根外的目录链接)", "error.rules.nameTaken": "目标名称已被占用", "error.rules.sceneNotEmpty": "场景不为空,无法删除", "error.rules.notBundle": "该记忆是 flat(单文件),不能带附件", "error.rules.noFiles": "没有选择附件", "error.rules.emptyFile": "附件内容为空", "error.rules.fileTooLarge": "附件过大(单个上限 {limit} MB)", "error.rules.tooManyFiles": "一次最多 {limit} 个附件", "error.rules.sceneInMode": "该场景正处在当前模式,请先退出模式再删除",
|
|
3682
|
-
"memory.archive.emptySection": "没有勾选任何条目 = 全部停用(整段没建也一样)", "memory.archive.emptySubagents": "没有勾选任何条目 = 本场景不启用任何人设(整段没建也一样)",
|
|
3683
3914
|
"subagents.title": "子智能体", "subagents.stat.total": "个子智能体", "subagents.stat.limited": "个有工具限制", "subagents.desc": "管理子智能体:新建、导入人设,正文即子代理的系统提示词。",
|
|
3684
|
-
"prompts.desc": "
|
|
3915
|
+
"prompts.desc": "管理提示词:全局那份写入 AGENTS.md,快捷那份只在对话框里取用。",
|
|
3685
3916
|
"sessions.desc": "管理已归档会话:可恢复或永久删除,超保留期自动清理。",
|
|
3686
3917
|
"subagents.new": "新增子智能体", "subagents.empty": "还没有子智能体——点「新增子智能体」创建第一个。", "subagents.empty.search": "没有匹配的子智能体", "subagents.search.placeholder": "搜索人设名或描述",
|
|
3687
3918
|
"subagents.create": "新建人设", "subagents.edit": "编辑人设",
|
|
@@ -3727,7 +3958,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3727
3958
|
"import.none": "没有导入任何文件(全部被跳过)", "import.skipped": "已跳过:{items}",
|
|
3728
3959
|
"error.import.noFiles": "没有选择要导入的文件",
|
|
3729
3960
|
"nav.title": "工具",
|
|
3730
|
-
"agm.stat.total": "
|
|
3961
|
+
"agm.stat.total": "个全局提示词", "agm.stat.active": "个生效中", "agm.loading": "加载中…", "agm.empty": "暂无提示词,点「新增提示词」创建", "agm.empty.search": "没有匹配的提示词", "agm.search.placeholder": "搜索提示词 id 或描述", "agm.btn.delete.blocked.refs": "不能删除:{refs}。先改掉引用(换绑提示词 / 退出场景 / 应用别的预设)再删除。", "agm.ref.sep": ";", "agm.ref.scene": "场景「{scene}」绑定了它", "agm.ref.sceneActive": "场景「{scene}」(已启用)绑定了它", "agm.ref.file": "AGENTS.md 当前内容就是它", "agm.ref.restore": "退出场景后要恢复的全局提示词", "agm.sceneLock.notice": "场景「{scene}」接管中:当前生效的是它绑定的「{id}」。在这里应用别的预设 = 把该场景改绑到那一份(会同步写入场景档案)。", "agm.active": "生效中", "agm.active.hint.scene": "当前生效:场景「{scene}」绑定的提示词预设", "agm.active.hint.file": "当前生效:AGENTS.md 就是这一份", "agm.file.applied": "文件里是它", "agm.file.applied.hint": "AGENTS.md 当前内容是它;启用的场景绑了别的提示词,切换场景时已按场景把文件改成那一份", "agm.apply.hint": "应用后写入 AGENTS.md;宿主每轮重读该文件,下一轮对话生效", "agm.apply.switch": "应用提示词", "agm.apply.switch.on": "当前生效的就是这一份 · 点一下重新写回 AGENTS.md(撤销手动改动)", "agm.applyModal.title": "应用预设", "agm.edit.title": "编辑预设", "agm.remove.title": "删除预设", "agm.btn.edit": "编辑", "agm.btn.delete": "删除", "agm.btn.new": "新增提示词", "agm.btn.import": "导入提示词", "agm.result.imported": "已导入 {count} 份预设:{names}", "agm.btn.save": "保存", "agm.btn.create": "创建", "agm.btn.confirmRemove": "确认删除", "agm.btn.confirmApply": "确认应用", "agm.field.id": "id(目录名)", "agm.field.id.hint": "中文、空格、点都可以;不能含斜杠、反斜杠与 : * ? < > 等符号;≤64 字符。改 id 等于目录改名。", "agm.field.id.placeholder": "例如:工作基线", "agm.field.content": "内容", "agm.field.content.placeholder": "直接写这份预设的正文(Markdown);留空 = 用空白模板", "agm.field.desc": "描述(模型在清单里看得到)", "agm.field.desc.placeholder": "这份预设是干什么的、什么时候用它", "agm.result.renamed": "已保存,id 改为「{id}」", "agm.result.rebound": "(同步了 {count} 个场景的绑定)", "agm.remove.suffix": " ?此操作不可恢复。", "agm.apply.prefix": "将把 ", "agm.apply.suffix": " 的内容写入 ~/.dsh/AGENTS.md。", "agm.apply.note": "下一轮对话生效;当前内容已备份到 __last-applied__(保留最近 5 代)", "hist.err.load": "加载失败", "hist.err.unarchive": "恢复失败", "hist.err.delete": "删除失败", "hist.err.retention": "设置失败", "hist.err.import": "导入失败", "hist.err.sessions": "加载会话列表失败", "hist.err.export": "导出失败", "hist.err.archive": "归档失败", "hist.retention.forever": "永久保留", "hist.retention.days": "{count} 天", "hist.arch.all": "全部会话", "hist.arch.archived": "仅已归档", "hist.arch.live": "仅未归档", "hist.arch.missing": "仅目录丢失", "hist.ws.all": "全部工作区", "hist.ungrouped": "未分组", "hist.group.removed": "已移除", "hist.group.removed.hint": "该目录当前在 DSH 里没有工作区登记;分组按会话目录重建,重新登记后会自动并回同一组", "hist.group.dirMissing": "目录已不存在", "hist.group.dirMissing.hint": "工作区目录已从磁盘删除,无法重新登记;组内会话仍可恢复(归入未分组)或永久删除", "hist.btn.registerWs": "重新登记", "hist.btn.registerWs.hint": "重新登记这个目录,并把该目录下已有的会话挂回该组(只写工作区记账,不移动、不删除文件与会话)", "hist.register.title": "重新登记?", "hist.register.body": "将为「{title}」在 DSH 中重新创建一条工作区登记,并把该目录下已有的会话挂回这个工作区(恢复其分组)。只写工作区记账,不移动、不删除任何文件与会话。", "hist.register.confirm": "确认登记", "hist.register.done.title": "已重新登记", "hist.register.done.body": "工作区「{title}」已登记;该目录下的归档会话会在刷新后并入这一组。", "hist.register.done.attached": "已把该目录下 {count} 个已有会话挂回这个工作区。", "hist.register.done.skipped": "{count} 个会话未能挂回:{reason}", "hist.err.register": "登记失败", "scenes.field.prompt": "提示词预设", "scenes.agents.note.applied": "已把「{id}」写入 AGENTS.md", "scenes.agents.note.restored": "已恢复进场景前的 AGENTS.md", "scenes.agents.note.error": "AGENTS.md 未写入:{reason}", "scenes.agents.note.baselineLost": "⚠ 退出场景时将恢复不回原来的 AGENTS.md:{reason}", "scenes.field.prompt.hint": "为当前场景设置全局提示词", "scenes.prompt.activeTag": "生效中", "scenes.prompt.noPresets": "还没有提示词预设;可先去「提示词」页新建一个。", "scenes.prompt.bound": "提示词:{id}", "scenes.prompt.live": "提示词生效中:{id}", "scenes.prompt.missing": "预设不存在:{id}", "scenes.prompt.tag.hint": "这个场景绑定的提示词预设", "scenes.prompt.mismatch.hint": "场景绑定的「{id}」与 AGENTS.md 当前内容不一致(可能被手动改过);到提示词页对它点「重新应用」写回", "scenes.enable.blocked": "已启用场景「{name}」;先关掉它才能启用别的", "scenes.result.created.collapsed": "场景「{name}」已创建,默认不启动(原先的「全部启用」已收敛为单选)", "scenes.legacyAll": "检测到 {count} 个场景同时处于启用状态(历史「全部启用」遗留);除「全局」外同时只能启用一个。", "scenes.legacyAll.fix": "收敛为单选", "scenes.preview.title": "进入场景「{name}」前,先看一眼会改什么", "scenes.preview.loading": "正在计算这次会改什么…", "scenes.preview.failed": "预览没算出来:{error}", "scenes.preview.enter": "确认进入", "scenes.preview.dismiss": "不再显示", "scenes.preview.dismiss.title": "关闭这张卡,并且以后进入场景前不再弹(想恢复用页面右上角的「开启提醒」)", "scenes.preview.toggle.off": "关闭提醒", "scenes.preview.toggle.on": "开启提醒", "scenes.preview.toggle.title": "进入场景前先弹一张「会改什么」的卡", "scenes.preview.nochange": "什么都不用改:当前运行时已经和这个场景要的一致了。", "scenes.preview.noArchive": "这个场景还没保存过档案 —— MCP / 技能 / 人设三个域都按「一个都没勾」处理,进入即全部停用。", "scenes.preview.exitFirst": "先退出「{scene}」:它改过的东西按进场景前的快照还原。", "scenes.preview.exitNoSnapshot": "当前在场景「{scene}」里,但进场景前的快照已丢失:退出只能清掉模式标记,运行时回不到原样。", "scenes.preview.row.serversOn": "启用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.serversOff": "停用 {count} 台 MCP 服务器:{names}", "scenes.preview.row.toolsOn": "放回 {count} 个 MCP 工具:{names}", "scenes.preview.row.toolsOff": "停用 {count} 个 MCP 工具:{names}", "scenes.preview.row.sourcesOn": "启用 {count} 个技能目录:{names}", "scenes.preview.row.sourcesOff": "停用 {count} 个技能目录:{names}", "scenes.preview.row.skillsOn": "启用 {count} 个技能:{names}", "scenes.preview.row.skillsOff": "停用 {count} 个技能:{names}", "scenes.preview.row.personasOn": "启用 {count} 个人设:{names}", "scenes.preview.row.personasOff": "停用 {count} 个人设:{names}", "scenes.preview.row.quickOn": "启用 {count} 条快捷提示词:{names}", "scenes.preview.row.quickOff": "停用 {count} 条快捷提示词:{names}", "scenes.preview.enter.servers": "启用 {count} 台 MCP:{names}", "scenes.preview.enter.memories": "启用 {count} 条记忆:{names}", "scenes.preview.row.notes": "用场景备注覆盖 {count} 台服务器的备注", "scenes.field.toolTable": "模型工具表方案", "scenes.field.toolTable.hint": "为当前场景设置可使用的模型工具方案", "scenes.field.quickPrompts": "快捷提示词", "scenes.field.quickPrompts.current": "当前启用的(保持现状)", "scenes.field.quickPrompts.custom": "按场景勾选", "scenes.field.quickPrompts.none": "还没有快捷提示词,先到「提示词」页新增", "scenes.field.quickPrompts.hint": "为当前场景设置快捷提示词", "scenes.field.quickPrompts.list": "本场景启用", "scenes.form.section.basic": "基本信息", "scenes.form.section.binding": "场景绑定", "scenes.field.toolTable.none": "不绑定(保持现状)", "scenes.field.toolTable.factory": "出厂默认(关 {count} 条)", "scenes.field.toolTable.count": "关 {count} 条", "scenes.field.toolTable.missing": "方案已不存在", "scenes.preview.row.toolPreset": "工具表将切到方案「{name}」(关 {count} 条)", "scenes.preview.row.toolPresetMissing": "工具表绑的方案「{name}」已不存在,这次不切换", "scenes.preview.more": " 等", "scenes.preview.truncated": "(每一类最多列前 60 项)", "scenes.preview.stale": "档案里有 {count} 项在当前环境里已经不存在,这次不会生效:{items}", "scenes.doctor.title": "有 {count} 处引用指向已经不存在的名字", "scenes.doctor.noneButSkipped": "体检没发现悬空引用", "scenes.doctor.hint": "改名或删除留下的:这一条看着还绑着,运行时按那个名字已经找不到东西了。改回正确的名字,或到档案里取消勾选。", "scenes.doctor.row": "{where}:「{name}」已不存在", "scenes.doctor.more": "另有 {count} 处未列出", "scenes.doctor.skipped": "({domains} 这次没读到,这一类没有算进体检)", "agm.restore.tag": "退出场景后恢复它", "agm.restore.tag.hint": "它是进场景前的全局提示词,退出场景时会按它恢复——删了就没法恢复了", "agm.file.mismatch": "文件里不是它", "agm.file.mismatch.hint": "AGENTS.md 当前内容与这份预设不一致(可能被手动改过);点左边那颗开关把它写回", "hist.restore.note.registered": "已恢复 {count} 个会话;工作区「{title}」已重新登记,会话已挂回该组。", "hist.restore.note.attached": "已恢复 {count} 个会话,并挂回工作区「{title}」。", "hist.restore.note.skip": "{count} 个会话的工作区归属未能恢复:{reason}", "hist.purge.title": "永久删除?", "hist.purge.one": "永久删除会话「{title}」及其全部记录,不可恢复", "hist.purge.batch": "永久删除所选 {count} 个会话及其全部记录,不可恢复", "hist.btn.restore": "恢复", "hist.btn.purge": "永久删除", "hist.btn.confirmPurge": "确认删除", "hist.selectGroup": "全选组 {title}", "hist.group.count": "{count} 个", "hist.selectAll": "全选", "hist.deselectAll": "取消全选", "hist.btn.import": "导入会话", "hist.btn.export": "导出会话", "hist.selected": "已选 {count} 项", "hist.btn.restoreSelected": "恢复所选 ({count})", "hist.btn.deleteSelected": "删除所选 ({count})", "hist.stat.archived": "个归档", "hist.stat.projects": "个项目", "hist.stat.retention": "天保留", "hist.search.placeholder": "搜索标题 / 会话 ID / 项目路径", "hist.loading": "加载中…", "hist.empty.search": "无匹配的归档会话", "hist.empty.none": "暂无归档会话", "hist.import.cwd": "工作区(可留空)", "hist.import.cwd.placeholder": "选择、浏览或直接输入绝对路径;留空 = 归入未分组", "hist.import.workspace": "选工作区", "hist.import.browse": "浏览", "hist.import.workspace.none": "本机还没有已登记的工作区:可以直接输入绝对路径,或用「浏览」选一个文件夹。", "hist.import.drop": "选择或拖入对话文件", "hist.import.done": "已创建会话 {id}({count} 条消息),可在 DSH 会话列表中继续对话。", "hist.import.lossy": "⚠ 整份文本没有识别出任何说话人标记,已折成一条用户消息:原对话的分轮结构可能已丢失。若源文件是 JSONL 或 Markdown 转录稿,请按原扩展名重新导入。", "hist.export.hint": "导出选中会话为转录文件,可再次导入", "hist.export.dir": "导出目录(绝对路径,自动创建)", "hist.export.browse": "浏览并选择文件夹", "hist.btn.browse": "选择", "hist.export.pickDir": "选择导出目录", "hist.export.format": "格式", "hist.export.detail": "导出内容", "hist.export.detail.simple": "仅正文", "hist.export.detail.tools": "正文 + 工具", "hist.export.detail.full": "正文 + 工具 + 思考", "hist.export.scope": "会话范围", "hist.export.workspace": "工作区", "hist.export.sessions": "会话({count} 个)", "hist.export.loading": "加载会话列表…", "hist.cwd.missing": "⚠ 工作区目录已不存在:", "hist.tag.archived": "已归档", "hist.tag.live": "未归档", "hist.export.empty": "没有符合条件的会话", "hist.result.exported": "已导出 {count} 个会话到 {dir}", "hist.result.skipped": ";跳过 {count} 个({ids})", "hist.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": "未知错误",
|
|
3731
3962
|
},
|
|
3732
3963
|
en: {
|
|
3733
3964
|
"title": "Skills", "desc": "Manage skills: enable, import, create, trash.", "link.project": "GitHub", "link.feedback": "Issues",
|
|
@@ -3766,7 +3997,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3766
3997
|
"source.external.note": "Read-only; toggles never rewrite source files", "source.dsh.note": "Create, import, trash; a default source, always read — it cannot be removed or toggled off",
|
|
3767
3998
|
"detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Metadata", "detail.noFrontmatter": "This skill provides no metadata.", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
|
|
3768
3999
|
"create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "skill_manager_save in chat creates user-level skills",
|
|
3769
|
-
"import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The
|
|
4000
|
+
"import.title": "Import skill", "upload.drop.title": "Click or drop here", "upload.drop.copy": "Supported: .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.requirement.persona.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.persona.2": "One .md = one persona; the file name is the persona name", "upload.requirement.persona.3": "Same names are skipped, never overwritten", "upload.requirement.memory.1": "Accepts .md / .zip (multi-select or drag in)", "upload.requirement.memory.2": "One .md = one memory; the file name is the memory name", "upload.requirement.memory.3": "A folder with SKILL.md becomes a bundle memory (attachments included)", "upload.requirement.memory.4": "Folder names inside the zip are scenes; otherwise the scene field below applies (blank = global)", "upload.requirement.prompt.1": "Accepts .md (multi-select or drag in)", "upload.requirement.prompt.2": "One .md = one preset; the file name is the id", "upload.requirement.prompt.3": "A preset with the same name is skipped, never overwritten", "upload.requirement.session.1": "Accepts .jsonl / .json / .md / .txt (one file at a time)", "upload.requirement.session.2": "Transcripts from Claude Code / Cursor / Codex, or any plain text", "upload.requirement.session.3": "The workspace may be left blank (the session goes to Ungrouped)", "upload.importing": "Importing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
|
|
3770
4001
|
"error.custom.absolute": "The folder path must be absolute: {path}", "error.custom.notDirectory": "Not a folder: {path}", "error.custom.unreadable": "Could not read the folder: {path}", "error.custom.overlap": "The folder overlaps an existing skill source: {path} ↔ {other}", "error.custom.invalidKey": "Invalid custom source key: {key}", "error.custom.notFound": "Custom source not found: {key}",
|
|
3771
4002
|
// 逐字对应宿主 http-fence.ts 的 TOKEN_MSG_*(见中文侧注释)。
|
|
3772
4003
|
"error.token.required": "Missing access token, or the wrong one",
|
|
@@ -3898,7 +4129,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3898
4129
|
"snapshot.export.dir": "Export directory", "snapshot.export.btn": "Export", "snapshot.export.busy": "Exporting…", "snapshot.export.done": "Snapshot written to: {dir}", "snapshot.export.failed": "Export failed",
|
|
3899
4130
|
"snapshot.export.secrets": "Full export (plaintext credentials included)", "snapshot.export.secrets.warn": "This writes secrets verbatim. The directory is meant to travel to another machine or a cloud drive — check it only if you mean it.",
|
|
3900
4131
|
"snapshot.counts": "{items}", "snapshot.notes": "Notes: {items}",
|
|
3901
|
-
"snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "Memories", "snapshot.domain.skills": "Skills", "snapshot.domain.subagents": "Personas", "snapshot.domain.prompts": "
|
|
4132
|
+
"snapshot.domain.mcp": "MCP", "snapshot.domain.memories": "Memories", "snapshot.domain.skills": "Skills", "snapshot.domain.subagents": "Personas", "snapshot.domain.prompts": "Global prompts", "snapshot.domain.quick-prompts": "Quick prompts", "snapshot.domain.settings": "Sidecar config", "snapshot.domain.scenes": "Scene profiles",
|
|
3902
4133
|
"snapshot.import.dir": "Snapshot location", "snapshot.import.dir.placeholder": "e.g. D:\\backup\\dsh-tool-management-snapshot-…",
|
|
3903
4134
|
"snapshot.import.preview": "Import", "snapshot.import.busy": "Working…",
|
|
3904
4135
|
"snapshot.import.row": "{domain} {count} item(s) · {clashes} clash",
|
|
@@ -3923,7 +4154,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3923
4154
|
"compat.audit.op.ruleSave": "Saved a memory", "compat.audit.op.ruleDelete": "Deleted a memory", "compat.audit.op.ruleSwitch": "Enabled/disabled a memory", "compat.audit.op.ruleImport": "Imported a memory", "compat.audit.op.ruleAttach": "Attached to / detached from a scene", "compat.audit.op.ruleIndex": "Rebuilt the memory index", "compat.audit.op.memorySwitch": "Switched the active memory set",
|
|
3924
4155
|
"compat.audit.op.sceneSave": "Saved a scene profile", "compat.audit.op.sceneDelete": "Deleted a scene", "compat.audit.op.sceneRebind": "Rebound the prompt", "compat.audit.op.sceneLock": "Changed the scene lock", "compat.audit.op.sceneEnter": "Entered / left a scene", "compat.audit.op.engineApply": "Scene engine applied a profile", "compat.audit.op.engineRestore": "Scene engine restored the snapshot",
|
|
3925
4156
|
"compat.audit.op.personaSave": "Saved a persona", "compat.audit.op.personaDelete": "Deleted a persona", "compat.audit.op.personaImport": "Imported a persona", "compat.audit.op.personaSwitch": "Enabled/disabled a persona",
|
|
3926
|
-
"compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
|
|
4157
|
+
"compat.audit.op.presetSave": "Saved a prompt", "compat.audit.op.quickSave": "Saved a quick prompt", "compat.audit.op.quickSwitch": "Toggled a quick prompt", "compat.audit.op.quickDelete": "Deleted a quick prompt", "compat.audit.op.presetDelete": "Deleted a prompt", "compat.audit.op.presetImport": "Imported a prompt", "compat.audit.op.presetApply": "Applied a prompt",
|
|
3927
4158
|
"compat.audit.op.trashRestore": "Restored from trash", "compat.audit.op.trashDelete": "Deleted forever",
|
|
3928
4159
|
"compat.audit.op.toolTable": "Model tool table", "compat.audit.op.injectSettings": "Injection settings", "compat.audit.op.sceneSettings": "Scenes page settings", "compat.audit.op.backupsClean": "Cleaned old backups",
|
|
3929
4160
|
"compat.audit.op.sessionArchive": "Archived a session", "compat.audit.op.sessionUnarchive": "Unarchived a session", "compat.audit.op.sessionDelete": "Deleted a session", "compat.audit.op.sessionRetention": "Changed retention", "compat.audit.op.sessionImport": "Imported sessions", "compat.audit.op.sessionExport": "Exported sessions", "compat.audit.op.sessionRegister": "Registered a workspace",
|
|
@@ -3981,6 +4212,62 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
3981
4212
|
"compat.ov.hostIdentity": "Host identity & modules", "compat.ov.hostIdentity.ok": "Modules are shared with the host (host DSH {version})", "compat.ov.hostIdentity.asar": "Official packages match the host version (DSH {version}), but the host runs inside {name} while the plugin sits outside it", "compat.ov.hostIdentity.degraded": "Blockers present: {list}",
|
|
3982
4213
|
"compat.ov.memory": "Memory", "compat.ov.prompts": "Prompts", "compat.ov.subagents": "Subagents", "compat.ov.scenes": "Scene profiles",
|
|
3983
4214
|
"compat.ov.plain.ok": "No degradation reported (assembly failures would appear here)",
|
|
4215
|
+
// Slash-command "Tools" section (chat composer). Domain labels stay short — the row keeps
|
|
4216
|
+
// room on the right for the summary and the drill chevron.
|
|
4217
|
+
"slash.section.tools": "Tools",
|
|
4218
|
+
"slash.domain.scene": "Scenes", "slash.domain.skill": "Skills", "slash.domain.mcp": "MCP servers",
|
|
4219
|
+
"slash.domain.agent": "Subagents", "slash.domain.preset": "Prompts", "slash.domain.memory": "Memories",
|
|
4220
|
+
"slash.sum.current": "now: {name}", "slash.sum.count": "{n} items",
|
|
4221
|
+
"slash.sum.onoff": "{on} on / {off} off", "slash.sum.sceneGlobal": "{n} in scene “{scene}” + global",
|
|
4222
|
+
"slash.sum.quick": "quick {on}/{total}",
|
|
4223
|
+
"slash.group.noScene": "No scene",
|
|
4224
|
+
"slash.state.on": "Enabled", "slash.state.off": "Disabled",
|
|
4225
|
+
"slash.skill.sourceRow": "directory",
|
|
4226
|
+
"slash.did.on": "Enabled {name}", "slash.did.off": "Disabled {name}",
|
|
4227
|
+
"slash.did.scene": "Entered scene “{name}”", "slash.did.sceneOff": "Left scene “{name}” (only global and shared stay injected)",
|
|
4228
|
+
"slash.did.preset": "Applied baseline “{name}”",
|
|
4229
|
+
"slash.did.quickOn": "Enabled quick prompt “{name}” — it is back in the / “Quick prompts” block",
|
|
4230
|
+
"slash.did.quickOff": "Disabled quick prompt “{name}” (hidden from the / “Quick prompts” block; the text stays)",
|
|
4231
|
+
"slash.did.sourceOn": "Enabled directory “{name}” — the skills inside it that were on are live again",
|
|
4232
|
+
"slash.did.sourceOff": "Disabled directory “{name}” (the whole folder stops being read)",
|
|
4233
|
+
"slash.did.already": "“{name}” is already the current one",
|
|
4234
|
+
"slash.err.load": "Could not read the {domain} list: {error}", "slash.err.gone": "That entry is gone — reopen the list",
|
|
4235
|
+
"slash.err.failed": "The change did not go through", "slash.err.pick": "Use ↑↓ to pick a row, then Enter (this half-typed command was not sent)",
|
|
4236
|
+
// Quick prompts (second section of the prompts page; its own block in the / menu).
|
|
4237
|
+
// The whole difference between the two kinds lives in these strings: one writes into
|
|
4238
|
+
// AGENTS.md and stays in context, the other is just text pasted into the composer.
|
|
4239
|
+
"prompts.kind.label": "Prompt type",
|
|
4240
|
+
"prompts.kind.global": "Global prompt", "prompts.kind.quick": "Quick prompt",
|
|
4241
|
+
"prompts.kind.global.hint": "Applying writes it into AGENTS.md and it takes effect next turn; scenes can bind it.",
|
|
4242
|
+
"prompts.kind.quick.hint": "For use in the composer: inserts the prompt into the input box.",
|
|
4243
|
+
"prompts.group.global": "Global prompts", "prompts.group.quick": "Quick prompts", "prompts.bulk.quick": "Turn every visible quick prompt in this group on / off", "prompts.bulk.failed": "{done} changed, {failed} did not",
|
|
4244
|
+
"quickprompt.stat": " quick prompts",
|
|
4245
|
+
"quickprompt.toggle": "Show in the / menu",
|
|
4246
|
+
"quickprompt.toggle.hint": "Turned off, this one stops appearing in the chat box's / “Quick prompts” block — its text and note are kept, and you can turn it back on (it stays listed under “Tools › Prompts”, where the same switch works)",
|
|
4247
|
+
"quickprompt.empty": "No quick prompts yet. Click “New prompt” and pick the type “Quick prompt”.",
|
|
4248
|
+
"quickprompt.edit.title": "Edit quick prompt",
|
|
4249
|
+
"quickprompt.field.id.hint": "This is also the row's name in the / menu; non-ASCII is fine.",
|
|
4250
|
+
"quickprompt.field.desc.placeholder": "One line to remember what it's for (only for you)",
|
|
4251
|
+
"quickprompt.field.content": "Text",
|
|
4252
|
+
"quickprompt.field.content.placeholder": "This is pasted verbatim into the composer when you click it",
|
|
4253
|
+
"quickprompt.remove.title": "Delete quick prompt",
|
|
4254
|
+
"quickprompt.remove.hint": "Deleting moves it to the trash — restore it from the Trash button on this page.",
|
|
4255
|
+
"trash.group.quick": "Quick prompts",
|
|
4256
|
+
"trash.section.quick.sub": "Quick-prompt folders were moved: restore brings them back, delete-forever removes them",
|
|
4257
|
+
"slash.section.quick": "Quick prompts",
|
|
4258
|
+
"slash.quick.empty": "This quick prompt has no text — add it under Settings → Tools → Prompts",
|
|
4259
|
+
"compat.ov.slashCommands": "Slash command entry", "compat.ov.slashCommands.on": "Both on (type / for the “Tools” and “Quick prompts” blocks)",
|
|
4260
|
+
"compat.ov.slashCommands.onTools": "Only the “Tools” section is on", "compat.ov.slashCommands.onQuick": "Only the “Quick prompts” section is on",
|
|
4261
|
+
"compat.ov.slashCommands.off": "Both sections turned off (re-enable them on this page)",
|
|
4262
|
+
"compat.slash.title": "Slash command entry",
|
|
4263
|
+
"compat.slash.tools": "“Tools”", "compat.slash.tools.desc": "Enter scenes; toggle skills / MCP / subagents / prompts (both kinds) / memories",
|
|
4264
|
+
"compat.slash.quick": "“Quick prompts”", "compat.slash.quick.desc": "Reuse saved snippets, one click into the composer (their switches live under “Tools › Prompts”)",
|
|
4265
|
+
"compat.slash.restartHint": "Slash commands: restart DSH for the change to take effect",
|
|
4266
|
+
"compat.slash.live.on": "In effect", "compat.slash.live.off": "Not in effect",
|
|
4267
|
+
"compat.slash.mount.registered": "Mounted this startup", "compat.slash.mount.pending": "Not mounted yet (host service still starting)", "compat.slash.mount.unregistered": "Unmounted this startup",
|
|
4268
|
+
"compat.slash.mount.disabled": "Switched off by its own checkbox on this page",
|
|
4269
|
+
"compat.slash.mount.missing": "This host has no inputTriggers service, so the entry never appears",
|
|
4270
|
+
"compat.slash.mount.conflict": "Another plugin already owns the source name, so it was not mounted",
|
|
3984
4271
|
// Templates for degraded runtime notes (detailKey per src/compat/runtime-notes.ts; placeholders mirror the zh side one for one).
|
|
3985
4272
|
"compat.note.skillsProvider": "Skills provider setup failed ({reason}): skill enable/disable and the agent skill catalog will not take effect this round.",
|
|
3986
4273
|
"compat.note.contextInjection": "Injection channel setup failed ({reason}): no injection domain's content will reach the model this round.",
|
|
@@ -4051,7 +4338,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4051
4338
|
"compat.inject.force": "Also inject in Minimal mode", "compat.inject.force.desc": "Off by default (follow the preset)",
|
|
4052
4339
|
"compat.inject.domains": "What to inject",
|
|
4053
4340
|
"compat.inject.domain.scene": "Scene", "compat.inject.domain.memory": "Memory", "compat.inject.domain.mcp": "MCP",
|
|
4054
|
-
"compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "
|
|
4341
|
+
"compat.inject.domain.subagents": "Subagents", "compat.inject.domain.prompt": "Global prompts",
|
|
4055
4342
|
"compat.inject.domain.skills": "Skills",
|
|
4056
4343
|
"compat.inject.domain.carrier.title": "Turning this off also stops the copy the system sends by itself",
|
|
4057
4344
|
"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)",
|
|
@@ -4109,8 +4396,8 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4109
4396
|
"compat.tools.about.subagent_manager_run": "Run a persona as a subagent",
|
|
4110
4397
|
"compat.tools.about.subagent_manager_switch": "Enable/disable a persona",
|
|
4111
4398
|
"compat.tools.about.subagent_manager_save": "Create/update a persona",
|
|
4112
|
-
"compat.tools.about.prompt_manager_list": "List prompt presets",
|
|
4113
|
-
"compat.tools.about.prompt_manager_apply": "Apply a prompt preset",
|
|
4399
|
+
"compat.tools.about.prompt_manager_list": "List global prompt presets",
|
|
4400
|
+
"compat.tools.about.prompt_manager_apply": "Apply a global prompt preset",
|
|
4114
4401
|
"compat.tools.hover.memory_manager_list": "Lists each memory's id, scene, on/off state and one-line description, and says which scene injection follows. Only the memories that would actually be injected by default; all=true adds disabled, inactive-scene and shadowed entries. The scenes themselves (archives, bound presets, locks) are in scene_manager_list. Read-only.",
|
|
4115
4402
|
"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.",
|
|
4116
4403
|
"compat.tools.hover.memory_manager_switch": "Flips the index switch only — the .md file is untouched and it is reversible. A disabled memory stops being injected, which frees byte budget for the others; prefer this over deleting when you want to spend fewer tokens.",
|
|
@@ -4129,7 +4416,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4129
4416
|
"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.",
|
|
4130
4417
|
"compat.tools.hover.subagent_manager_switch": "A disabled persona leaves the catalog and cannot be delegated to; personas bound to a scene are enabled automatically when that scene is entered.",
|
|
4131
4418
|
"compat.tools.hover.subagent_manager_save": "Creates a persona, or updates the existing one — the receipt says which. You can set the name, description, body (the body becomes the subagent's system prompt) and output contract; omitted fields keep their current values, and a rename makes scene and archive bindings follow. Provider, model and tool limits stay in the panel (the model cannot tell whether it got them right, so it is not offered them). The description decides when the model picks it.",
|
|
4132
|
-
"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.",
|
|
4419
|
+
"compat.tools.hover.prompt_manager_list": "Lists the global prompt presets (the ones written into AGENTS.md) with their summaries and marks the one currently in effect — only one is active at a time. Use it to confirm the name before switching. Quick prompts are not this domain: the user picks those in the composer, they never enter the context, and there is nothing for the model to do with them.",
|
|
4133
4420
|
"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.",
|
|
4134
4421
|
"compat.live.title": "Live injection", "compat.live.hint": "What the model actually sees (read-only)",
|
|
4135
4422
|
"compat.live.never": "nothing delivered in this conversation yet", "compat.live.delivered": "{count} delivered in this conversation · last {time}",
|
|
@@ -4164,7 +4451,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4164
4451
|
"scenes.mode.active": "Active mode: {name}", "scenes.mode.profile": "Profile: {parts}", "scenes.mode.noProfile": "This scene has no profile yet",
|
|
4165
4452
|
"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.",
|
|
4166
4453
|
"scenes.search.placeholder": "Search scene names or descriptions", "scenes.empty.search": "No matching scenes",
|
|
4167
|
-
"scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.memories": "{count} memor(ies)",
|
|
4454
|
+
"scenes.profile.mcp": "{count} MCP", "scenes.profile.skills": "{count} skill(s)", "scenes.profile.subagents": "{count} subagent(s)", "scenes.profile.quickPrompts": "{count} quick prompt(s)", "scenes.profile.memories": "{count} memor(ies)",
|
|
4168
4455
|
"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.",
|
|
4169
4456
|
"scenes.seg.checked": "{checked}/{total} selected",
|
|
4170
4457
|
"bulk.selectAll": "Select all", "bulk.unselectAll": "Deselect all",
|
|
@@ -4189,7 +4476,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4189
4476
|
"memory.archive.tools": "MCP tools", "memory.archive.skills": "Skills", "memory.archive.subagents": "Subagent set", "memory.archive.memories": "Memory set", "memory.archive.applyFailed": "Could not apply at runtime: {reason}",
|
|
4190
4477
|
"memory.archive.addTools": "+ Tools", "memory.archive.addSkills": "+ Skills", "memory.archive.addSubagents": "+ Subagent set",
|
|
4191
4478
|
"memory.archive.removeSection": "Remove section", "memory.archive.save": "Save to scene",
|
|
4192
|
-
"memory.archive.
|
|
4479
|
+
"memory.archive.stale": "Skipped {items} (missing, shadowed by a same-name skill, or structurally broken — it could never take effect)",
|
|
4193
4480
|
"memory.result.archiveSaved": "Scene profile saved: {name}", "memory.result.modeSet": "Entered mode: {name}", "memory.result.modeSwitched": "{mcp} server(s) / {skills} source(s) switched", "memory.result.modeExited": "Exited mode",
|
|
4194
4481
|
"memory.desc": "Manage memories: enabled memories enter the system prompt; enable them per scene.",
|
|
4195
4482
|
"memory.btn.refresh": "Refresh", "memory.btn.new": "New memory", "memory.btn.create": "Create", "memory.btn.save": "Save",
|
|
@@ -4244,7 +4531,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4244
4531
|
"memory.table.name": "Memory name and description", "memory.table.tags": "Tags",
|
|
4245
4532
|
"error.rules.invalidArgs": "Missing arguments: pass all:true (enable everything) or scenes:[...] (explicit narrowing)", "error.rules.invalidGroup": "Invalid scene/group name (non-empty, ≤64 chars, no path separators or < > : \" | ? *, must not start with a dot)", "error.rules.invalidName": "Invalid memory name (non-empty, ≤64 chars, no / \\ < > : \" | ? *, must not start with a dot)", "error.rules.descriptionRequired": "Description is required", "error.rules.descriptionTooLong": "Description is too long (max 500 chars)", "error.rules.bodyRequired": "Body is required", "error.rules.tooLarge": "Rule content is too large", "error.rules.shadowed": "Rule is shadowed by a bundle of the same name", "error.rules.exists": "A memory with this name already exists", "error.rules.notFound": "Rule not found", "error.rules.budgetExceeded": "Scene memory section exceeds its budget", "error.rules.ioFailed": "File operation failed", "error.rules.outsideRoot": "Refused: the resolved path falls outside the memories root (a directory link may point outside)", "error.rules.nameTaken": "That name is already taken", "error.rules.sceneNotEmpty": "Scene is not empty; cannot be deleted", "error.rules.notBundle": "This memory is flat (a single file) and cannot carry attachments", "error.rules.noFiles": "No files selected", "error.rules.emptyFile": "Empty file", "error.rules.fileTooLarge": "File too large (max {limit} MB each)", "error.rules.tooManyFiles": "At most {limit} files per upload", "error.rules.sceneInMode": "This scene is the active mode; exit the mode before deleting it",
|
|
4246
4533
|
"subagents.title": "Subagents", "subagents.stat.total": " subagents", "subagents.stat.limited": " tool-restricted", "subagents.desc": "Manage subagents: create and import personas; a persona's body is the subagent's system prompt.",
|
|
4247
|
-
"prompts.desc": "Manage prompts:
|
|
4534
|
+
"prompts.desc": "Manage prompts: the global ones are written into AGENTS.md, the quick ones are only for the composer.",
|
|
4248
4535
|
"sessions.desc": "Manage archived sessions: restore or delete them permanently; expired ones are cleaned up automatically.",
|
|
4249
4536
|
"subagents.new": "New subagent", "subagents.empty": "No subagents yet — click “New subagent” to create the first one.", "subagents.empty.search": "No matching subagents", "subagents.search.placeholder": "Search persona names or descriptions",
|
|
4250
4537
|
"subagents.create": "New persona", "subagents.edit": "Edit persona",
|
|
@@ -4290,7 +4577,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4290
4577
|
"import.none": "Nothing was imported (all skipped)", "import.skipped": "Skipped: {items}",
|
|
4291
4578
|
"error.import.noFiles": "No files selected",
|
|
4292
4579
|
"nav.title": "Tools",
|
|
4293
|
-
"agm.stat.total": " prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.btn.reapply": "Apply again", "agm.btn.apply": "Apply", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.copyFrom": "Or copy from an existing preset", "agm.field.copyFrom.none": "Do not copy (empty template)", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); click “Apply again” to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Project directory (optional)", "hist.import.cwd.placeholder": "Absolute path, optional (the session goes to Ungrouped)", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.result.archived": "Archived {count} session(s) to History; manage them on the History page", "hist.result.archiveFailed": "; {count} could not be archived", "hist.btn.archiveSelected": "Archive selected", "hist.btn.archiveSelected.title": "Move the selected sessions into History so retention applies to them", "hist.export.busy": "Working…", "hist.btn.exportSelected": "Export {count} session(s)", "hist.error.title": "Operation failed", "hist.error.unknown": "Unknown error",
|
|
4580
|
+
"agm.stat.total": " global prompts", "agm.stat.active": " active", "agm.loading": "Loading…", "agm.empty": "No prompts yet — click “New prompt” to create one", "agm.empty.search": "No matching prompts", "agm.search.placeholder": "Search prompt ids or descriptions", "agm.btn.delete.blocked.refs": "Cannot delete: {refs}. Change those references first (rebind the prompt, leave the scene, or apply another preset).", "agm.ref.sep": "; ", "agm.ref.scene": "bound by scene “{scene}”", "agm.ref.sceneActive": "bound by scene “{scene}” (enabled)", "agm.ref.file": "this is what AGENTS.md currently holds", "agm.ref.restore": "the global prompt to restore after leaving the scene", "agm.sceneLock.notice": "Scene “{scene}” is driving the baseline: the prompt in effect is its bound “{id}”. Applying another preset here rebinds that scene to it (written into the scene profile as well).", "agm.active": "Active", "agm.active.hint.scene": "Currently active: the prompt preset bound to scene “{scene}”", "agm.active.hint.file": "Currently active: AGENTS.md is exactly this preset", "agm.file.applied": "In AGENTS.md", "agm.file.applied.hint": "AGENTS.md currently holds this one; the enabled scene binds a different preset, which now takes effect", "agm.apply.hint": "Writes AGENTS.md; the host re-reads that file every turn, so it takes effect on the next turn", "agm.apply.switch": "Apply prompt", "agm.apply.switch.on": "This one is in effect · click to write AGENTS.md again (undoing hand edits)", "agm.applyModal.title": "Apply preset", "agm.edit.title": "Edit preset", "agm.remove.title": "Delete preset", "agm.btn.edit": "Edit", "agm.btn.delete": "Delete", "agm.btn.new": "New prompt", "agm.btn.import": "Import prompts", "agm.result.imported": "Imported {count} preset(s): {names}", "agm.btn.save": "Save", "agm.btn.create": "Create", "agm.btn.confirmRemove": "Delete", "agm.btn.confirmApply": "Apply", "agm.field.id": "id (directory name)", "agm.field.id.hint": "Letters, spaces and dots are fine; no slashes or : * ? < >; up to 64 chars. Changing it renames the directory.", "agm.field.id.placeholder": "e.g. work-baseline", "agm.field.content": "Content", "agm.field.content.placeholder": "Write the preset body (Markdown) here; leave blank to start from the empty template", "agm.field.desc": "Description (the model sees this in the list)", "agm.field.desc.placeholder": "What this preset is for and when to use it", "agm.result.renamed": "Saved; id is now “{id}”", "agm.result.rebound": " ({count} scene binding(s) updated)", "agm.remove.suffix": "? This cannot be undone.", "agm.apply.prefix": "Write the contents of ", "agm.apply.suffix": " into ~/.dsh/AGENTS.md.", "agm.apply.note": "Takes effect on the next turn; the current content is backed up to __last-applied__ (last 5 kept)", "hist.err.load": "Load failed", "hist.err.unarchive": "Restore failed", "hist.err.delete": "Delete failed", "hist.err.retention": "Update failed", "hist.err.import": "Import failed", "hist.err.sessions": "Failed to load sessions", "hist.err.export": "Export failed", "hist.err.archive": "Archive failed", "hist.retention.forever": "Keep forever", "hist.retention.days": "{count} days", "hist.arch.all": "All sessions", "hist.arch.archived": "Archived only", "hist.arch.live": "Not archived only", "hist.arch.missing": "Missing directory only", "hist.ws.all": "All workspaces", "hist.ungrouped": "Ungrouped", "hist.group.removed": "Removed", "hist.group.removed.hint": "This directory has no DSH workspace registration right now; the group is rebuilt from session directories and rejoins automatically once registered again", "hist.group.dirMissing": "Directory missing", "hist.group.dirMissing.hint": "The workspace directory was deleted from disk, so it cannot be registered again; sessions in this group can still be restored (they join Ungrouped) or deleted forever", "hist.btn.registerWs": "Register", "hist.btn.registerWs.hint": "Register this directory again and attach its existing sessions back to that group (workspace accounting only — no file or session is moved or deleted)", "hist.register.title": "Register again?", "hist.register.body": "A workspace registration will be created in DSH for “{title}”, and the sessions under this directory will be attached back to it (restoring their grouping). This writes workspace accounting only — no file or session is moved or deleted.", "hist.register.confirm": "Register", "hist.register.done.title": "Registered", "hist.register.done.body": "Workspace “{title}” is registered; archived sessions under this directory join that group after a refresh.", "hist.register.done.attached": "Attached {count} existing session(s) under this directory back to the workspace.", "hist.register.done.skipped": "{count} session(s) could not be attached: {reason}", "hist.err.register": "Registration failed", "scenes.field.prompt": "Prompt preset", "scenes.agents.note.applied": "wrote “{id}” into AGENTS.md", "scenes.agents.note.restored": "restored the AGENTS.md baseline from before the scene", "scenes.agents.note.error": "AGENTS.md was not written: {reason}", "scenes.agents.note.baselineLost": "⚠ Leaving the scene will not restore the previous AGENTS.md: {reason}", "scenes.field.prompt.hint": "Sets the global prompt for this scene", "scenes.prompt.activeTag": "in effect", "scenes.prompt.noPresets": "No prompt presets yet — create one on the Prompts page.", "scenes.prompt.bound": "Prompt: {id}", "scenes.prompt.live": "Prompt active: {id}", "scenes.prompt.missing": "Preset missing: {id}", "scenes.prompt.tag.hint": "The prompt preset bound to this scene", "scenes.prompt.mismatch.hint": "The scene-bound “{id}” does not match the current contents of AGENTS.md (it may have been edited by hand); click “Apply again” for it on the Prompts page to write it back", "scenes.enable.blocked": "Scene “{name}” is already enabled; turn it off first", "scenes.result.created.collapsed": "Scene “{name}” created and left off (the legacy “all enabled” default collapsed to a single choice)", "scenes.legacyAll": "{count} scenes are enabled at once (a leftover of the legacy “all enabled” default); only one scene besides “Global” may be enabled.", "scenes.legacyAll.fix": "Collapse to one", "scenes.preview.title": "Before entering scene “{name}” — what this will change", "scenes.preview.loading": "Working out what changes…", "scenes.preview.failed": "Preview failed: {error}", "scenes.preview.enter": "Enter the scene", "scenes.preview.dismiss": "Don't show again", "scenes.preview.dismiss.title": "Close this card and stop showing it before entering scenes (bring it back with “Turn the reminder on” at the top of this page)", "scenes.preview.toggle.off": "Turn the reminder off", "scenes.preview.toggle.on": "Turn the reminder on", "scenes.preview.toggle.title": "Show a “what will change” card before entering a scene", "scenes.preview.nochange": "Nothing to change: the runtime already matches what this scene asks for.", "scenes.preview.noArchive": "This scene has no saved profile — MCP, skills and personas are all read as “none checked”, so entering disables every one of them.", "scenes.preview.exitFirst": "Leaves “{scene}” first: what it changed is restored from the before-entering snapshot.", "scenes.preview.exitNoSnapshot": "“{scene}” is active but its before-entering snapshot is gone: leaving only clears the mode flag, the runtime will not go back.", "scenes.preview.row.serversOn": "Enables {count} MCP server(s): {names}", "scenes.preview.row.serversOff": "Disables {count} MCP server(s): {names}", "scenes.preview.row.toolsOn": "Puts back {count} MCP tool(s): {names}", "scenes.preview.row.toolsOff": "Disables {count} MCP tool(s): {names}", "scenes.preview.row.sourcesOn": "Enables {count} skill director(ies): {names}", "scenes.preview.row.sourcesOff": "Disables {count} skill director(ies): {names}", "scenes.preview.row.skillsOn": "Enables {count} skill(s): {names}", "scenes.preview.row.skillsOff": "Disables {count} skill(s): {names}", "scenes.preview.row.personasOn": "Enables {count} persona(s): {names}", "scenes.preview.row.personasOff": "Disables {count} persona(s): {names}", "scenes.preview.row.quickOn": "Enables {count} quick prompt(s): {names}", "scenes.preview.row.quickOff": "Disables {count} quick prompt(s): {names}", "scenes.preview.enter.servers": "{count} MCP server(s) stay on: {names}", "scenes.preview.enter.memories": "{count} memory(ies) enabled: {names}", "scenes.preview.row.notes": "Overrides the notes of {count} server(s) with scene notes", "scenes.field.toolTable": "Model tool-table scheme", "scenes.field.toolTable.hint": "Choose the model tool-table scheme this scene may use", "scenes.field.quickPrompts": "Quick prompts", "scenes.field.quickPrompts.current": "The enabled ones (keep as is)", "scenes.field.quickPrompts.custom": "Pick per scene", "scenes.field.quickPrompts.none": "No quick prompts yet — add some on the Prompts page", "scenes.field.quickPrompts.hint": "Sets the quick prompts for this scene", "scenes.field.quickPrompts.list": "Enabled in this scene", "scenes.form.section.basic": "Basics", "scenes.form.section.binding": "Scene bindings", "scenes.field.toolTable.none": "Not bound (keep as is)", "scenes.field.toolTable.factory": "Factory default (hides {count})", "scenes.field.toolTable.count": "hides {count}", "scenes.field.toolTable.missing": "scheme no longer exists", "scenes.preview.row.toolPreset": "Tool table switches to scheme “{name}” (hides {count})", "scenes.preview.row.toolPresetMissing": "The tool-table scheme “{name}” bound here no longer exists — nothing switches this time", "scenes.preview.more": " etc.", "scenes.preview.truncated": "(each kind lists its first 60 entries only)", "scenes.preview.stale": "{count} profile entry(ies) no longer exist in this environment and will not take effect: {items}", "scenes.doctor.title": "{count} reference(s) point at a name that no longer exists", "scenes.doctor.noneButSkipped": "The check found no dangling references", "scenes.doctor.hint": "Left behind by a rename or a delete: it still looks bound here, but nothing matches that name at runtime. Point it at the right name, or untick it in the profile.", "scenes.doctor.row": "{where}: “{name}” no longer exists", "scenes.doctor.more": "{count} more not listed", "scenes.doctor.skipped": "({domains} could not be read, so that kind was not checked)", "agm.restore.tag": "Restored after the scene", "agm.restore.tag.hint": "The global prompt from before the scene; leaving the scene restores it, so deleting it would lose that fallback", "agm.file.mismatch": "Not in AGENTS.md", "agm.file.mismatch.hint": "AGENTS.md does not currently match this preset (it may have been edited by hand); flip the switch on the left to write it back", "hist.restore.note.registered": "Restored {count} session(s); workspace “{title}” was registered again and the sessions are back in that group.", "hist.restore.note.attached": "Restored {count} session(s) and attached them to workspace “{title}”.", "hist.restore.note.skip": "Workspace ownership could not be restored for {count} session(s): {reason}", "hist.purge.title": "Delete forever?", "hist.purge.one": "Delete “{title}” and all of its records forever — this cannot be undone", "hist.purge.batch": "Delete the {count} selected sessions and all of their records forever — this cannot be undone", "hist.btn.restore": "Restore", "hist.btn.purge": "Delete forever", "hist.btn.confirmPurge": "Delete", "hist.selectGroup": "Select group {title}", "hist.group.count": "{count}", "hist.selectAll": "Select all", "hist.deselectAll": "Deselect all", "hist.btn.import": "Import sessions", "hist.btn.export": "Export sessions", "hist.selected": "{count} selected", "hist.btn.restoreSelected": "Restore selected ({count})", "hist.btn.deleteSelected": "Delete selected ({count})", "hist.stat.archived": " archived", "hist.stat.projects": " projects", "hist.stat.retention": " days kept", "hist.search.placeholder": "Search title / session id / project path", "hist.loading": "Loading…", "hist.empty.search": "No matching archived sessions", "hist.empty.none": "No archived sessions yet", "hist.import.cwd": "Workspace (optional)", "hist.import.cwd.placeholder": "Pick, browse, or type an absolute path; leave blank to go to Ungrouped", "hist.import.workspace": "Workspace", "hist.import.browse": "Browse", "hist.import.workspace.none": "No registered workspace on this machine yet — type an absolute path, or use Browse to pick a folder.", "hist.import.drop": "Choose or drop a conversation file", "hist.import.done": "Created session {id} ({count} messages); continue it from the DSH session list.", "hist.import.lossy": "⚠ No speaker markers were found in the text, so it was folded into a single user message: the original turn structure may have been lost. If the source is a JSONL or Markdown transcript, import it with its original extension.", "hist.export.hint": "Export the selected sessions as transcript files that can be imported again", "hist.export.dir": "Export directory (absolute path, created automatically)", "hist.export.browse": "Browse and choose a folder", "hist.btn.browse": "Choose", "hist.export.pickDir": "Choose export directory", "hist.export.format": "Format", "hist.export.detail": "Export detail", "hist.export.detail.simple": "Text only", "hist.export.detail.tools": "Text + tools", "hist.export.detail.full": "Text + tools + thinking", "hist.export.scope": "Session scope", "hist.export.workspace": "Workspace", "hist.export.sessions": "Sessions ({count})", "hist.export.loading": "Loading sessions…", "hist.cwd.missing": "⚠ The workspace directory no longer exists: ", "hist.tag.archived": "Archived", "hist.tag.live": "Not archived", "hist.export.empty": "No matching sessions", "hist.result.exported": "Exported {count} session(s) to {dir}", "hist.result.skipped": "; skipped {count} ({ids})", "hist.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",
|
|
4294
4581
|
}
|
|
4295
4582
|
};
|
|
4296
4583
|
|
|
@@ -4632,6 +4919,17 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
4632
4919
|
}
|
|
4633
4920
|
probeTokenLock(0)
|
|
4634
4921
|
|
|
4922
|
+
// ── 斜杠命令「工具」段与「快捷提示词」段(实现与理由见 48-slash.js)───────────
|
|
4923
|
+
// 放在 slots 守卫**之前**:这两段与设置页挂载与否无关,宿主没给 slots 时对话页照样该有它。
|
|
4924
|
+
// 开关读一次就定本次启动的形态(改了开关要重开页面才生效 —— 源注册是启动期一次性的事,
|
|
4925
|
+
// 热挂热撤会让 `/` 菜单在用户眼皮底下变样,比"重启后生效"更难解释)。
|
|
4926
|
+
// 读失败按**开**处理:这两颗开关的默认值就是开,判不出来时不该把一个官方通道上的功能藏掉。
|
|
4927
|
+
apiCall('slash-settings', {}).then(function (res) {
|
|
4928
|
+
mountSlashCommands(ctx, t, !res || res.ok !== true ? {} : res.settings || {})
|
|
4929
|
+
}).catch(function () {
|
|
4930
|
+
mountSlashCommands(ctx, t, {})
|
|
4931
|
+
})
|
|
4932
|
+
|
|
4635
4933
|
const slots = ctx.get('slots')
|
|
4636
4934
|
if (slots === undefined) return
|
|
4637
4935
|
|
|
@@ -5180,9 +5478,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5180
5478
|
const renderGroup = (group, groupRows) => {
|
|
5181
5479
|
if (groupRows.length === 0) return null
|
|
5182
5480
|
const open = collapsed[group.key] !== true || !!normalizedQuery
|
|
5481
|
+
// 翻的是**收起标记本身**(与记忆 / 会话页的 `toggleCollapse` 同一写法)。这里原先写的是
|
|
5482
|
+
// `!open` —— 展开态下 `!open` 是 false,而 `false` 在这张表里的意思正是"没收起",
|
|
5483
|
+
// 于是每次点击都把同一个值写回去:**这张卡片从写下那天起就收不起来**(0.11.0 起,用户实测)。
|
|
5484
|
+
const flip = () => setCollapsed((prev) => Object.assign({}, prev, { [group.key]: prev[group.key] !== true }))
|
|
5183
5485
|
return React.createElement('section', { key: group.key, className: 'dsm-source' },
|
|
5184
5486
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
5185
|
-
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick:
|
|
5487
|
+
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: flip },
|
|
5186
5488
|
React.createElement('span', { className: 'dsm-source-title', title: group.title }, group.title),
|
|
5187
5489
|
React.createElement('span', { className: 'dsm-count' }, mt('mcp.servers.count', { count: groupRows.length })),
|
|
5188
5490
|
group.path ? React.createElement('span', { className: 'dsm-path', title: group.path }, group.path) : null)),
|
|
@@ -5439,18 +5741,46 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5439
5741
|
var xo = React.useState(null), exportState = xo[0], setExportState = xo[1]
|
|
5440
5742
|
var flipRef = React.useRef(null)
|
|
5441
5743
|
useFlipReorder(flipRef)
|
|
5442
|
-
|
|
5744
|
+
// ── 快捷提示词(用户存好的现成文字,只在对话框 `/` 菜单里由自己点出来)────
|
|
5745
|
+
// 独立一处状态,不塞进上面的 `state`:那边 `refresh()` 是整对象替换,两份数据混在
|
|
5746
|
+
// 一次替换里更容易漏;而快捷列表读失败不该把预设列表一起打成错误页。
|
|
5747
|
+
var qs = React.useState({ loading: true, error: null, prompts: [] })
|
|
5748
|
+
var quick = qs[0], setQuick = qs[1]
|
|
5749
|
+
var qk = React.useState(null), quickEdit = qk[0], setQuickEdit = qk[1]
|
|
5750
|
+
var qf = React.useState(null), quickConfirm = qf[0], setQuickConfirm = qf[1]
|
|
5751
|
+
function refreshQuick() {
|
|
5752
|
+
apiCall('quickprompt-list', {})
|
|
5753
|
+
.then(function (res) {
|
|
5754
|
+
if (res && res.ok) setQuick({ loading: false, error: null, prompts: res.prompts || [] })
|
|
5755
|
+
else setQuick({ loading: false, error: (res && res.error) || t('mcp.msg.loadFailed'), prompts: [] })
|
|
5756
|
+
})
|
|
5757
|
+
.catch(function (e) { setQuick({ loading: false, error: errMsg(e), prompts: [] }) })
|
|
5758
|
+
}
|
|
5759
|
+
/** 打开/刷新回收站(两个提示词域一起列:点一次「回收站」要看全)。 */
|
|
5443
5760
|
function loadTrash(keepOpen) {
|
|
5444
|
-
var
|
|
5445
|
-
setTrash(
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5761
|
+
var prev = keepOpen ? trash || {} : {}
|
|
5762
|
+
setTrash(Object.assign({ loading: true, error: null, presets: [], quick: [] }, prev))
|
|
5763
|
+
var read = function (op) {
|
|
5764
|
+
return apiCall(op, {}).catch(function (e) { return { ok: false, error: errMsg(e) } })
|
|
5765
|
+
}
|
|
5766
|
+
Promise.all([read('agentsmd-trash-list'), read('quickprompt-trash-list')]).then(function (both) {
|
|
5767
|
+
var errs = []
|
|
5768
|
+
if (!both[0] || both[0].ok === false) errs.push((both[0] && both[0].error) || t('trash.loadFailed'))
|
|
5769
|
+
if (!both[1] || both[1].ok === false) errs.push((both[1] && both[1].error) || t('trash.loadFailed'))
|
|
5770
|
+
setTrash({
|
|
5771
|
+
loading: false, error: errs.length ? errs.join(t('agm.ref.sep')) : null,
|
|
5772
|
+
presets: ((both[0] && both[0].trash) || []).map(function (e) { return Object.assign({ kind: 'presets' }, e) }),
|
|
5773
|
+
quick: ((both[1] && both[1].trash) || []).map(function (e) { return Object.assign({ kind: 'quick' }, e) }),
|
|
5774
|
+
})
|
|
5775
|
+
})
|
|
5776
|
+
}
|
|
5777
|
+
/** 回收站两个域共用一套按钮:条目自带 `kind`,op 名按它挑(不再各写一份恢复/永久删除)。 */
|
|
5778
|
+
function trashOp(item, action) {
|
|
5779
|
+
return item.kind === 'quick' ? 'quickprompt-trash-' + action : 'agentsmd-trash-' + action
|
|
5450
5780
|
}
|
|
5451
5781
|
function restorePreset(item) {
|
|
5452
5782
|
setTrashBusy(true)
|
|
5453
|
-
apiCall('
|
|
5783
|
+
apiCall(trashOp(item, 'restore'), { id: item.id }).then(function (res) {
|
|
5454
5784
|
setTrashBusy(false)
|
|
5455
5785
|
if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: { kind: 'ok', text: t('trash.restored', { name: item.name }) } }) }); refresh(); loadTrash(true) }
|
|
5456
5786
|
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
@@ -5458,7 +5788,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5458
5788
|
}
|
|
5459
5789
|
function purgePreset(item) {
|
|
5460
5790
|
setTrashBusy(true)
|
|
5461
|
-
apiCall('
|
|
5791
|
+
apiCall(trashOp(item, 'delete'), { id: item.id }).then(function (res) {
|
|
5462
5792
|
setTrashBusy(false)
|
|
5463
5793
|
if (res && res.ok) { setState(function (s) { return Object.assign({}, s, { notice: { kind: 'ok', text: t('trash.purged', { name: item.name }) } }) }); loadTrash(true) }
|
|
5464
5794
|
else setTrash(function (cur) { return Object.assign({}, cur, { error: (res && res.error) || t('mcp.msg.failed') }) })
|
|
@@ -5466,9 +5796,33 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5466
5796
|
}
|
|
5467
5797
|
// 导入:与技能页同一个 ImportModal(拖拽区 + 文件要求清单 + 结果反馈)。
|
|
5468
5798
|
// 一次可选多份 .md,逐个读取正文导入;同名/失败逐条回报,不因一个失败中断其余的。
|
|
5799
|
+
// 两个提示词域共用这一颗按钮,类型在弹窗顶部选(`extra` 槽)—— 选完导入的是哪一档
|
|
5800
|
+
// 就写进哪一档,不靠"看当前在哪个分组"猜。
|
|
5469
5801
|
var impOpenState = React.useState(false); var importOpen = impOpenState[0], setImportOpen = impOpenState[1]
|
|
5470
5802
|
var impBusyState = React.useState(false); var importBusy = impBusyState[0], setImportBusy = impBusyState[1]
|
|
5471
5803
|
var impResultState = React.useState(null); var importResult = impResultState[0], setImportResult = impResultState[1]
|
|
5804
|
+
var impKindState = React.useState('global'); var importKind = impKindState[0], setImportKind = impKindState[1]
|
|
5805
|
+
/**
|
|
5806
|
+
* 「全局提示词 / 快捷提示词」二选一的勾选行(与兼容页同款打勾框)。
|
|
5807
|
+
*
|
|
5808
|
+
* 为什么放在弹窗里而不是页头:这两类东西的**读法完全不同**(一份会覆盖写全局
|
|
5809
|
+
* AGENTS.md、一份只是贴进对话框的文字),把它们混进同一个列表再打标签,用户要先
|
|
5810
|
+
* 看懂标签才知道点下去会发生什么。新建与导入都先问一次,问在最贴近动作的地方。
|
|
5811
|
+
*
|
|
5812
|
+
* 两颗框恒有一棵勾着(点已勾着的那颗不改状态)—— 没有"都不选"这一态,所以是
|
|
5813
|
+
* 单选的语义、打勾框的样子。
|
|
5814
|
+
*/
|
|
5815
|
+
function kindPicker(value, onChange) {
|
|
5816
|
+
const pick = function (kind) {
|
|
5817
|
+
return React.createElement('label', { className: 'dsm-kind-pick' },
|
|
5818
|
+
React.createElement('input', { type: 'checkbox', checked: value === kind, onChange: function () { onChange(kind) } }),
|
|
5819
|
+
t('prompts.kind.' + kind))
|
|
5820
|
+
}
|
|
5821
|
+
return React.createElement('div', { className: 'dsm-field' },
|
|
5822
|
+
React.createElement('span', { className: 'dsm-label' }, t('prompts.kind.label')),
|
|
5823
|
+
React.createElement('div', { className: 'dsm-kind-picks' }, pick('global'), pick('quick')),
|
|
5824
|
+
React.createElement('p', { className: 'dsm-help' }, t(value === 'quick' ? 'prompts.kind.quick.hint' : 'prompts.kind.global.hint')))
|
|
5825
|
+
}
|
|
5472
5826
|
function importIdOf(name) {
|
|
5473
5827
|
return String(name || '').replace(/\.(md|markdown|txt)$/i, '').trim().replace(/[\\/<>:"|?*]+/g, '-') || 'imported'
|
|
5474
5828
|
}
|
|
@@ -5477,12 +5831,13 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5477
5831
|
if (!list.length) return
|
|
5478
5832
|
setImportBusy(true); setImportResult(null)
|
|
5479
5833
|
var done = [], failed = []
|
|
5834
|
+
var quickIn = importKind === 'quick'
|
|
5480
5835
|
var chain = Promise.resolve()
|
|
5481
5836
|
list.forEach(function (file) {
|
|
5482
5837
|
chain = chain.then(function () {
|
|
5483
5838
|
return file.text().then(function (text) {
|
|
5484
5839
|
var id = importIdOf(file.name)
|
|
5485
|
-
return apiCall('agentsmd-import', { id: id, content: String(text) }).then(function (res) {
|
|
5840
|
+
return apiCall(quickIn ? 'quickprompt-create' : 'agentsmd-import', { id: id, content: String(text) }).then(function (res) {
|
|
5486
5841
|
if (res && res.ok) done.push(res.id || id)
|
|
5487
5842
|
else failed.push(String(file.name) + '(' + ((res && res.error) || '') + ')')
|
|
5488
5843
|
})
|
|
@@ -5504,6 +5859,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5504
5859
|
// 手动刷新(含首次加载)要把按钮切到「刷新中…」——点完毫无反馈等于没点。
|
|
5505
5860
|
// 页面内部那些「操作完顺手重读」的调用传 silent,免得按钮每次都闪一下。
|
|
5506
5861
|
if (!silent) setState(function (s) { return Object.assign({}, s, { loading: true }) })
|
|
5862
|
+
refreshQuick()
|
|
5507
5863
|
apiCall('agentsmd-list', {}).then(function (res) {
|
|
5508
5864
|
// 刷新即清掉上一次的「拒绝」说明(它解释的是那一次点击,不是常驻状态)。
|
|
5509
5865
|
setState(function (s) { return { loading: false, error: res && res.ok ? null : ((res && res.error) || mt('mcp.msg.loadFailed')), presets: (res && res.presets) || [], current: s.current, scenePrompt: (res && res.scenePrompt) || null, anyLocked: (res && res.anyLocked) === true, deny: res && res.ok ? null : s.deny } })
|
|
@@ -5595,14 +5951,25 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5595
5951
|
else refresh()
|
|
5596
5952
|
}).catch(function (e) { setBusy(null); setApplyConfirm(null); actionFailed(e) })
|
|
5597
5953
|
}
|
|
5598
|
-
function doCreate(id,
|
|
5954
|
+
function doCreate(id, content, description) {
|
|
5599
5955
|
beginAction()
|
|
5600
5956
|
setBusy('create')
|
|
5601
|
-
|
|
5957
|
+
if (createModal && createModal.kind === 'quick') {
|
|
5958
|
+
// 快捷提示词:正文原样存。没有"空模板"也没有"从现有复制"——那两个概念都属于
|
|
5959
|
+
// 会覆盖全局 AGENTS.md 的基线预设,套到快捷词上只会让人以为贴进对话框的文字会被注入。
|
|
5960
|
+
apiCall('quickprompt-create', {
|
|
5961
|
+
id: id, content: String(content || ''), description: String(description == null ? '' : description),
|
|
5962
|
+
}).then(function (res) {
|
|
5963
|
+
setBusy(null)
|
|
5964
|
+
if (res && res.ok) { setCreateModal(null); refreshQuick() }
|
|
5965
|
+
else if (res) setCreateModal(Object.assign({}, createModal, { error: res.error }))
|
|
5966
|
+
}).catch(function () { setBusy(null) })
|
|
5967
|
+
return
|
|
5968
|
+
}
|
|
5969
|
+
// 留空 = 宿主那一份空白模板(`agentsmd-create` 自己的默认)。
|
|
5602
5970
|
apiCall('agentsmd-create', {
|
|
5603
5971
|
id: id,
|
|
5604
5972
|
...(content ? { content: content } : {}),
|
|
5605
|
-
...(from ? { from: from } : {}),
|
|
5606
5973
|
// 描述存 meta.json,**不写进 AGENTS.md**(那个文件的正文会被原样注入)。
|
|
5607
5974
|
// 0.14.0 起模型也在 `prompt_manager_list` 的清单里看得到它,所以有了字数上限
|
|
5608
5975
|
// (输入框那枚 maxLength / 计数就是它)。
|
|
@@ -5625,16 +5992,53 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5625
5992
|
} else if (res) setEditModal(Object.assign({}, editModal, { error: res.error }))
|
|
5626
5993
|
}).catch(function () { setBusy(null) })
|
|
5627
5994
|
}
|
|
5628
|
-
/**
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5995
|
+
/**
|
|
5996
|
+
* 快捷提示词的编辑与删除。
|
|
5997
|
+
*
|
|
5998
|
+
* 打开编辑**不再发一次读请求**:`quickprompt-list` 连正文一起回(服务端那份清单的
|
|
5999
|
+
* 理由见 `src/prompts/quick-service.ts` 头注 —— `/` 菜单点的时候要当场有字可插),
|
|
6000
|
+
* 预设那边要单独 `agentsmd-read` 是因为它的 list 只有元信息。
|
|
6001
|
+
* 改名也不牵连别处:快捷提示词不被场景绑定、不被引用,改完就是改完。
|
|
6002
|
+
*/
|
|
6003
|
+
function openQuickEdit(p) {
|
|
6004
|
+
beginAction()
|
|
6005
|
+
setQuickEdit({ id: p.id, nextId: p.id, content: p.content || '', description: p.description || '' })
|
|
6006
|
+
}
|
|
6007
|
+
function doQuickUpdate() {
|
|
6008
|
+
beginAction()
|
|
6009
|
+
setBusy('quick-update')
|
|
6010
|
+
apiCall('quickprompt-update', {
|
|
6011
|
+
id: quickEdit.id, nextId: String(quickEdit.nextId || quickEdit.id),
|
|
6012
|
+
content: quickEdit.content, description: String(quickEdit.description == null ? '' : quickEdit.description),
|
|
6013
|
+
}).then(function (res) {
|
|
6014
|
+
setBusy(null)
|
|
6015
|
+
if (res && res.ok) { setQuickEdit(null); refreshQuick() }
|
|
6016
|
+
else if (res) setQuickEdit(Object.assign({}, quickEdit, { error: res.error }))
|
|
6017
|
+
}).catch(function () { setBusy(null) })
|
|
6018
|
+
}
|
|
6019
|
+
function doQuickRemove(id) {
|
|
6020
|
+
beginAction()
|
|
6021
|
+
setBusy('quick-remove')
|
|
6022
|
+
apiCall('quickprompt-remove', { id: id }).then(function (res) {
|
|
6023
|
+
setBusy(null); setQuickConfirm(null)
|
|
6024
|
+
if (res && res.ok) refreshQuick()
|
|
6025
|
+
else if (res) actionFailed(res)
|
|
6026
|
+
}).catch(function (e) { setBusy(null); setQuickConfirm(null); actionFailed(e) })
|
|
6027
|
+
}
|
|
6028
|
+
/**
|
|
6029
|
+
* 那颗开关:只改"这条在不在对话框 `/` 菜单里出现",正文与备注一个字不动。
|
|
6030
|
+
* 与子智能体那一档同一条口径:成功不弹提示(列表重读后开关自己翻过来),失败要说人话。
|
|
6031
|
+
*/
|
|
6032
|
+
function toggleQuick(p) {
|
|
6033
|
+
beginAction()
|
|
6034
|
+
const next = p.enabled === false
|
|
6035
|
+
setBusy('quick-toggle-' + p.id)
|
|
6036
|
+
apiCall('quickprompt-toggle', { id: p.id, enabled: next }).then(function (res) {
|
|
6037
|
+
setBusy(null)
|
|
6038
|
+
if (res && res.ok) refreshQuick()
|
|
6039
|
+
else if (res) actionFailed(res)
|
|
6040
|
+
}).catch(function (e) { setBusy(null); actionFailed(e) })
|
|
5636
6041
|
}
|
|
5637
|
-
|
|
5638
6042
|
// 只有「还没有任何内容」时才让加载占位顶掉列表:手动点刷新时列表不该凭空消失,
|
|
5639
6043
|
// 反馈由刷新按钮自己的「刷新中…」承担(与记忆页 / 技能页同一口径)。
|
|
5640
6044
|
// 过滤只跟数据与(防抖后的)搜索词有关:每次渲染重扫一整份列表没有必要。
|
|
@@ -5649,6 +6053,102 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5649
6053
|
var activeCount = React.useMemo(function () {
|
|
5650
6054
|
return state.presets.filter(function (p) { return p.active }).length
|
|
5651
6055
|
}, [state.presets])
|
|
6056
|
+
var visibleQuick = React.useMemo(function () {
|
|
6057
|
+
return quick.prompts.filter(function (p) { return matchPresetQuery(p, dq) })
|
|
6058
|
+
}, [quick.prompts, dq])
|
|
6059
|
+
// 与上面预设那一档同一条口径:亮着的排前面。关掉的照样列着(那只是"不在 `/` 菜单里出现",
|
|
6060
|
+
// 不是删掉),沉到底部是为了让常用的那几条留在第一眼。
|
|
6061
|
+
var orderedQuick = React.useMemo(function () {
|
|
6062
|
+
return enabledFirst(visibleQuick, function (p) { return p.enabled !== false })
|
|
6063
|
+
}, [visibleQuick])
|
|
6064
|
+
// ── 两组都是**可折叠卡片**(用户 2026-10-01 裁定:做成技能那样)──────────
|
|
6065
|
+
// 默认展开;打了字强制展开 —— 卡片收着的话搜索结果一行都看不见(技能页 / MCP 页同口径)。
|
|
6066
|
+
var cg = React.useState({})
|
|
6067
|
+
var groupsCollapsed = cg[0], setGroupsCollapsed = cg[1]
|
|
6068
|
+
function toggleGroup(key) {
|
|
6069
|
+
// 翻的是**收起标记本身**:`groupsCollapsed[key] === true` 才是收着的。
|
|
6070
|
+
// 写成 `!(groupsCollapsed[key] !== true)` 的话,展开态下算出来是 `false`,
|
|
6071
|
+
// 而 `false` 在这张表里的意思正是"没收起" —— 每次点击把同一个值写回去,卡片收不起来。
|
|
6072
|
+
setGroupsCollapsed(function (prev) { return Object.assign({}, prev, { [key]: prev[key] !== true }) })
|
|
6073
|
+
}
|
|
6074
|
+
/** 一张分组卡片:标题 + 条数(点标题行收起/展开)+ 右侧动作 + 展开时的卡片体。 */
|
|
6075
|
+
function groupCard(key, label, count, actions, bodyNode) {
|
|
6076
|
+
var open = groupsCollapsed[key] !== true || !!dq
|
|
6077
|
+
return React.createElement('section', { key: key, className: 'dsm-source' },
|
|
6078
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
6079
|
+
React.createElement('button', { type: 'button', className: 'dsm-source-head-main', 'aria-expanded': open, onClick: function () { toggleGroup(key) } },
|
|
6080
|
+
React.createElement('span', { className: 'dsm-source-title', title: label }, label),
|
|
6081
|
+
React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: count }))),
|
|
6082
|
+
actions ? React.createElement('div', { className: 'dsm-source-actions' }, actions) : null),
|
|
6083
|
+
open ? React.createElement('div', { className: 'dsm-source-body' }, bodyNode) : null)
|
|
6084
|
+
}
|
|
6085
|
+
/**
|
|
6086
|
+
* 快捷那组的「全选 / 取消全选」:只作用于**当前可见**的那几条(与技能页那颗同一条口径),
|
|
6087
|
+
* 目标集 = 状态与"这一组现在是不是全开"相反的行。点完顺带把卡片展开 —— 收着的话
|
|
6088
|
+
* 到底改了哪几条看不见。
|
|
6089
|
+
*
|
|
6090
|
+
* 全局那一组**不给**这颗按钮(用户 2026-10-01 明确裁定):那一档是单选,
|
|
6091
|
+
* "把所有基线都应用"根本没有意义 —— 应用一份就是关掉其余每一份。
|
|
6092
|
+
*/
|
|
6093
|
+
function toggleQuickBulk() {
|
|
6094
|
+
var list = visibleQuick
|
|
6095
|
+
if (!list.length) return
|
|
6096
|
+
var turnOn = !list.every(function (p) { return p.enabled !== false })
|
|
6097
|
+
var targets = list.filter(function (p) { return (p.enabled !== false) !== turnOn })
|
|
6098
|
+
if (!targets.length) return
|
|
6099
|
+
beginAction()
|
|
6100
|
+
setGroupsCollapsed(Object.assign({}, groupsCollapsed, { quick: false }))
|
|
6101
|
+
setBusy('quick-bulk')
|
|
6102
|
+
// 逐条走同一个 `quickprompt-toggle`(不新增批量 op):每条都要能被场景档案同步跟上
|
|
6103
|
+
// (登记表里那颗 syncsArchive),自己拼一条批量通道就会把这件事漏掉。
|
|
6104
|
+
Promise.all(targets.map(function (p) {
|
|
6105
|
+
return apiCall('quickprompt-toggle', { id: p.id, enabled: turnOn })
|
|
6106
|
+
.catch(function (e) { return { ok: false, error: errMsg(e) } })
|
|
6107
|
+
})).then(function (all) {
|
|
6108
|
+
setBusy(null)
|
|
6109
|
+
refreshQuick()
|
|
6110
|
+
var bad = all.filter(function (r) { return !r || r.ok !== true }).length
|
|
6111
|
+
if (bad) setState(function (s) { return Object.assign({}, s, { notice: { kind: 'err', text: t('prompts.bulk.failed', { done: all.length - bad, failed: bad }) } }) })
|
|
6112
|
+
})
|
|
6113
|
+
}
|
|
6114
|
+
var quickAllOn = visibleQuick.length > 0 && visibleQuick.every(function (p) { return p.enabled !== false })
|
|
6115
|
+
var quickBulkButton = React.createElement('button', {
|
|
6116
|
+
type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-bulk',
|
|
6117
|
+
disabled: busy !== null || state.anyLocked === true || !visibleQuick.length,
|
|
6118
|
+
title: t('prompts.bulk.quick'),
|
|
6119
|
+
onClick: toggleQuickBulk,
|
|
6120
|
+
}, bulkPair(t(quickAllOn ? 'bulk.unselectAll' : 'bulk.selectAll'), t(quickAllOn ? 'bulk.selectAll' : 'bulk.unselectAll')))
|
|
6121
|
+
function renderQuick(p) {
|
|
6122
|
+
return React.createElement('div', { key: p.id, className: 'dsm-source', 'data-flip-key': 'q:' + p.id },
|
|
6123
|
+
React.createElement('div', { className: 'dsm-source-head' },
|
|
6124
|
+
// 与子智能体行同一形状:名字一行、备注一行,都在头部里 —— 备注不再单独占一条
|
|
6125
|
+
// 带分隔线的卡片底(用户 2026-09-30 要求少占空间)。
|
|
6126
|
+
// 不挂「生效中」那类标签:快捷提示词不写 AGENTS.md、不进上下文,它唯一的两种状态
|
|
6127
|
+
// (在不在 `/` 菜单里出现)就是左边那颗开关。
|
|
6128
|
+
React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
|
|
6129
|
+
React.createElement('div', { className: 'dsm-persona-name-row' },
|
|
6130
|
+
React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id)),
|
|
6131
|
+
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
6132
|
+
React.createElement('div', { className: 'dsm-source-actions' },
|
|
6133
|
+
React.createElement(Switch, {
|
|
6134
|
+
on: p.enabled !== false,
|
|
6135
|
+
disabled: busy !== null || state.anyLocked === true,
|
|
6136
|
+
label: t('quickprompt.toggle') + ' ' + p.id,
|
|
6137
|
+
title: t('quickprompt.toggle.hint'),
|
|
6138
|
+
onClick: function () { toggleQuick(p) } }),
|
|
6139
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openQuickEdit(p) } }, t('agm.btn.edit')),
|
|
6140
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: state.anyLocked === true ? t('scenes.lock.blockedEdit') : '', onClick: function () { setQuickConfirm({ id: p.id }) } }, t('agm.btn.delete')))))
|
|
6141
|
+
}
|
|
6142
|
+
// 快捷提示词那一组。整段独立于上面的 `body`:它读失败只红自己那一组,
|
|
6143
|
+
// 不该把预设列表一起打成错误页(两件事各自有各自的结论)。
|
|
6144
|
+
var quickSection = groupCard('quick', t('prompts.group.quick'), quick.prompts.length, quickBulkButton,
|
|
6145
|
+
quick.error
|
|
6146
|
+
? React.createElement(Notice, { kind: 'err', text: quick.error })
|
|
6147
|
+
: !quick.prompts.length
|
|
6148
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('quickprompt.empty'))
|
|
6149
|
+
: !visibleQuick.length
|
|
6150
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('agm.empty.search'))
|
|
6151
|
+
: React.createElement('div', { className: 'dsm-sources' }, orderedQuick.map(renderQuick)))
|
|
5652
6152
|
var body = state.loading && !state.presets.length
|
|
5653
6153
|
? React.createElement('div', { className: 'dsm-empty' }, t('agm.loading'))
|
|
5654
6154
|
: state.error
|
|
@@ -5665,7 +6165,7 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5665
6165
|
var restoreRef = refs.some(function (r) { return r.kind === 'restore' })
|
|
5666
6166
|
var refTexts = refsOf(p)
|
|
5667
6167
|
// 场景绑的就是它,但 AGENTS.md 里的内容不是它(被手改过?)→ 说清楚,
|
|
5668
|
-
//
|
|
6168
|
+
// 并留"再点一次那颗开关"这条路写回。
|
|
5669
6169
|
var fileMismatch = !!sceneDriver && sceneDriver.presetId === p.id && !p.fileApplied
|
|
5670
6170
|
// 场景接管的是**别的**预设时,应用这一份 = 把该场景改绑到它(用户裁定
|
|
5671
6171
|
// 2026-09-17:未锁定时切换要可用,并且同步写进场景档案),所以不再拦截;
|
|
@@ -5673,23 +6173,38 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5673
6173
|
var applyRebinds = !!sceneDriver && sceneDriver.presetId !== p.id
|
|
5674
6174
|
return React.createElement('div', { key: p.id, className: 'dsm-source', 'data-flip-key': p.id, 'data-flip-on': p.active ? '1' : '0' },
|
|
5675
6175
|
React.createElement('div', { className: 'dsm-source-head' },
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
6176
|
+
// 与子智能体行同一形状:名字(带标签)一行、备注一行,都在头部里 ——
|
|
6177
|
+
// 备注不再单独占一条带分隔线的卡片底(用户 2026-09-30 要求少占空间)。
|
|
6178
|
+
React.createElement('div', { className: 'dsm-source-head-main dsm-persona-main' },
|
|
6179
|
+
React.createElement('div', { className: 'dsm-persona-name-row' },
|
|
6180
|
+
React.createElement('span', { className: 'dsm-source-title', title: p.id }, p.id),
|
|
6181
|
+
p.active ? React.createElement('span', { className: 'dsm-tag dsm-tag-on', title: viaScene ? t('agm.active.hint.scene', { scene: (state.scenePrompt && state.scenePrompt.label) || (state.scenePrompt && state.scenePrompt.scene) || '' }) : t('agm.active.hint.file') }, t('agm.active')) : null,
|
|
6182
|
+
!p.active && p.fileApplied ? React.createElement('span', { className: 'dsm-tag', title: t('agm.file.applied.hint') }, t('agm.file.applied')) : null,
|
|
6183
|
+
fileMismatch ? React.createElement('span', { className: 'dsm-tag dsm-tag-off', title: t('agm.file.mismatch.hint') }, t('agm.file.mismatch')) : null,
|
|
6184
|
+
// 场景绑定**不在这里标标签**(用户裁定):多场景绑同一份时标签会把行挤爆;
|
|
6185
|
+
// 谁在引用它,点「删除」时会逐条说出来(`agm.btn.delete.blocked.refs`)。
|
|
6186
|
+
restoreRef ? React.createElement('span', { className: 'dsm-tag', title: t('agm.restore.tag.hint') }, t('agm.restore.tag')) : null),
|
|
6187
|
+
p.description ? React.createElement('div', { className: 'dsm-persona-desc', title: p.description }, p.description) : null),
|
|
5684
6188
|
React.createElement('div', { className: 'dsm-source-actions' },
|
|
6189
|
+
// 「应用 / 重新应用」换成与子智能体同一颗药丸开关(用户 2026-09-30 裁定):
|
|
6190
|
+
// 那一档是单选,屏幕上永远只有一份生效中 —— 开关说的正是这件事,而两颗文字
|
|
6191
|
+
// 按钮把"当前是哪份"留给标签去猜。关不掉是**语义**不是缺功能:基线没有
|
|
6192
|
+
// "全部不应用"这一态,所以点已亮着的那颗 = 重新写回(把被手改的 AGENTS.md
|
|
6193
|
+
// 拉回来),与旧「重新应用」同一个出口,确认弹窗也照旧。
|
|
6194
|
+
React.createElement(Switch, {
|
|
6195
|
+
on: p.active === true,
|
|
6196
|
+
disabled: busy !== null || state.anyLocked === true,
|
|
6197
|
+
label: t('agm.apply.switch') + ' ' + p.id,
|
|
6198
|
+
title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel })
|
|
6199
|
+
: p.active ? t('agm.apply.switch.on') : t('agm.apply.hint'),
|
|
6200
|
+
onClick: function () { setApplyConfirm({ id: p.id }) } }),
|
|
5685
6201
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: state.anyLocked === true, onClick: function () { openEdit(p) } }, t('agm.btn.edit')),
|
|
5686
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null || state.anyLocked === true, onClick: function () { setApplyConfirm({ id: p.id }) }, title: applyRebinds ? t('agm.apply.rebindScene', { scene: sceneDriverLabel }) : t('agm.apply.hint') }, p.fileApplied ? t('agm.btn.reapply') : t('agm.btn.apply')),
|
|
5687
6202
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: busy !== null || state.anyLocked === true, title: refTexts.length > 0 ? t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) : t('scenes.lock.blockedEdit'), onClick: function () {
|
|
5688
6203
|
// 被引用的预设**点得动但删不掉**:当面说出「谁在用」,而不是让用户对着
|
|
5689
6204
|
// 禁用按钮猜(disabled 的按钮连 title 都不弹)。宿主侧同样拒绝,这里只是先说。
|
|
5690
6205
|
if (refTexts.length > 0) { setState(function (s) { return Object.assign({}, s, { deny: t('agm.btn.delete.blocked.refs', { refs: refTexts.join(t('agm.ref.sep')) }) }) }); return }
|
|
5691
6206
|
setApplyConfirm({ id: p.id, remove: true })
|
|
5692
|
-
} }, t('agm.btn.delete'))))
|
|
6207
|
+
} }, t('agm.btn.delete')))))
|
|
5693
6208
|
}))
|
|
5694
6209
|
|
|
5695
6210
|
return React.createElement('section', { className: 'dsm-section' },
|
|
@@ -5700,9 +6215,9 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5700
6215
|
React.createElement('p', { className: 'dsm-desc' }, t('prompts.desc'))),
|
|
5701
6216
|
React.createElement('div', { className: 'dsm-actions' },
|
|
5702
6217
|
refreshButton(t, state.loading, { className: 'dsm-btn dsm-btn-secondary', disabled: state.loading, onClick: function () { refresh() } }),
|
|
5703
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({
|
|
6218
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: state.anyLocked === true, onClick: function () { setCreateModal({ kind: 'global', id: '', content: '', description: '', error: null }) } }, t('agm.btn.new')),
|
|
5704
6219
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || state.anyLocked === true, onClick: function () { setImportResult(null); setImportOpen(true) } }, t('agm.btn.import')),
|
|
5705
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !state.presets.length, onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
|
|
6220
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: busy !== null || !(state.presets.length || quick.prompts.length), onClick: function () { setExportState({ busy: false, result: null }) } }, t('export.presets')),
|
|
5706
6221
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', onClick: function () { loadTrash(false) } }, t('trash.btn.open')))),
|
|
5707
6222
|
state.anyLocked === true ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('lock.banner')) : null,
|
|
5708
6223
|
// 场景接管时把「为什么应用按钮点不动」直接写出来,而不是让用户对着置灰的按钮猜。
|
|
@@ -5713,8 +6228,11 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5713
6228
|
(state.anyLocked !== true && sceneDriver) ? React.createElement('div', { className: 'dsm-feedback dsm-warning', role: 'status' }, t('agm.sceneLock.notice', { scene: sceneDriverLabel, id: sceneDriver.presetId })) : null,
|
|
5714
6229
|
// 一次性拒绝说明(被引用的预设删不掉):不替换列表,只加一条横幅。
|
|
5715
6230
|
state.deny ? React.createElement(Notice, { kind: 'warn', text: state.deny }) : null,
|
|
5716
|
-
|
|
5717
|
-
|
|
6231
|
+
// 三枚指标:两类各自计数 + 生效中。第一枚的文案点明「全局」—— 页面上现在有两类
|
|
6232
|
+
// 提示词,只说「个提示词」会让用户以为快捷那几条没被算进来。
|
|
6233
|
+
React.createElement('div', { className: 'dsm-summary', style: { '--dsm-stat-cols': '3' } },
|
|
6234
|
+
[['total', state.presets.length, t('agm.stat.total')], ['active', activeCount, t('agm.stat.active')],
|
|
6235
|
+
['quick', quick.prompts.length, t('quickprompt.stat')]].map(function (item) {
|
|
5718
6236
|
return React.createElement('div', { key: item[0], className: 'dsm-stat' },
|
|
5719
6237
|
React.createElement('strong', null, item[1]), item[2])
|
|
5720
6238
|
})),
|
|
@@ -5726,28 +6244,50 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5726
6244
|
placeholder: t('agm.search.placeholder'),
|
|
5727
6245
|
onChange: function (e) { setQuery(e.target.value) },
|
|
5728
6246
|
})),
|
|
5729
|
-
|
|
6247
|
+
// 两段各一张可折叠卡片:卡片头是组名 + 条数(点它收起/展开),卡片体里才是条目。
|
|
6248
|
+
groupCard('global', t('prompts.group.global'), state.presets.length, null, body),
|
|
6249
|
+
quickSection,
|
|
5730
6250
|
state.notice ? React.createElement(Notice, { kind: state.notice.kind, text: state.notice.text }) : null,
|
|
5731
6251
|
exportState ? React.createElement(ExportModal, {
|
|
5732
6252
|
key: 'export', t: t, title: t('export.presets'),
|
|
5733
|
-
|
|
6253
|
+
// 两类一起列、按类分组(键前缀 `q:` 标快捷):勾完一次就能把两类都带走,
|
|
6254
|
+
// 不必为了导另一类再开一次弹窗。
|
|
6255
|
+
items: state.presets.map(function (p) { return { key: p.id, name: p.id, desc: p.description || '', group: t('prompts.group.global') } })
|
|
6256
|
+
.concat(quick.prompts.map(function (p) { return { key: 'q:' + p.id, name: p.id, desc: p.description || '', group: t('prompts.group.quick') } })),
|
|
5734
6257
|
busy: exportState.busy, result: exportState.result,
|
|
5735
6258
|
onClose: function () { setExportState(null) },
|
|
5736
6259
|
onSubmit: function (names, outDir) {
|
|
5737
|
-
setExportState({ busy: true, result: null })
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
6260
|
+
setExportState({ busy: true, result: null })
|
|
6261
|
+
var quickIds = [], presetIds = []
|
|
6262
|
+
names.forEach(function (k) {
|
|
6263
|
+
if (String(k).indexOf('q:') === 0) quickIds.push(String(k).slice(2))
|
|
6264
|
+
else presetIds.push(String(k))
|
|
6265
|
+
})
|
|
6266
|
+
var calls = []
|
|
6267
|
+
if (presetIds.length) calls.push({ kind: 'presets', call: apiCall('bundle-export', { kind: 'presets', names: presetIds, outDir: outDir }) })
|
|
6268
|
+
if (quickIds.length) calls.push({ kind: 'quick-prompts', call: apiCall('bundle-export', { kind: 'quick-prompts', names: quickIds, outDir: outDir }) })
|
|
6269
|
+
Promise.all(calls.map(function (c) { return c.call.catch(function (e) { return { ok: false, error: errMsg(e) } }) })).then(function (all) {
|
|
6270
|
+
// 两类各打一个 zip(文件名里带 kind,不会互相覆盖)。两份结果都要说出来:
|
|
6271
|
+
// 只报第一个就是"看起来全导了",而第二个可能根本没写出去。
|
|
6272
|
+
var texts = all.map(function (res, i) {
|
|
6273
|
+
var one = res && res.ok ? exportResultText(t, res) : translateError(t, res)
|
|
6274
|
+
return calls.length > 1 ? t(calls[i].kind === 'presets' ? 'prompts.group.global' : 'prompts.group.quick') + ':' + one : one
|
|
6275
|
+
})
|
|
6276
|
+
var okAll = all.length > 0 && all.every(function (res) { return res && res.ok })
|
|
6277
|
+
setExportState({ busy: false, result: { ok: okAll, warning: okAll && all.some(function (r) { return (r.missing || []).length }) }, text: texts.join(';') })
|
|
6278
|
+
})
|
|
5742
6279
|
},
|
|
5743
6280
|
}) : null,
|
|
5744
6281
|
trash ? React.createElement(TrashModal, {
|
|
5745
6282
|
t: t,
|
|
5746
6283
|
title: t('trash.title'),
|
|
5747
|
-
|
|
5748
|
-
|
|
6284
|
+
// 两组一起列:回收站是"找回被删的东西"的地方,让用户先猜它在哪个域里再切弹窗,
|
|
6285
|
+
// 等于把恢复这条路堵在自己手里。
|
|
6286
|
+
groups: [
|
|
6287
|
+
{ title: t('trash.group.presets'), sub: t('trash.section.presets.sub'), entries: trash.presets || [] },
|
|
6288
|
+
{ title: t('trash.group.quick'), sub: t('trash.section.quick.sub'), entries: trash.quick || [] },
|
|
6289
|
+
],
|
|
5749
6290
|
locked: state.anyLocked === true,
|
|
5750
|
-
entries: trash.entries,
|
|
5751
6291
|
loading: trash.loading,
|
|
5752
6292
|
error: trash.error,
|
|
5753
6293
|
busy: trashBusy,
|
|
@@ -5776,9 +6316,12 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5776
6316
|
editModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, editModal.error) : null,
|
|
5777
6317
|
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
5778
6318
|
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(editModal.nextId || '').trim(), onClick: doUpdate }, t('agm.btn.save')))) : null,
|
|
5779
|
-
// 新建:id +
|
|
5780
|
-
//
|
|
5781
|
-
|
|
6319
|
+
// 新建:id + 描述 + **正文文本框**(用户直接写内容)。原先还有一格「或从现有预设复制」,
|
|
6320
|
+
// 用户 2026-09-30 要求删掉 —— 抄一份再改是编辑页的事,新建时只想直接写。
|
|
6321
|
+
// 弹窗定高(`dsm-modal-fixed`):切类型时说明那一句长短不同,框不该跟着跳(手正停在
|
|
6322
|
+
// 下一格上),多出来的高度给正文文本框吃掉。
|
|
6323
|
+
createModal ? React.createElement(Modal, { title: t('agm.btn.new'), closeLabel: t('btn.close'), className: 'dsm-modal-fixed', onClose: function () { setCreateModal(null) } },
|
|
6324
|
+
kindPicker(createModal.kind, function (v) { setCreateModal(Object.assign({}, createModal, { kind: v, error: null })) }),
|
|
5782
6325
|
React.createElement('div', { className: 'dsm-field' },
|
|
5783
6326
|
React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
|
|
5784
6327
|
React.createElement('input', { className: 'dsm-control', placeholder: t('agm.field.id.placeholder'), value: createModal.id, onChange: function (e) { setCreateModal(Object.assign({}, createModal, { id: e.target.value, error: null })) } }),
|
|
@@ -5788,19 +6331,14 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5788
6331
|
React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
|
|
5789
6332
|
React.createElement('span', { className: 'dsm-char-count' + (String(createModal.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(createModal.description || '').length + ' / ' + PRESET_DESC_MAX)),
|
|
5790
6333
|
React.createElement('input', { className: 'dsm-control', maxLength: PRESET_DESC_MAX, placeholder: t('agm.field.desc.placeholder'), value: createModal.description || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { description: e.target.value })) } })),
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
options: [{ value: '', label: t('agm.field.copyFrom.none') }].concat((state.presets || []).map(function (p) { return { value: p.id, label: p.id } })),
|
|
5795
|
-
value: createModal.from || '',
|
|
5796
|
-
onChange: function (v) { loadFromPreset('create', v) },
|
|
5797
|
-
})) : null,
|
|
5798
|
-
React.createElement('div', { className: 'dsm-field' },
|
|
6334
|
+
// 原先这里有一格「或从现有预设复制」—— 用户 2026-09-30 要求删掉:新建时想要的正文
|
|
6335
|
+
// 直接写在下面那格里,抄一份再改是编辑页的事。
|
|
6336
|
+
React.createElement('div', { className: 'dsm-field dsm-field-grow' },
|
|
5799
6337
|
React.createElement('label', { className: 'dsm-label' }, t('agm.field.content')),
|
|
5800
|
-
React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', placeholder: t('agm.field.content.placeholder'), value: createModal.content || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { content: e.target.value })) } })),
|
|
6338
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', placeholder: t(createModal.kind === 'quick' ? 'quickprompt.field.content.placeholder' : 'agm.field.content.placeholder'), value: createModal.content || '', onChange: function (e) { setCreateModal(Object.assign({}, createModal, { content: e.target.value })) } })),
|
|
5801
6339
|
createModal.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, createModal.error) : null,
|
|
5802
6340
|
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
5803
|
-
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.
|
|
6341
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(createModal.id || '').trim(), onClick: function () { doCreate(createModal.id, createModal.content, createModal.description) } }, t('agm.btn.create')))) : null,
|
|
5804
6342
|
applyConfirm ? React.createElement(Modal, { title: applyConfirm.remove ? (t('agm.remove.title') + ' · ' + applyConfirm.id) : (t('agm.applyModal.title') + ' · ' + applyConfirm.id), closeLabel: t('btn.close'), onClose: function () { setApplyConfirm(null) } },
|
|
5805
6343
|
applyConfirm.remove
|
|
5806
6344
|
? React.createElement('p', { className: 'dsm-help' }, t('agm.remove.title') + ' ' + applyConfirm.id + t('agm.remove.suffix'))
|
|
@@ -5812,10 +6350,35 @@ html,body{scrollbar-gutter:stable}.dsm-settings-scroll-host{overflow-y:scroll!im
|
|
|
5812
6350
|
importOpen ? React.createElement(ImportModal, {
|
|
5813
6351
|
key: 'imp', t: t, title: t('agm.btn.import'), accept: '.md',
|
|
5814
6352
|
busy: importBusy, result: importResult,
|
|
6353
|
+
// 类型选择放在弹窗顶部(`extra` 槽):一次导入只进一类,选完再拖文件。
|
|
6354
|
+
extra: kindPicker(importKind, setImportKind),
|
|
5815
6355
|
requirements: [t('upload.requirement.prompt.1'), t('upload.requirement.prompt.2'), t('upload.requirement.prompt.3')],
|
|
5816
6356
|
onClose: function () { setImportOpen(false); setImportResult(null) },
|
|
5817
6357
|
onSubmit: function (entries, files) { doImportFiles(files) },
|
|
5818
|
-
}) : null
|
|
6358
|
+
}) : null,
|
|
6359
|
+
// 快捷提示词的编辑:正文直接来自列表(`quickprompt-list` 连正文一起回),
|
|
6360
|
+
// 不像预设那样还要再发一次 read。
|
|
6361
|
+
quickEdit ? React.createElement(Modal, { title: t('quickprompt.edit.title') + ' · ' + quickEdit.id, closeLabel: t('btn.close'), onClose: function () { setQuickEdit(null) } },
|
|
6362
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
6363
|
+
React.createElement('label', { className: 'dsm-label' }, t('agm.field.id')),
|
|
6364
|
+
React.createElement('input', { className: 'dsm-control', value: quickEdit.nextId || '', onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { nextId: e.target.value, error: null })) } }),
|
|
6365
|
+
React.createElement('p', { className: 'dsm-help' }, t('quickprompt.field.id.hint'))),
|
|
6366
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
6367
|
+
React.createElement('div', { className: 'dsm-budget-meta' },
|
|
6368
|
+
React.createElement('label', { className: 'dsm-label' }, t('agm.field.desc')),
|
|
6369
|
+
React.createElement('span', { className: 'dsm-char-count' + (String(quickEdit.description || '').length > PRESET_DESC_MAX ? ' dsm-char-over' : '') }, String(quickEdit.description || '').length + ' / ' + PRESET_DESC_MAX)),
|
|
6370
|
+
React.createElement('input', { className: 'dsm-control', maxLength: PRESET_DESC_MAX, placeholder: t('quickprompt.field.desc.placeholder'), value: quickEdit.description || '', onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { description: e.target.value })) } })),
|
|
6371
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
6372
|
+
React.createElement('label', { className: 'dsm-label' }, t('quickprompt.field.content')),
|
|
6373
|
+
React.createElement('textarea', { className: 'dsm-control dsm-textarea-lg', value: quickEdit.content, onChange: function (e) { setQuickEdit(Object.assign({}, quickEdit, { content: e.target.value })) } })),
|
|
6374
|
+
quickEdit.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, quickEdit.error) : null,
|
|
6375
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
6376
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn', disabled: busy !== null || !String(quickEdit.nextId || '').trim(), onClick: doQuickUpdate }, t('agm.btn.save')))) : null,
|
|
6377
|
+
quickConfirm ? React.createElement(Modal, { title: t('quickprompt.remove.title') + ' · ' + quickConfirm.id, closeLabel: t('btn.close'), onClose: function () { setQuickConfirm(null) } },
|
|
6378
|
+
React.createElement('p', { className: 'dsm-help' }, t('quickprompt.remove.hint')),
|
|
6379
|
+
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
6380
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-quiet', disabled: busy !== null, onClick: function () { setQuickConfirm(null) } }, t('btn.cancel')),
|
|
6381
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy !== null, onClick: function () { doQuickRemove(quickConfirm.id) } }, t('agm.btn.confirmRemove')))) : null)
|
|
5819
6382
|
}
|
|
5820
6383
|
|
|
5821
6384
|
// 「工具」设置页:一个侧栏项,内部 tab 切换 场景 / MCP / 技能 / 子智能体 / 提示词 / 记忆 / 会话。
|
|
@@ -6290,6 +6853,11 @@ function enabledMemoryCount(rules) {
|
|
|
6290
6853
|
}
|
|
6291
6854
|
// 场景输入(combobox):既能从已有场景里挑,也能直接手输一个新场景名——
|
|
6292
6855
|
// 宿主 rules-create 用 mkdir(recursive) 落盘,所以「自定义场景」无需先建目录。
|
|
6856
|
+
//
|
|
6857
|
+
// `onPickFolder` 是可选的第二颗按钮(0.18.5,会话导入选工作区):选项来自登记列表,但用户
|
|
6858
|
+
// 也可能要挑一个从没登记过的目录,那就得走目录浏览器。它渲染在 .dsm-combo-row **里面**
|
|
6859
|
+
// 而不是外面 —— .dsm-field 是列向 flex,放外面会掉到下拉框下方;而 .dsm-select-menu 相对
|
|
6860
|
+
// .dsm-select 绝对定位,另起一行就没有对齐的锚点了。两颗都是 .dsm-btn-secondary,同高。
|
|
6293
6861
|
function SceneCombo(props) {
|
|
6294
6862
|
var state = react.useState(false), open = state[0], setOpen = state[1], ref = react.useRef(null);
|
|
6295
6863
|
react.useEffect(function () { if (!open) return undefined; function close(event) { if (!ref.current || !ref.current.contains(event.target)) setOpen(false); } document.addEventListener("pointerdown", close); return function () { document.removeEventListener("pointerdown", close); }; }, [open]);
|
|
@@ -6297,7 +6865,9 @@ function enabledMemoryCount(rules) {
|
|
|
6297
6865
|
return h("div", { className: "dsm-select", ref: ref },
|
|
6298
6866
|
h("div", { className: "dsm-combo-row" },
|
|
6299
6867
|
h("input", { className: "dsm-control" + (props.invalid ? " dsm-rule-invalid" : ""), type: "text", value: props.value || "", placeholder: props.placeholder, "aria-label": props.label, onChange: function (e) { props.onChange(e.target.value); } }),
|
|
6300
|
-
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel)
|
|
6868
|
+
h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", "aria-haspopup": "listbox", "aria-expanded": open, disabled: props.disabled || !options.length, onClick: function () { setOpen(!open); } }, props.browseLabel),
|
|
6869
|
+
// 开目录浏览器前先收菜单:这颗按钮在 ref 里面,document 上那个 pointerdown 关闭器不会替我关。
|
|
6870
|
+
props.onPickFolder ? h("button", { type: "button", className: "dsm-btn dsm-btn-secondary", title: props.pickFolderTitle, disabled: props.disabled === true, onClick: function () { setOpen(false); props.onPickFolder(); } }, props.pickFolderLabel) : null),
|
|
6301
6871
|
open && options.length ? h("div", { className: "dsm-select-menu", role: "listbox" }, options.map(function (o) { return h("button", { key: o.value, type: "button", role: "option", className: "dsm-option", "aria-selected": o.value === props.value, onClick: function () { props.onChange(o.value); setOpen(false); } }, o.label); })) : null);
|
|
6302
6872
|
}
|
|
6303
6873
|
function Switch(props) { return h("button", { type: "button", className: "dsm-switch" + (props.on ? " dsm-switch-on" : ""), role: "switch", "aria-checked": props.on, "aria-label": props.label, title: props.title, disabled: props.disabled, onClick: props.onClick }); }
|
|
@@ -6411,15 +6981,14 @@ function enabledMemoryCount(rules) {
|
|
|
6411
6981
|
/* ── 勾选列表原语(场景档案编辑器 / 人设工具选择器 / 任何「多选一组条目」的地方共用)────
|
|
6412
6982
|
原先这套排版只在 ScenesPage 内部,人设的工具选择器要么复制一份、要么写成裸 input;
|
|
6413
6983
|
现在提到模块作用域,页面之间才可能真正长得一样。 */
|
|
6414
|
-
/** 段卡片:标题栏 +
|
|
6984
|
+
/** 段卡片:标题栏 + 固定高度滚动体(高度不随内容增减变化,弹窗不跳)。 */
|
|
6415
6985
|
function seg(props) {
|
|
6416
6986
|
return h('div', { className: 'dsm-seg' },
|
|
6417
6987
|
h('div', { className: 'dsm-seg-head' },
|
|
6418
6988
|
h('span', { className: 'dsm-seg-title' }, props.title),
|
|
6419
6989
|
h('span', { className: 'dsm-seg-count' }, props.count),
|
|
6420
6990
|
h('div', { className: 'dsm-seg-actions' }, props.actions)),
|
|
6421
|
-
props.body
|
|
6422
|
-
props.foot || null)
|
|
6991
|
+
props.body)
|
|
6423
6992
|
}
|
|
6424
6993
|
/**
|
|
6425
6994
|
* 通用回收站弹窗:一行一个条目(名字 + 删除时间),右侧「恢复 / 永久删除」。
|
|
@@ -6431,7 +7000,6 @@ function enabledMemoryCount(rules) {
|
|
|
6431
7000
|
const t = props.t
|
|
6432
7001
|
const c = React.useState(null)
|
|
6433
7002
|
const confirmId = c[0], setConfirmId = c[1]
|
|
6434
|
-
const entries = props.entries || []
|
|
6435
7003
|
const row = function (item) {
|
|
6436
7004
|
const confirming = confirmId === item.id
|
|
6437
7005
|
const actions = confirming
|
|
@@ -6452,11 +7020,26 @@ function enabledMemoryCount(rules) {
|
|
|
6452
7020
|
}))),
|
|
6453
7021
|
actions)
|
|
6454
7022
|
}
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
7023
|
+
// 组:默认就是调用方传的那一组(`groupTitle` / `entries`);给了 `groups` 就按数组列多组
|
|
7024
|
+
// —— 提示词页的回收站要一次看完"全局"与"快捷"两类,让用户先猜被删的东西在哪个域里,
|
|
7025
|
+
// 等于把恢复这条路堵在他自己手里。每组各自有空的说明,不合并成一句"什么都没删"。
|
|
7026
|
+
const groups = props.groups || [{ title: props.groupTitle, sub: props.groupSub, entries: props.entries || [] }]
|
|
7027
|
+
const renderGroup = function (g) {
|
|
7028
|
+
const list = g.entries || []
|
|
7029
|
+
const rows = props.loading
|
|
7030
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('memory.loading'))
|
|
7031
|
+
: list.length === 0
|
|
7032
|
+
? React.createElement('div', { className: 'dsm-empty' }, t('trash.empty'))
|
|
7033
|
+
: list.map(row)
|
|
7034
|
+
return React.createElement('div', { className: 'dsm-trash-group', key: String(g.title) },
|
|
7035
|
+
React.createElement('div', { className: 'dsm-trash-group-head' },
|
|
7036
|
+
React.createElement('div', { className: 'dsm-trash-group-head-row' },
|
|
7037
|
+
React.createElement('span', { className: 'dsm-trash-group-title' }, g.title),
|
|
7038
|
+
React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: list.length }))),
|
|
7039
|
+
// 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
|
|
7040
|
+
g.sub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, g.sub) : null),
|
|
7041
|
+
React.createElement('div', { className: 'dsm-trash-group-body' }, rows))
|
|
7042
|
+
}
|
|
6460
7043
|
// 列表类弹窗:宽高都固定(`.dsm-modal-list`),滚动只发生在 Modal 的 body 这一层 ——
|
|
6461
7044
|
// 条目数变化时弹窗不跳,也不会出现「弹窗滚 + 组内滚」的嵌套滚动条。
|
|
6462
7045
|
return React.createElement(Modal, {
|
|
@@ -6468,14 +7051,7 @@ function enabledMemoryCount(rules) {
|
|
|
6468
7051
|
onClose: props.onClose,
|
|
6469
7052
|
},
|
|
6470
7053
|
props.error ? React.createElement(Notice, { kind: 'err', text: props.error }) : null,
|
|
6471
|
-
|
|
6472
|
-
React.createElement('div', { className: 'dsm-trash-group-head' },
|
|
6473
|
-
React.createElement('div', { className: 'dsm-trash-group-head-row' },
|
|
6474
|
-
React.createElement('span', { className: 'dsm-trash-group-title' }, props.groupTitle),
|
|
6475
|
-
React.createElement('span', { className: 'dsm-count' }, t('trash.items.count', { count: entries.length }))),
|
|
6476
|
-
// 后果说明进组头(sticky 跟着滚):这组删的是记录还是文件,一眼可辨。
|
|
6477
|
-
props.groupSub ? React.createElement('p', { className: 'dsm-trash-group-sub' }, props.groupSub) : null),
|
|
6478
|
-
React.createElement('div', { className: 'dsm-trash-group-body' }, body)))
|
|
7054
|
+
groups.map(renderGroup))
|
|
6479
7055
|
}
|
|
6480
7056
|
|
|
6481
7057
|
/** 勾选行:复选框 + 名称/说明 + 右侧指标 + 可选行内动作。 */
|
|
@@ -6500,6 +7076,15 @@ function enabledMemoryCount(rules) {
|
|
|
6500
7076
|
h('span', null, label),
|
|
6501
7077
|
h('span', { className: 'dsm-seg-group-line' }))
|
|
6502
7078
|
}
|
|
7079
|
+
/**
|
|
7080
|
+
* 表单分段标题(「基本信息 / 场景绑定」这类模块切分)。
|
|
7081
|
+
*
|
|
7082
|
+
* 与 `segGroup` 的差别只在**用在哪**:那个是滚动列表内部的地标(左右各留了内缩),
|
|
7083
|
+
* 这个是表单第一层,必须与下面的字段左边缘对齐,所以不套那圈 padding。
|
|
7084
|
+
*/
|
|
7085
|
+
function formSection(label) {
|
|
7086
|
+
return h('div', { className: 'dsm-form-section' }, h('span', null, label))
|
|
7087
|
+
}
|
|
6503
7088
|
/**
|
|
6504
7089
|
* 「全选 ↔ 取消全选」二合一按钮的标签对 —— fixedLabelPair 的语义化别名(同一套定宽机制)。
|
|
6505
7090
|
*
|
|
@@ -6523,11 +7108,6 @@ function enabledMemoryCount(rules) {
|
|
|
6523
7108
|
if (props.onRemove) out.push(h('button', { key: 'rm', type: 'button', className: 'dsm-btn dsm-btn-quiet dsm-btn-danger', disabled: props.busy, onClick: props.onRemove }, props.removeLabel))
|
|
6524
7109
|
return out
|
|
6525
7110
|
}
|
|
6526
|
-
/** 段脚注:段已定义但一项未勾 = 该域全部停用(必须显式提示,否则像没保存上)。 */
|
|
6527
|
-
function segFoot(defined, count, emptyLabel) {
|
|
6528
|
-
if (!defined || count > 0) return null
|
|
6529
|
-
return h('div', { className: 'dsm-seg-foot' }, emptyLabel)
|
|
6530
|
-
}
|
|
6531
7111
|
/** 固定高度滚动体内的条目列表(空态居中提示)。 */
|
|
6532
7112
|
function segList(nodes, emptyText) {
|
|
6533
7113
|
if (!nodes.length) return h('div', { className: 'dsm-seg-body' }, h('div', { className: 'dsm-pick-empty' }, emptyText))
|
|
@@ -7534,10 +8114,10 @@ function enabledMemoryCount(rules) {
|
|
|
7534
8114
|
// 多选集合:勾选的 sessionId。最小 React 支持函数式初值(Set 惰性创建)。
|
|
7535
8115
|
var selState = React.useState(function () { return new Set() })
|
|
7536
8116
|
var selected = selState[0], setSelected = selState[1]
|
|
7537
|
-
// 导入对话弹窗:null = 关闭;{ busy, error, result, cwd } = 打开。
|
|
8117
|
+
// 导入对话弹窗:null = 关闭;{ busy, error, result, cwd, pickingDir } = 打开。
|
|
7538
8118
|
var impState = React.useState(null)
|
|
7539
8119
|
var imp = impState[0], setImp = impState[1]
|
|
7540
|
-
// 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, wsFilter, selectedIds } = 打开。
|
|
8120
|
+
// 导出对话弹窗:null = 关闭;{ busy, error, result, outDir, format, detail, wsFilter, selectedIds } = 打开。
|
|
7541
8121
|
var expState = React.useState(null)
|
|
7542
8122
|
var exp = expState[0], setExp = expState[1]
|
|
7543
8123
|
// 分组卡片折叠状态:key → true(收起)。默认全部展开。
|
|
@@ -7706,11 +8286,11 @@ function enabledMemoryCount(rules) {
|
|
|
7706
8286
|
reader.readAsText(file)
|
|
7707
8287
|
}
|
|
7708
8288
|
function openImportPicker() {
|
|
7709
|
-
setImp({ busy: false, error: null, result: null, cwd: '' })
|
|
8289
|
+
setImp({ busy: false, error: null, result: null, cwd: '', pickingDir: false })
|
|
7710
8290
|
}
|
|
7711
8291
|
// ---------- 导出对话:弹窗内选范围/工作区 → 勾选会话 → 写入指定目录 ----------
|
|
7712
8292
|
function openExportPicker() {
|
|
7713
|
-
setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
|
|
8293
|
+
setExp({ busy: false, error: null, result: null, outDir: '', format: 'markdown', detail: 'simple', wsFilter: '', archFilter: 'all', items: [], loading: true, selectedIds: new Set() })
|
|
7714
8294
|
// 拉取全部持久化会话(含未归档/冷会话),供选择导出。
|
|
7715
8295
|
apiCall('history-sessions', {}).then(function (res) {
|
|
7716
8296
|
setExp(function (prev) {
|
|
@@ -7751,7 +8331,7 @@ function enabledMemoryCount(rules) {
|
|
|
7751
8331
|
var ids = Array.from(exp.selectedIds)
|
|
7752
8332
|
if (!ids.length || !String(exp.outDir || '').trim()) return
|
|
7753
8333
|
setExp(Object.assign({}, exp, { busy: true, error: null, result: null }))
|
|
7754
|
-
apiCall('history-export', { sessionIds: ids, format: exp.format, outDir: String(exp.outDir).trim() }).then(function (res) {
|
|
8334
|
+
apiCall('history-export', { sessionIds: ids, format: exp.format, detail: exp.detail, outDir: String(exp.outDir).trim() }).then(function (res) {
|
|
7755
8335
|
if (res && res.ok) setExp(Object.assign({}, exp, { busy: false, result: res }))
|
|
7756
8336
|
else setExp(Object.assign({}, exp, { busy: false, error: (res && res.error) || t('hist.err.export') }))
|
|
7757
8337
|
}).catch(function () {
|
|
@@ -7793,6 +8373,11 @@ function enabledMemoryCount(rules) {
|
|
|
7793
8373
|
{ value: 'markdown', label: 'Markdown' },
|
|
7794
8374
|
{ value: 'jsonl', label: 'JSONL' },
|
|
7795
8375
|
]
|
|
8376
|
+
var EXPORT_DETAIL_OPTS = [
|
|
8377
|
+
{ value: 'simple', label: t('hist.export.detail.simple') },
|
|
8378
|
+
{ value: 'tools', label: t('hist.export.detail.tools') },
|
|
8379
|
+
{ value: 'full', label: t('hist.export.detail.full') },
|
|
8380
|
+
]
|
|
7796
8381
|
var EXPORT_ARCH_OPTS = [
|
|
7797
8382
|
{ value: 'all', label: t('hist.arch.all') },
|
|
7798
8383
|
{ value: 'archived', label: t('hist.arch.archived') },
|
|
@@ -7811,6 +8396,24 @@ function enabledMemoryCount(rules) {
|
|
|
7811
8396
|
})
|
|
7812
8397
|
exportWsOptions.push({ value: 'ungrouped', label: t('hist.ungrouped') })
|
|
7813
8398
|
}
|
|
8399
|
+
// 导入弹窗的工作区候选:只列**活登记**的组,值用目录路径(history-import 吃的就是路径)。
|
|
8400
|
+
// 「已移除」的游离组不进来 —— 它按定义就是没登记,让它冒充「选一个工作区」会静默造出
|
|
8401
|
+
// 本页标着「已移除」的那种状态;真要用那个目录还有「浏览」,手输也一直留着。
|
|
8402
|
+
var importWsOptions = []
|
|
8403
|
+
;(function () {
|
|
8404
|
+
var seen = {}
|
|
8405
|
+
function pushOption(title, path) {
|
|
8406
|
+
var p = String(path || '')
|
|
8407
|
+
if (!p || seen[p]) return
|
|
8408
|
+
seen[p] = true
|
|
8409
|
+
importWsOptions.push({ value: p, label: (title ? title + ' — ' : '') + p })
|
|
8410
|
+
}
|
|
8411
|
+
;(data.groups || []).forEach(function (g) { if (g && g.kind === 'live') pushOption(g.title, g.path) })
|
|
8412
|
+
if (!importWsOptions.length) {
|
|
8413
|
+
var ws = data.workspaces || {}
|
|
8414
|
+
Object.keys(ws).forEach(function (wid) { pushOption(ws[wid] && ws[wid].title, ws[wid] && ws[wid].path) })
|
|
8415
|
+
}
|
|
8416
|
+
})()
|
|
7814
8417
|
var exportItems = []
|
|
7815
8418
|
if (exp) {
|
|
7816
8419
|
var useGroups = !!(data.groups && data.groups.length)
|
|
@@ -7941,28 +8544,49 @@ function enabledMemoryCount(rules) {
|
|
|
7941
8544
|
extra: React.createElement('div', null,
|
|
7942
8545
|
React.createElement('div', { className: 'dsm-field' },
|
|
7943
8546
|
React.createElement('label', { className: 'dsm-label' }, t('hist.import.cwd')),
|
|
7944
|
-
|
|
8547
|
+
// 三条路并存:从已登记的工作区挑、浏览一个文件夹、直接粘路径。
|
|
8548
|
+
// 标签是 combo 的**兄弟**而不是祖先 —— 嵌套 label 会把点击转给清单里第一项。
|
|
8549
|
+
React.createElement(SceneCombo, {
|
|
8550
|
+
options: importWsOptions, value: imp.cwd || '',
|
|
8551
|
+
placeholder: t('hist.import.cwd.placeholder'), label: t('hist.import.cwd'),
|
|
8552
|
+
browseLabel: t('hist.import.workspace'),
|
|
8553
|
+
pickFolderLabel: t('hist.import.browse'), pickFolderTitle: t('hist.export.browse'),
|
|
8554
|
+
disabled: imp.busy === true,
|
|
8555
|
+
onChange: function (v) { setImp(Object.assign({}, imp, { cwd: v })) },
|
|
8556
|
+
onPickFolder: function () { setImp(Object.assign({}, imp, { pickingDir: true })) },
|
|
8557
|
+
}),
|
|
8558
|
+
imp.pickingDir ? React.createElement(DirPickerModal, { key: 'imp-dir-picker', title: t('dir.title'), initial: String(imp.cwd || ''), closeLabel: t('btn.close'), onClose: function () { setImp(Object.assign({}, imp, { pickingDir: false })) }, onPick: function (path) { setImp(Object.assign({}, imp, { cwd: path, pickingDir: false })) } }) : null,
|
|
8559
|
+
importWsOptions.length ? null : React.createElement('p', { className: 'dsm-help' }, t('hist.import.workspace.none')))),
|
|
7945
8560
|
onClose: function () { setImp(null) },
|
|
7946
8561
|
onSubmit: function (entries, files) { doImportFile(files[0]) },
|
|
7947
8562
|
}) : null,
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
React.createElement('
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
8563
|
+
// 导出弹窗(0.18.5):字段从五行压成三行(格式|导出内容 一行、会话范围|工作区
|
|
8564
|
+
// 一行),整块包进 .dsm-export-form 让列表吃掉剩下的高度。范围和工作是两个正交
|
|
8565
|
+
// 条件,不做成笛卡尔积的单个下拉 —— 合成一行只是省位置,语义没合。
|
|
8566
|
+
exp ? React.createElement(Modal, { key: 'exp', className: 'dsm-modal-list dsm-modal-export-lg dsm-export-modal', list: true, title: t('hist.btn.export'), closeLabel: t('btn.close'), onClose: function () { setExp(null) } },
|
|
8567
|
+
React.createElement('div', { className: 'dsm-form dsm-export-form' },
|
|
8568
|
+
React.createElement('p', { className: 'dsm-help' }, t('hist.export.hint')),
|
|
8569
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
8570
|
+
React.createElement('label', { className: 'dsm-label' }, t('hist.export.dir')),
|
|
8571
|
+
React.createElement('div', { className: 'dsm-dir-row' },
|
|
8572
|
+
React.createElement('input', { className: 'dsm-control', type: 'text', placeholder: 'D:\\backups\\dsh\\exports', value: exp.outDir || '', onChange: function (e) { setExp(Object.assign({}, exp, { outDir: e.target.value })) } }),
|
|
8573
|
+
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-secondary', disabled: exp.busy, onClick: openExportDir, title: t('hist.export.browse') }, t('hist.btn.browse'))),
|
|
8574
|
+
exp.pickingDir ? React.createElement(DirPickerModal, { key: 'exp-dir-picker', title: t('hist.export.pickDir'), initial: String(exp.outDir || ''), closeLabel: t('btn.close'), onClose: function () { setExp(Object.assign({}, exp, { pickingDir: false })) }, onPick: function (path) { setExp(Object.assign({}, exp, { outDir: path, pickingDir: false })) } }) : null),
|
|
8575
|
+
React.createElement('div', { className: 'dsm-field-row' },
|
|
8576
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
8577
|
+
React.createElement('label', { className: 'dsm-label' }, t('hist.export.format')),
|
|
8578
|
+
React.createElement(SourceSelect, { options: EXPORT_FORMAT_OPTS, value: exp.format, onChange: function (v) { setExp(Object.assign({}, exp, { format: v })) } })),
|
|
8579
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
8580
|
+
React.createElement('label', { className: 'dsm-label' }, t('hist.export.detail')),
|
|
8581
|
+
React.createElement(SourceSelect, { options: EXPORT_DETAIL_OPTS, value: exp.detail, onChange: function (v) { setExp(Object.assign({}, exp, { detail: v })) } }))),
|
|
8582
|
+
React.createElement('div', { className: 'dsm-field-row' },
|
|
8583
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
8584
|
+
React.createElement('label', { className: 'dsm-label' }, t('hist.export.scope')),
|
|
8585
|
+
React.createElement(SourceSelect, { options: EXPORT_ARCH_OPTS, value: exp.archFilter, onChange: function (v) { setExp(Object.assign({}, exp, { archFilter: v, selectedIds: new Set() })) } })),
|
|
8586
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
8587
|
+
React.createElement('label', { className: 'dsm-label' }, t('hist.export.workspace')),
|
|
8588
|
+
React.createElement(SourceSelect, { options: exportWsOptions, value: exp.wsFilter, onChange: function (v) { setExp(Object.assign({}, exp, { wsFilter: v, selectedIds: new Set() })) } }))),
|
|
8589
|
+
React.createElement('div', { className: 'dsm-field dsm-export-body' },
|
|
7966
8590
|
React.createElement('div', { className: 'dsm-label' }, t('hist.export.sessions', { count: exportItems.length })),
|
|
7967
8591
|
React.createElement('div', { className: 'dsm-source dsm-pick-list' },
|
|
7968
8592
|
exp.loading ? React.createElement('div', { className: 'dsm-empty' }, t('hist.export.loading'))
|
|
@@ -7992,7 +8616,8 @@ function enabledMemoryCount(rules) {
|
|
|
7992
8616
|
React.createElement('span', { className: 'dsm-tag' + (it.archived ? '' : ' dsm-tag-on') }, it.archived ? t('hist.tag.archived') : t('hist.tag.live'))))
|
|
7993
8617
|
}) : null)
|
|
7994
8618
|
})
|
|
7995
|
-
})() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty')))),
|
|
8619
|
+
})() : React.createElement('div', { className: 'dsm-empty' }, t('hist.export.empty'))))),
|
|
8620
|
+
// ↑ 最后那个括号收掉 .dsm-export-form:结果条与动作行留在表单外面,才能常驻不动。
|
|
7996
8621
|
exp.error ? React.createElement('div', { className: 'dsm-feedback dsm-error' }, String(exp.error)) : null,
|
|
7997
8622
|
exp.result ? React.createElement('div', { className: 'dsm-feedback' },
|
|
7998
8623
|
exp.result.exported
|
|
@@ -8132,6 +8757,9 @@ function enabledMemoryCount(rules) {
|
|
|
8132
8757
|
// 工具表方案下拉的数据源(C1;按用户 2026-09-28 裁定从档案编辑器移进「编辑场景」表单)。
|
|
8133
8758
|
var tts = React.useState(null)
|
|
8134
8759
|
var ttInfo = tts[0], setTtInfo = tts[1]
|
|
8760
|
+
// 快捷提示词候选(同一族:勾选入口在「修改场景」表单里,档案编辑器不带这一栏)。
|
|
8761
|
+
var qps = React.useState([])
|
|
8762
|
+
var quickOptions = qps[0], setQuickOptions = qps[1]
|
|
8135
8763
|
var dts = React.useState(null)
|
|
8136
8764
|
var drillTools = dts[0], setDrillTools = dts[1]
|
|
8137
8765
|
// 场景回收站(记录 + 档案):null = 关闭;{loading, error, entries}
|
|
@@ -8292,15 +8920,23 @@ function enabledMemoryCount(rules) {
|
|
|
8292
8920
|
setSceneForm(function (f) { return (f && String(f.prompt || '') !== '') ? f : Object.assign({}, f, { prompt: activeId }) })
|
|
8293
8921
|
}
|
|
8294
8922
|
// ── 场景建 / 改描述 / 改绑定的提示词 / 删 ──
|
|
8295
|
-
|
|
8923
|
+
// `quickPrompts` 三态:null = 「当前启用的」(这一域不绑、进出不碰);数组 = 按场景勾选
|
|
8924
|
+
// (**空数组也是"绑了"**,意思是进入后全部停用)。表单里那颗下拉只给这两个方向。
|
|
8925
|
+
function openCreateScene() { setSceneForm({ name: '', description: '', prompt: '', toolTablePreset: '', quickPrompts: null, error: null }); setModal({ type: 'scene-create' }); loadPresetOptions(defaultPromptToActive); loadToolTablePresets(); loadQuickOptions() }
|
|
8296
8926
|
function openEditScene(scene) {
|
|
8297
8927
|
var sceneArchive = (data.archives || {})[scene.name] || {}
|
|
8298
|
-
setSceneForm({
|
|
8928
|
+
setSceneForm({
|
|
8929
|
+
name: scene.name, description: scene.description || '', prompt: scene.prompt || '',
|
|
8930
|
+
toolTablePreset: typeof sceneArchive.toolTablePreset === 'string' ? sceneArchive.toolTablePreset : '',
|
|
8931
|
+
quickPrompts: Array.isArray(sceneArchive.quickPrompts) ? sceneArchive.quickPrompts.slice() : null,
|
|
8932
|
+
error: null,
|
|
8933
|
+
})
|
|
8299
8934
|
setModal({ type: 'scene-edit', name: scene.name })
|
|
8300
8935
|
// 该场景还没绑定时,默认选中当前生效的那份(用户裁定「默认就是当前启动的」)。
|
|
8301
8936
|
if (!scene.prompt) loadPresetOptions(defaultPromptToActive)
|
|
8302
8937
|
else loadPresetOptions()
|
|
8303
8938
|
loadToolTablePresets()
|
|
8939
|
+
loadQuickOptions()
|
|
8304
8940
|
}
|
|
8305
8941
|
/** 新建与编辑共用一个表单:字段相同,只是分别走 rules-create-scene / rules-update-scene。 */
|
|
8306
8942
|
function submitSceneForm() {
|
|
@@ -8315,9 +8951,13 @@ function enabledMemoryCount(rules) {
|
|
|
8315
8951
|
// 工具表方案绑定(C1)单独一个参数:空串 = 解绑;服务端写进档案,
|
|
8316
8952
|
// 改的是启用中的场景时会立即应用那份方案(见 scene-sync 的包装)。
|
|
8317
8953
|
var toolTablePreset = String(sceneForm.toolTablePreset || '')
|
|
8954
|
+
// 快捷提示词:null = 解绑(回到"保持现状"),数组 = 勾选集(空数组是"一条都不勾")。
|
|
8955
|
+
// 两个方向都必须**显式送出去**,不能省略 —— 省略在服务端那边是"这一域不碰"。
|
|
8956
|
+
var quickPrompts = sceneForm.quickPrompts === null || sceneForm.quickPrompts === undefined
|
|
8957
|
+
? null : (sceneForm.quickPrompts || []).slice()
|
|
8318
8958
|
var payload = isEdit
|
|
8319
|
-
? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset }
|
|
8320
|
-
: { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset }
|
|
8959
|
+
? { name: originalName, nextName: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
|
|
8960
|
+
: { name: name, description: description, prompt: prompt, toolTablePreset: toolTablePreset, quickPrompts: quickPrompts }
|
|
8321
8961
|
apiCall(isEdit ? 'rules-update-scene' : 'rules-create-scene', payload).then(function (res) {
|
|
8322
8962
|
setBusy(false)
|
|
8323
8963
|
if (res && res.ok) {
|
|
@@ -8372,6 +9012,15 @@ function enabledMemoryCount(rules) {
|
|
|
8372
9012
|
} })
|
|
8373
9013
|
}).catch(function (e) { setBusy(false); setResult({ ok: false, text: errMsg(e) }) })
|
|
8374
9014
|
}
|
|
9015
|
+
/**
|
|
9016
|
+
* 快捷提示词候选(只读 op,没填令牌也拿得到)。读不到就当没有:下拉仍给
|
|
9017
|
+
* 「当前启用的(保持现状)」这一项,只是切到「按场景勾选」时列表是空的。
|
|
9018
|
+
*/
|
|
9019
|
+
function loadQuickOptions() {
|
|
9020
|
+
apiCall('quickprompt-list', {}).then(function (res) {
|
|
9021
|
+
setQuickOptions((res && res.ok && Array.isArray(res.prompts)) ? res.prompts : [])
|
|
9022
|
+
}).catch(function () { setQuickOptions([]) })
|
|
9023
|
+
}
|
|
8375
9024
|
/**
|
|
8376
9025
|
* 工具表方案下拉的数据源(C1):只读调用(不带 `set:true`),
|
|
8377
9026
|
* 所以没填令牌也能拿。读不到就当没有(下拉只剩「不绑定」+「出厂默认」)。
|
|
@@ -8428,8 +9077,8 @@ function enabledMemoryCount(rules) {
|
|
|
8428
9077
|
* 取 2026-09-13 前的「预勾当前运行时状态」是错的方向:用户点「添加」只是想开始配,
|
|
8429
9078
|
* 不是想把自己现有的启用状态抄进档案。代价要讲清楚——勾选集语义是「勾 = 启用」,
|
|
8430
9079
|
* 空段 = 全部停用(未定义段也是同一个意思,见 archive-engine 的四域同口径),
|
|
8431
|
-
* 所以「添加 MCP 工具集」当下就等价于「该场景下全部 MCP
|
|
8432
|
-
*
|
|
9080
|
+
* 所以「添加 MCP 工具集」当下就等价于「该场景下全部 MCP 停用」。段脚注原先把这句话
|
|
9081
|
+
* 印在每一段底下,2026-09-30 用户裁定撤掉(四段同屏时同一件事说了四遍)。
|
|
8433
9082
|
*/
|
|
8434
9083
|
function emptyMcpPreset() { return {} }
|
|
8435
9084
|
/** 全选:列出全部服务器(含未运行的)并各勾「全部工具」。 */
|
|
@@ -8452,13 +9101,6 @@ function enabledMemoryCount(rules) {
|
|
|
8452
9101
|
onAdd: onAdd, onRemove: onRemove, onAll: onAll, onClear: onClear, allChecked: allChecked,
|
|
8453
9102
|
})
|
|
8454
9103
|
}
|
|
8455
|
-
/**
|
|
8456
|
-
* 段脚注:段已定义但一项未勾必须显式提示,否则像「没保存上」。
|
|
8457
|
-
* 各域的后果不同(MCP/技能/记忆 = 全部停用;子智能体 = 不限制),所以文案由调用方给。
|
|
8458
|
-
*/
|
|
8459
|
-
function archiveSegFoot(defined, count, emptyLabel) {
|
|
8460
|
-
return segFoot(defined, count, emptyLabel || t('memory.archive.emptySection'))
|
|
8461
|
-
}
|
|
8462
9104
|
|
|
8463
9105
|
// ── 段 4:记忆(P5:单一真相源 = `rules[*].enabled`)────────────────────
|
|
8464
9106
|
// 这个段**没有自己的「段定义」概念**:勾选状态**就是**记忆自身的 `enabled`,
|
|
@@ -8610,7 +9252,6 @@ function enabledMemoryCount(rules) {
|
|
|
8610
9252
|
}))
|
|
8611
9253
|
})())
|
|
8612
9254
|
: React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.mcp.hint'))),
|
|
8613
|
-
foot: archiveSegFoot(defined, defined ? Object.keys(sections.mcp).length : 0),
|
|
8614
9255
|
})
|
|
8615
9256
|
}
|
|
8616
9257
|
/**
|
|
@@ -8760,7 +9401,6 @@ function enabledMemoryCount(rules) {
|
|
|
8760
9401
|
}))
|
|
8761
9402
|
: React.createElement('div', { className: 'dsm-pick-empty' }, modal.skillQuery ? t('scenes.mem.noMatch') : t('scenes.skills.empty')))
|
|
8762
9403
|
: React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.skills.hint'))),
|
|
8763
|
-
foot: archiveSegFoot(defined, defined ? pickedCount : 0),
|
|
8764
9404
|
})
|
|
8765
9405
|
}
|
|
8766
9406
|
/** 段 3:子智能体绑定。 */
|
|
@@ -8835,7 +9475,6 @@ function enabledMemoryCount(rules) {
|
|
|
8835
9475
|
}))
|
|
8836
9476
|
: React.createElement('div', { className: 'dsm-pick-empty' }, modal.subQuery ? t('scenes.mem.noMatch') : t('scenes.subagents.empty')))
|
|
8837
9477
|
: React.createElement('div', { className: 'dsm-seg-body' }, React.createElement('div', { className: 'dsm-pick-empty' }, t('scenes.subagents.hint'))),
|
|
8838
|
-
foot: archiveSegFoot(defined, defined ? sections.subagents.length : 0, t('memory.archive.emptySubagents')),
|
|
8839
9478
|
})
|
|
8840
9479
|
}
|
|
8841
9480
|
/**
|
|
@@ -9000,6 +9639,10 @@ function enabledMemoryCount(rules) {
|
|
|
9000
9639
|
pushRow('scenes.preview.row.skillsOff', sk.off, true)
|
|
9001
9640
|
pushRow('scenes.preview.row.personasOn', sub.on)
|
|
9002
9641
|
pushRow('scenes.preview.row.personasOff', sub.off)
|
|
9642
|
+
// 快捷提示词:只有绑了才出行(没绑 = 这一域进都不进,服务端给的就是 null)。
|
|
9643
|
+
var qp = plan.quickPrompts || {}
|
|
9644
|
+
pushRow('scenes.preview.row.quickOn', qp.on)
|
|
9645
|
+
pushRow('scenes.preview.row.quickOff', qp.off)
|
|
9003
9646
|
// 工具表方案(C1):绑了方案就是一句「会切成哪一份」,没绑不出行。
|
|
9004
9647
|
// 方案已经不存在时(hiddenCount 为 null)说"这次不切换",不报一个看不懂的数字。
|
|
9005
9648
|
if (plan.toolPreset) {
|
|
@@ -9122,6 +9765,9 @@ function enabledMemoryCount(rules) {
|
|
|
9122
9765
|
if (archive.mcp) parts.push(t('scenes.profile.mcp', { count: Object.keys(archive.mcp).length }))
|
|
9123
9766
|
if (Array.isArray(archive.skills)) parts.push(t('scenes.profile.skills', { count: archive.skills.length }))
|
|
9124
9767
|
if (Array.isArray(archive.subagents)) parts.push(t('scenes.profile.subagents', { count: archive.subagents.length }))
|
|
9768
|
+
// 快捷提示词绑了才报(没绑 = 这一域不碰)。**空表也要报** —— 它说的是"进入后全部停用",
|
|
9769
|
+
// 与"没绑"是两件事,漏掉就等于把用户的一个真实选择显示成没有这个选择。
|
|
9770
|
+
if (Array.isArray(archive.quickPrompts)) parts.push(t('scenes.profile.quickPrompts', { count: archive.quickPrompts.length }))
|
|
9125
9771
|
// P5:记忆**不再是档案的一部分**(开关是 `rules[*].enabled` 单一真相源),
|
|
9126
9772
|
// 所以这里不再统计 memories —— 否则会显示一个已经不存在的段的数量。
|
|
9127
9773
|
return parts.join(' · ')
|
|
@@ -9249,6 +9895,9 @@ function enabledMemoryCount(rules) {
|
|
|
9249
9895
|
})),
|
|
9250
9896
|
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) } },
|
|
9251
9897
|
React.createElement('div', { className: 'dsm-form' },
|
|
9898
|
+
// 弹窗里的字段分两模块:改名 / 描述是"这个场景是什么",下面三条是"进入时改什么"。
|
|
9899
|
+
// 一屏六行同一种字重、同一种间距时,用户读不出哪几条是一回事(用户 2026-10-01 反馈)。
|
|
9900
|
+
formSection(t('scenes.form.section.basic')),
|
|
9252
9901
|
React.createElement('label', { className: 'dsm-field' },
|
|
9253
9902
|
React.createElement('span', { className: 'dsm-label' }, t('memory.scene.field.name')),
|
|
9254
9903
|
React.createElement('input', {
|
|
@@ -9274,16 +9923,77 @@ function enabledMemoryCount(rules) {
|
|
|
9274
9923
|
onChange: function (e) { setSceneForm(Object.assign({}, sceneForm, { description: e.target.value, error: null })) },
|
|
9275
9924
|
})),
|
|
9276
9925
|
// 提示词预设:一个场景**只能绑一个**(单值字段天然单选);「不绑定」= 解绑。
|
|
9277
|
-
|
|
9926
|
+
formSection(t('scenes.form.section.binding')),
|
|
9927
|
+
// 下面三格都是「下拉 + 说明」这一类复合字段,外层用 `div` 而不是 `label`:
|
|
9928
|
+
// 不带 `for` 的 label 会把**第一个可标注后代**当成自己的控件 —— 点标题文字就等于
|
|
9929
|
+
// 点那颗下拉,而在快捷那一格里,第一个控件是勾选列表里的第一颗复选框
|
|
9930
|
+
// (点段头的空白会把那条勾翻掉)。纯文本输入框那两格保留 label,点标题聚焦输入框
|
|
9931
|
+
// 正是想要的。
|
|
9932
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
9278
9933
|
React.createElement('span', { className: 'dsm-label' }, t('scenes.field.prompt')),
|
|
9279
9934
|
presetOptions.length > 0
|
|
9280
9935
|
? React.createElement(SourceSelect, { options: presetOptions, value: sceneForm.prompt || '', onChange: function (v) { setSceneForm(Object.assign({}, sceneForm, { prompt: v })) } })
|
|
9281
9936
|
: React.createElement('p', { className: 'dsm-help' }, t('scenes.prompt.noPresets')),
|
|
9282
9937
|
helpBullets(t, 'scenes.field.prompt.hint')),
|
|
9938
|
+
// 快捷提示词(0.18.0):默认「当前启用的」= 这一域**不绑**,进出不碰它;
|
|
9939
|
+
// 切到「按场景勾选」才给清单 —— 勾进来的进入本场景后启用、没勾的停用,
|
|
9940
|
+
// 退出场景按进场景前那批开关还原(与子智能体那一档同一条机制)。
|
|
9941
|
+
// 空表是"绑了但一条都不勾"(进入后全部停用),与"不绑"是相反的意思,
|
|
9942
|
+
// 所以那颗下拉只给两个方向,不让用户靠"清空"去表达"不绑"。
|
|
9943
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
9944
|
+
React.createElement('span', { className: 'dsm-label' }, t('scenes.field.quickPrompts')),
|
|
9945
|
+
React.createElement(SourceSelect, {
|
|
9946
|
+
value: Array.isArray(sceneForm.quickPrompts) ? 'custom' : '',
|
|
9947
|
+
options: [{ value: '', label: t('scenes.field.quickPrompts.current') }, { value: 'custom', label: t('scenes.field.quickPrompts.custom') }],
|
|
9948
|
+
onChange: function (v) {
|
|
9949
|
+
setSceneForm(Object.assign({}, sceneForm, {
|
|
9950
|
+
quickPrompts: v === 'custom'
|
|
9951
|
+
// 切过来时从**当前开着的那些**起步(用户裁定:默认就是当前启用的),
|
|
9952
|
+
// 而不是给一张空表 —— 空表说的是"进入后全部停用"。
|
|
9953
|
+
? (quickOptions || []).filter(function (p) { return p.enabled !== false }).map(function (p) { return p.id })
|
|
9954
|
+
: null,
|
|
9955
|
+
}))
|
|
9956
|
+
},
|
|
9957
|
+
}),
|
|
9958
|
+
// 勾选清单是一张段卡片(与档案编辑器同一套排版):段头报"已勾几条 / 共几条"并给
|
|
9959
|
+
// 全选,段体按内容长(超过 6 行才内滚)。原先这里是一块裸的固定 216px 滚动体,
|
|
9960
|
+
// 一条也占 216px —— 弹窗中间凭空一片空白(用户 2026-10-01 反馈)。
|
|
9961
|
+
Array.isArray(sceneForm.quickPrompts)
|
|
9962
|
+
? (quickOptions || []).length
|
|
9963
|
+
? seg({
|
|
9964
|
+
title: t('scenes.field.quickPrompts.list'),
|
|
9965
|
+
count: t('scenes.seg.checked', { checked: (sceneForm.quickPrompts || []).length, total: quickOptions.length }),
|
|
9966
|
+
actions: segActions({
|
|
9967
|
+
defined: true, busy: busy,
|
|
9968
|
+
selectAllLabel: t('bulk.selectAll'), clearLabel: t('bulk.unselectAll'),
|
|
9969
|
+
onAll: function () {
|
|
9970
|
+
setSceneForm(Object.assign({}, sceneForm, { quickPrompts: quickOptions.map(function (p) { return p.id }) }))
|
|
9971
|
+
},
|
|
9972
|
+
onClear: function () { setSceneForm(Object.assign({}, sceneForm, { quickPrompts: [] })) },
|
|
9973
|
+
allChecked: quickOptions.length > 0 && (sceneForm.quickPrompts || []).length === quickOptions.length,
|
|
9974
|
+
}),
|
|
9975
|
+
body: React.createElement('div', { className: 'dsm-seg-body dsm-seg-fit' }, quickOptions.map(function (p) {
|
|
9976
|
+
var list = sceneForm.quickPrompts || []
|
|
9977
|
+
return pickRow({
|
|
9978
|
+
key: p.id, name: p.id, desc: p.description || '',
|
|
9979
|
+
checked: list.indexOf(p.id) >= 0,
|
|
9980
|
+
onChange: function () {
|
|
9981
|
+
var next = list.slice()
|
|
9982
|
+
var at = next.indexOf(p.id)
|
|
9983
|
+
if (at >= 0) next.splice(at, 1)
|
|
9984
|
+
else next.push(p.id)
|
|
9985
|
+
setSceneForm(Object.assign({}, sceneForm, { quickPrompts: next }))
|
|
9986
|
+
},
|
|
9987
|
+
})
|
|
9988
|
+
})),
|
|
9989
|
+
})
|
|
9990
|
+
: React.createElement('p', { className: 'dsm-help' }, t('scenes.field.quickPrompts.none'))
|
|
9991
|
+
: null,
|
|
9992
|
+
helpBullets(t, 'scenes.field.quickPrompts.hint')),
|
|
9283
9993
|
// 工具表方案(C1):与提示词预设同族的单值绑定字段,按用户 2026-09-28 裁定
|
|
9284
9994
|
// 从档案编辑器挪进这张表单 —— 两个「场景绑谁」的字段放在一起,档案编辑器
|
|
9285
9995
|
// 只留内容勾选。改的是启用中的场景时,服务端会立即把关停名单换成那份方案。
|
|
9286
|
-
React.createElement('
|
|
9996
|
+
React.createElement('div', { className: 'dsm-field' },
|
|
9287
9997
|
React.createElement('span', { className: 'dsm-label' }, t('scenes.field.toolTable')),
|
|
9288
9998
|
React.createElement(SourceSelect, {
|
|
9289
9999
|
value: String(sceneForm.toolTablePreset || ''),
|
|
@@ -10913,6 +11623,797 @@ function enabledMemoryCount(rules) {
|
|
|
10913
11623
|
React.createElement('div', { className: 'dsm-modal-actions' },
|
|
10914
11624
|
React.createElement('button', { type: 'button', className: 'dsm-btn dsm-btn-danger', disabled: busy, onClick: function () { submitTrashDelete(modal.entry) } }, t('memory.trash.purge')))) : null)
|
|
10915
11625
|
}
|
|
11626
|
+
// ── 斜杠命令「工具」段(对话输入框里直接管六个域)────────────────────────────
|
|
11627
|
+
//
|
|
11628
|
+
// 要解决的问题:切个场景、临时停一个 MCP,都得离开当前对话去「设置 → 工具」。这一段把
|
|
11629
|
+
// 这些**可逆**动作搬进宿主的 `/` 菜单:打一个 `/` → 出现「工具」段 → 点一行原地进入该域
|
|
11630
|
+
// → 点一条直接启停,**菜单停在原地不关**,接着点下一条也行。
|
|
11631
|
+
//
|
|
11632
|
+
// 为什么走 `inputTriggers` 而不是更省事的 `commandUi`(官方弹窗壳,宿主 /model 用的那个):
|
|
11633
|
+
// `commandUi` 注册的命令落在哪个段由宿主写死的 `SECTION_ROWS` 表决定,表里没有的一律排到
|
|
11634
|
+
// 「指令」段尾(dsh-client-ui-commands 的 sectionRows),插件**拿不到自己的段标题**。
|
|
11635
|
+
// 用户要的就是「工具」这个独立分区,所以只能走源这一层 —— 代价是弹窗壳、右下角 ✓ 与官方
|
|
11636
|
+
// 确认门都拿不到,状态呈现改由图标位(绿勾 / 空心框)+ 右侧灰字承担。
|
|
11637
|
+
//
|
|
11638
|
+
// 四条纪律:
|
|
11639
|
+
// ① 读写一律复用面板那批 op,不新增后端通道 —— 面板能做的这里才看得到,两边不会各算一套
|
|
11640
|
+
// 状态(技能写完后服务端照旧走 notifyChatCatalog,官方技能目录随之重拉)。
|
|
11641
|
+
// ② 只放**可逆**动作(进场景 / 启停)。增删改、导入导出、改配置仍只在面板里 —— `/` 菜单
|
|
11642
|
+
// 里没有确认门可用,把不可逆动作放进来就是拿用户的键盘赌他不多看一眼。
|
|
11643
|
+
// ③ 宿主没有 `inputTriggers` 服务时**什么都不发生**:`ctx.inject` 是等依赖的,服务不出现
|
|
11644
|
+
// 回调就不跑(cordis 语义),不报错、不拖累设置页。
|
|
11645
|
+
// ④ 点完不关菜单:宿主的 `settle()` 对**非下钻**的选取一律先 `reduce({type:'close'})`
|
|
11646
|
+
// (input-trigger client.js),所以叶子选取除了把 token 收回本域根,还要在微任务里用
|
|
11647
|
+
// 官方口重新打开这一层 —— 连点多个开关是这个功能的主要用法。整段包在 try 里,任何一步
|
|
11648
|
+
// 取不到就退回"点完关菜单",不能把功能本身带坏。
|
|
11649
|
+
var SLASH_SOURCE_NAME = 'tool-management'
|
|
11650
|
+
/** 快捷提示词那一段自己的源名(与「工具」段分开注册,才能排到官方「技能」段之后)。 */
|
|
11651
|
+
var QUICK_SOURCE_NAME = 'tool-quick-prompts'
|
|
11652
|
+
/** 「全局」保留场景的目录名 —— 与服务端 `src/memories/constants.ts` 的 `GLOBAL_SCENE` 同值。 */
|
|
11653
|
+
var GLOBAL_SCENE = 'global'
|
|
11654
|
+
/** 域清单读到的数据用多久(界面本身 5s 轮询,这里比它短一档,够一次连续输入)。 */
|
|
11655
|
+
var SLASH_CACHE_TTL_MS = 1500
|
|
11656
|
+
/**
|
|
11657
|
+
* 叶子行右侧那句备注截多长。宿主那一格自己会省略号(`itemDescription` 是 nowrap + ellipsis、
|
|
11658
|
+
* `flex:1`),但同一行还要放名字(≤40%)与灰色 alias(≤20%),留给它的大约四成 —— 先按这个
|
|
11659
|
+
* 宽度裁一刀,别让一行读起来像两段话。
|
|
11660
|
+
*/
|
|
11661
|
+
var LEAF_NOTE_MAX = 40
|
|
11662
|
+
|
|
11663
|
+
/** 图标位:启用态的绿色对勾。宿主按 `createElement(icon, {size})` 调用。 */
|
|
11664
|
+
function SlashOnIcon(props) {
|
|
11665
|
+
var s = props && props.size ? props.size : 14
|
|
11666
|
+
return React.createElement('svg', { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
11667
|
+
React.createElement('path', { d: 'M3.2 8.4 6.3 11.5 12.8 4.6', stroke: '#57c27d', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }))
|
|
11668
|
+
}
|
|
11669
|
+
/** 图标位:停用态的空心框(与绿勾同尺寸,两行并排时不会跳)。 */
|
|
11670
|
+
function SlashOffIcon(props) {
|
|
11671
|
+
var s = props && props.size ? props.size : 14
|
|
11672
|
+
return React.createElement('svg', { width: s, height: s, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
11673
|
+
React.createElement('rect', { x: 2.6, y: 2.6, width: 10.8, height: 10.8, rx: 3, stroke: '#6b6d75', strokeWidth: 1.6 }))
|
|
11674
|
+
}
|
|
11675
|
+
|
|
11676
|
+
/**
|
|
11677
|
+
* 六个域的读法与写法。
|
|
11678
|
+
*
|
|
11679
|
+
* `opts.quickPrompts` = 兼容页那颗「快捷提示词」开关:开着时快捷词并进「提示词」这一域
|
|
11680
|
+
* (用户 2026-10-01 裁定:这一域叫「提示词」,全局与快捷两类都在里面,各自一个分组标题)。
|
|
11681
|
+
*
|
|
11682
|
+
* `load` 回 `{items, summaryOf}`:items 是叶子行,summaryOf 由**当前显示的那批条目**算出摘要
|
|
11683
|
+
* (域行右侧那句话)。摘要不预先算成字符串,是因为乐观覆盖会改条目状态 —— 预先算好的那句
|
|
11684
|
+
* 会跟改完的列表自相矛盾(`0 启用 / 6 停用` 配上一排绿勾)。
|
|
11685
|
+
*
|
|
11686
|
+
* 每条 item 的 `run(next)` / `done(next)` 都**收目标状态为参数**:闭包里存着"读到的时候是什么
|
|
11687
|
+
* 状态",连点两次就会把同一个值发两遍(第二次点了没反应)。
|
|
11688
|
+
*
|
|
11689
|
+
* 带 `single` 的那一条是**单选且没有"全关"这一态**(全局基线):点亮它就把同组别的关掉,
|
|
11690
|
+
* 再点当前那条什么都不做。域级的 `exclusive` 留给"整域都是单选"的域(场景)—— 一个域里混
|
|
11691
|
+
* 两类时这类标记只能挂在条目上:挂在域上会把快捷词那颗开关一起管掉(关掉一条快捷词会被判成
|
|
11692
|
+
* "点的就是当前那条",什么都不发生)。
|
|
11693
|
+
*
|
|
11694
|
+
* **这一域里没有"贴文字"那种行**:快捷提示词的正文是用户在输入框里自己取用的,走的是
|
|
11695
|
+
* `quickSource` 那一段;这一域的每一行只回答"它现在算不算生效"。
|
|
11696
|
+
*
|
|
11697
|
+
* 每条 item 的 `key` 是**域内唯一身份**,`id` 只是给人看的那一段(也是 `/` 菜单里的 alias)。
|
|
11698
|
+
* 两者必须分开:技能名在两个目录里可以完全一样,MCP 服务器可以在用户级和项目级各有一份 ——
|
|
11699
|
+
* 只按 id 找目标就会点到 A 却改了 B(表现就是"点了没反应")。
|
|
11700
|
+
*/
|
|
11701
|
+
function slashDomains(t, opts) {
|
|
11702
|
+
const quickOn = !opts || opts.quickPrompts !== false
|
|
11703
|
+
return [
|
|
11704
|
+
{
|
|
11705
|
+
key: 'scene', token: 'tool-scene', label: t('slash.domain.scene'), exclusive: true,
|
|
11706
|
+
load: async function () {
|
|
11707
|
+
const res = await apiCall('rules-list', {})
|
|
11708
|
+
if (!res || !res.ok) throw new Error(translateError(t, res))
|
|
11709
|
+
const active = res.activeScene || null
|
|
11710
|
+
// 两个保留场景都不列**这一域**:`_shared` 是公共基线(面板里恒为常开、不给开关),
|
|
11711
|
+
// `global` 是「全局」—— 它永远跟着生效,列出来就是一行永远点不动的"未启用"。
|
|
11712
|
+
// 注意别把这条判断复制到记忆域:全局不进场景区,但它的记忆照样要能启停。
|
|
11713
|
+
const scenes = (res.scenes || []).filter(function (s) { return s.shared !== true && s.global !== true })
|
|
11714
|
+
return {
|
|
11715
|
+
summaryOf: function (items) {
|
|
11716
|
+
const now = items.filter(function (x) { return x.on === true })[0]
|
|
11717
|
+
return now ? t('slash.sum.current', { name: now.id }) : t('slash.sum.count', { n: items.length })
|
|
11718
|
+
},
|
|
11719
|
+
items: scenes.map(function (s) {
|
|
11720
|
+
return {
|
|
11721
|
+
id: s.name, key: s.name, label: s.name, on: s.name === active, note: s.description,
|
|
11722
|
+
// 单选,但**关得掉**:再点当前这一项 = 退出场景(`scenes:[]` 是服务端明确支持的
|
|
11723
|
+
// 写法,含义是"只注入全局与公共基线"),跟面板上那颗开关同一个出口。
|
|
11724
|
+
// 运行时(档案里的 MCP / 技能 / 人设 / 工具表)由服务端在这一次调用里跟着切
|
|
11725
|
+
// —— 见 `ops/scene-sync.ts` 的「两轴恒等」,这里**不需要**再补一枪 `scene-mode-set`。
|
|
11726
|
+
run: function (next) { return apiCall('rules-set-active', { scenes: next ? [s.name] : [] }) },
|
|
11727
|
+
done: function (next) { return t(next ? 'slash.did.scene' : 'slash.did.sceneOff', { name: s.name }) },
|
|
11728
|
+
}
|
|
11729
|
+
}),
|
|
11730
|
+
}
|
|
11731
|
+
},
|
|
11732
|
+
},
|
|
11733
|
+
{
|
|
11734
|
+
key: 'skill', token: 'tool-skill', label: t('slash.domain.skill'),
|
|
11735
|
+
load: async function () {
|
|
11736
|
+
const res = await apiCall('skill-state', {})
|
|
11737
|
+
const data = (res && res.data) || {}
|
|
11738
|
+
const items = []
|
|
11739
|
+
for (const root of data.roots || []) {
|
|
11740
|
+
if (root.removed === true) continue
|
|
11741
|
+
const rootName = rootDisplayName(t, root)
|
|
11742
|
+
// `toggleable === false` 的目录里单条技能没有开关(面板同样不给),列出来就是死行。
|
|
11743
|
+
if (root.toggleable === false) continue
|
|
11744
|
+
// **目录本身**一行,开着的也列 —— 不然只关得开、关不掉,等于半个开关。
|
|
11745
|
+
// 它钉在本组最上面(`first`),读起来像"组标题带一颗开关",与技能页卡片头那颗
|
|
11746
|
+
// 是同一个动作(`skill-source-enable` / `skill-source-disable`)。
|
|
11747
|
+
// 默认来源(dsh / hub / 官方内置)与项目作用域没有来源开关,按面板口径不给。
|
|
11748
|
+
if (root.scope !== 'project' && root.defaultSource !== true) {
|
|
11749
|
+
items.push({
|
|
11750
|
+
id: rootName, key: '@' + root.key, label: rootName + ' · ' + t('slash.skill.sourceRow'),
|
|
11751
|
+
on: root.enabled !== false, group: rootName, first: true,
|
|
11752
|
+
run: function (next) { return apiCall(next ? 'skill-source-enable' : 'skill-source-disable', { root: root.key }) },
|
|
11753
|
+
done: function (next) { return t(next ? 'slash.did.sourceOn' : 'slash.did.sourceOff', { name: rootName }) },
|
|
11754
|
+
})
|
|
11755
|
+
}
|
|
11756
|
+
// 目录停用时**照样把里面的技能读进来**,只是由覆盖表那一层先不显示(`effective`):
|
|
11757
|
+
// 这样"重新启用"当场就能把原本生效的那几条摆回来,不用等下一次真读。状态按"假设来源
|
|
11758
|
+
// 开着"算 —— 服务端的 `managerEnabled = 可写 && 来源启用 && 单条没被关掉`,来源关着时
|
|
11759
|
+
// 它恒为 false,而单条自己的意图在 `managerOverride` 里,把来源那一项补成 true 就是
|
|
11760
|
+
// 启用之后会读到的值。
|
|
11761
|
+
const sourceOff = root.enabled === false
|
|
11762
|
+
for (const skill of root.skills || []) {
|
|
11763
|
+
// 被同名遮蔽的那条按了也没效果(面板不给开关,给的是「启用这个」——那是切首选,
|
|
11764
|
+
// 不在"可逆启停"这一类里,所以 `/` 菜单不列)。
|
|
11765
|
+
if (skill.shadowedBy) continue
|
|
11766
|
+
const view = sourceOff ? Object.assign({}, skill, { managerEnabled: true }) : skill
|
|
11767
|
+
items.push({
|
|
11768
|
+
// `key` 带来源,`id` 不带:同名技能在两个目录里各有一行时,`id` 一样而 `key` 不一样
|
|
11769
|
+
// —— 只按 id 找目标会改到另一条(点了没反应就是这个)。alias 那行灰字仍显示短名。
|
|
11770
|
+
id: skill.name, key: root.key + '/' + skill.name,
|
|
11771
|
+
label: skill.declaredName || skill.name, on: isSkillEnabled(view), note: skill.description,
|
|
11772
|
+
// 分组标题 = 来源目录(技能页那套显示名)。技能动辄几十条又分属多个目录,
|
|
11773
|
+
// 不分组就是一片平铺,找不到自己要改的那一条。
|
|
11774
|
+
group: rootName,
|
|
11775
|
+
run: function (next) { return apiCall(next ? 'skill-enable' : 'skill-disable', { root: root.key, name: skill.name }) },
|
|
11776
|
+
done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: skill.name }) },
|
|
11777
|
+
})
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
11780
|
+
// 域行那句「N 启用 / M 停用」只数技能,不数目录行 —— 混进去的话数字里会多出几条
|
|
11781
|
+
// 根本不是技能的东西,而这句话是拿来核对"我这批技能现在什么状况"的。
|
|
11782
|
+
const skillSummary = slashOnOffSummary(t)
|
|
11783
|
+
return {
|
|
11784
|
+
summaryOf: function (shown) {
|
|
11785
|
+
return skillSummary(shown.filter(function (x) { return x.first !== true }))
|
|
11786
|
+
},
|
|
11787
|
+
items: items,
|
|
11788
|
+
}
|
|
11789
|
+
},
|
|
11790
|
+
},
|
|
11791
|
+
{
|
|
11792
|
+
key: 'mcp', token: 'tool-mcp', label: t('slash.domain.mcp'),
|
|
11793
|
+
load: async function () {
|
|
11794
|
+
const res = await apiCall('mcpm-list', {})
|
|
11795
|
+
if (!res || !res.ok) throw new Error(translateError(t, res))
|
|
11796
|
+
const items = (res.rows || []).map(function (row) {
|
|
11797
|
+
return {
|
|
11798
|
+
// 同一台服务器可以在用户级与项目级各有一份(同名同 id),所以 key 带 level。
|
|
11799
|
+
id: row.id, key: row.level + ':' + row.id, label: row.serverName || row.id, on: row.disabled !== true, note: row.notes,
|
|
11800
|
+
run: function (next) { return apiCall('mcpm-set-enabled', { id: row.id, level: row.level, enabled: next }) },
|
|
11801
|
+
done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: row.serverName || row.id }) },
|
|
11802
|
+
}
|
|
11803
|
+
})
|
|
11804
|
+
return { summaryOf: slashOnOffSummary(t), items: items }
|
|
11805
|
+
},
|
|
11806
|
+
},
|
|
11807
|
+
{
|
|
11808
|
+
key: 'agent', token: 'tool-agent', label: t('slash.domain.agent'),
|
|
11809
|
+
load: async function () {
|
|
11810
|
+
const res = await apiCall('subagent-list', {})
|
|
11811
|
+
if (!res || !res.ok) throw new Error(translateError(t, res))
|
|
11812
|
+
const items = (res.subagents || []).map(function (p) {
|
|
11813
|
+
return {
|
|
11814
|
+
id: p.name, key: p.name, label: p.name, on: p.enabled !== false, note: p.description,
|
|
11815
|
+
run: function (next) { return apiCall('subagent-toggle', { name: p.name, enabled: next }) },
|
|
11816
|
+
done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: p.name }) },
|
|
11817
|
+
}
|
|
11818
|
+
})
|
|
11819
|
+
return { summaryOf: slashOnOffSummary(t), items: items }
|
|
11820
|
+
},
|
|
11821
|
+
},
|
|
11822
|
+
{
|
|
11823
|
+
key: 'preset', token: 'tool-preset', label: t('slash.domain.preset'),
|
|
11824
|
+
load: async function () {
|
|
11825
|
+
const res = await apiCall('agentsmd-list', {})
|
|
11826
|
+
if (!res || !res.ok) throw new Error(translateError(t, res))
|
|
11827
|
+
const items = (res.presets || []).map(function (p) {
|
|
11828
|
+
return {
|
|
11829
|
+
id: p.id, key: p.id, label: p.id, on: p.active === true, note: p.description,
|
|
11830
|
+
// 应用基线改的是**整轮环境**(全局 AGENTS.md)。它可逆(再应用原来那套就切回去),
|
|
11831
|
+
// 所以按②的口径可以进 `/`;但这里没有官方确认门可用,于是把"当前是哪套"写进域行
|
|
11832
|
+
// 摘要 —— 点之前屏幕上至少说清了要换掉什么。
|
|
11833
|
+
// 点"当前这套"不做任何事(`single`):基线没有"全部不应用"这一态,
|
|
11834
|
+
// 把勾去掉等于让全局 AGENTS.md 凭空消失,那不是这里的语义。
|
|
11835
|
+
single: true,
|
|
11836
|
+
run: function () { return apiCall('agentsmd-apply', { id: p.id }) },
|
|
11837
|
+
done: function () { return t('slash.did.preset', { name: p.id }) },
|
|
11838
|
+
}
|
|
11839
|
+
})
|
|
11840
|
+
const quicks = await slashQuickRows(t, quickOn)
|
|
11841
|
+
// 两类都在时才给全局那一组出行标题:只剩一类时,「全局提示词」这条小标题与面包屑上
|
|
11842
|
+
// 的域名说的是同一件事,等于把同一句话写两遍。
|
|
11843
|
+
if (quicks.length) for (const it of items) it.group = t('prompts.group.global')
|
|
11844
|
+
return {
|
|
11845
|
+
summaryOf: function (shown) {
|
|
11846
|
+
const globals = shown.filter(function (x) { return x.single === true })
|
|
11847
|
+
const now = globals.filter(function (x) { return x.on === true })[0]
|
|
11848
|
+
const quicks = shown.filter(function (x) { return x.single !== true })
|
|
11849
|
+
// 基线那句只数全局那一类:快捷词不是"当前生效的基线",把它算进来这句就成谎了。
|
|
11850
|
+
const base = now ? t('slash.sum.current', { name: now.id }) : ''
|
|
11851
|
+
if (!quicks.length) return base
|
|
11852
|
+
// 快捷那一类给个数而不是"当前是哪条":它们是多对开关,说得出"开着几条"就够了。
|
|
11853
|
+
const on = quicks.filter(function (x) { return x.on === true }).length
|
|
11854
|
+
const tail = t('slash.sum.quick', { on: on, total: quicks.length })
|
|
11855
|
+
return base ? base + ' · ' + tail : tail
|
|
11856
|
+
},
|
|
11857
|
+
items: items.concat(quicks),
|
|
11858
|
+
}
|
|
11859
|
+
},
|
|
11860
|
+
},
|
|
11861
|
+
{
|
|
11862
|
+
key: 'memory', token: 'tool-memory', label: t('slash.domain.memory'),
|
|
11863
|
+
load: async function () {
|
|
11864
|
+
const res = await apiCall('rules-list', {})
|
|
11865
|
+
if (!res || !res.ok) throw new Error(translateError(t, res))
|
|
11866
|
+
const active = res.activeScene || ''
|
|
11867
|
+
// 场景显示名(`global` → 「全局」):分组标题用它,别让用户在 `/` 菜单里看到磁盘名。
|
|
11868
|
+
const labels = {}
|
|
11869
|
+
for (const s of res.scenes || []) if (s && s.name) labels[s.name] = s.label || s.name
|
|
11870
|
+
const toRow = function (r) {
|
|
11871
|
+
// 记忆的场景 = group 的第一段(与服务端 `sceneOf` 同一口径,见 memories/projection.ts)。
|
|
11872
|
+
const scene = String(r.group || '').split('/')[0]
|
|
11873
|
+
return {
|
|
11874
|
+
id: r.id, key: r.id, label: r.name, on: r.enabled !== false, note: r.description,
|
|
11875
|
+
scene: scene,
|
|
11876
|
+
// 打了字会跨场景搜,那时按场景分组才看得清是哪里的记忆。
|
|
11877
|
+
group: labels[scene] || scene || t('slash.group.noScene'),
|
|
11878
|
+
run: function (next) { return apiCall('rules-toggle', { id: r.id, enabled: next }) },
|
|
11879
|
+
done: function (next) { return t(next ? 'slash.did.on' : 'slash.did.off', { name: r.name }) },
|
|
11880
|
+
}
|
|
11881
|
+
}
|
|
11882
|
+
const all = (res.rules || []).filter(function (r) { return r.shadowed !== true }).map(toRow)
|
|
11883
|
+
// 「全局」这一组排最前面(用户 2026-09-30 裁定):它恒常注入,是这一域里最常要动的那组,
|
|
11884
|
+
// 不该沉在当前场景后面。sort 是稳定的,组内顺序不动。
|
|
11885
|
+
const globalFirst = function (list) {
|
|
11886
|
+
return list.slice().sort(function (a, b) {
|
|
11887
|
+
return (a.scene === GLOBAL_SCENE ? 0 : 1) - (b.scene === GLOBAL_SCENE ? 0 : 1)
|
|
11888
|
+
})
|
|
11889
|
+
}
|
|
11890
|
+
// 默认列**当前场景 + 「全局」**:记忆动辄上百条,全列进来等于把记忆页搬进 `/` 菜单,
|
|
11891
|
+
// 而这里要改的几乎总是正在用的那个场景;全局那份虽然不是"当前场景的",却对任何对话
|
|
11892
|
+
// 都成立、恒常在上下文里,用户想停掉的往往正是它。(场景域不列 global 是另一回事:
|
|
11893
|
+
// 它没有开关可给;这里的判断对象是记忆条目,不是场景。)打了字才跨场景搜。
|
|
11894
|
+
const shown = active ? all.filter(function (r) { return r.scene === active || r.scene === GLOBAL_SCENE }) : all
|
|
11895
|
+
return {
|
|
11896
|
+
summaryOf: function (items) {
|
|
11897
|
+
return active
|
|
11898
|
+
? t('slash.sum.sceneGlobal', { scene: labels[active] || active, n: items.length })
|
|
11899
|
+
: t('slash.sum.count', { n: all.length })
|
|
11900
|
+
},
|
|
11901
|
+
items: globalFirst(shown),
|
|
11902
|
+
searchPool: active ? globalFirst(all) : null,
|
|
11903
|
+
}
|
|
11904
|
+
},
|
|
11905
|
+
},
|
|
11906
|
+
]
|
|
11907
|
+
}
|
|
11908
|
+
|
|
11909
|
+
/** 进入场景时被引擎跳过的档案键(服务端 `rules-set-active` 把 `scene-mode-set` 的 stale 并进来)。 */
|
|
11910
|
+
function slashModeStaleWarn(t, res) {
|
|
11911
|
+
const stale = res && res.modeStale
|
|
11912
|
+
return Array.isArray(stale) && stale.length ? t('memory.archive.stale', { items: stale.join('、') }) : ''
|
|
11913
|
+
}
|
|
11914
|
+
|
|
11915
|
+
/**
|
|
11916
|
+
* 「提示词」域里快捷那一组(每一行 = 面板上那颗药丸开关,同一个动作、同一份状态)。
|
|
11917
|
+
*
|
|
11918
|
+
* 读失败或开关关着一律回空数组,**不抛**:这一域的主体是全局基线,不能因为快捷那一组读不到
|
|
11919
|
+
* 就把整域打成"读不到"(那连切基线都做不了了)。
|
|
11920
|
+
*
|
|
11921
|
+
* 这里列**全部**快捷词,包括已经关着的那些 —— 与贴文字那一段(`quickSource`)正好相反:
|
|
11922
|
+
* 那一段按 `enabled` 筛过,关掉的不再出现;如果这一域也只列开着的,关掉一条之后两个入口就
|
|
11923
|
+
* 都没有它了,只能回设置页去开。域是"管"的那一层,管的东西不能因为被管起来就消失。
|
|
11924
|
+
*/
|
|
11925
|
+
async function slashQuickRows(t, quickOn) {
|
|
11926
|
+
if (quickOn === false) return []
|
|
11927
|
+
let res
|
|
11928
|
+
try { res = await apiCall('quickprompt-list', {}) } catch (e) { return [] }
|
|
11929
|
+
if (!res || res.ok !== true) return []
|
|
11930
|
+
return (res.prompts || []).map(function (p) {
|
|
11931
|
+
return {
|
|
11932
|
+
// `key` 带前缀:全局预设与快捷词各存一份目录,同名完全可能(`id` 那一格仍然显示短名)。
|
|
11933
|
+
id: p.id, key: 'q:' + p.id, label: p.id, note: p.description,
|
|
11934
|
+
on: p.enabled !== false,
|
|
11935
|
+
group: t('prompts.group.quick'),
|
|
11936
|
+
// 与面板那颗开关同一条 op(`quickprompt-toggle`):只改 `meta.json` 里那一个布尔,
|
|
11937
|
+
// 正文与备注一个字不动。含义是"这条还出现在不在 `/` 的贴文字段里"。
|
|
11938
|
+
run: function (next) { return apiCall('quickprompt-toggle', { id: p.id, enabled: next }) },
|
|
11939
|
+
done: function (next) { return t(next ? 'slash.did.quickOn' : 'slash.did.quickOff', { name: p.id }) },
|
|
11940
|
+
}
|
|
11941
|
+
})
|
|
11942
|
+
}
|
|
11943
|
+
|
|
11944
|
+
/** 「N 启用 / M 停用」那句摘要(技能 / MCP / 子智能体三个域共用)。 */
|
|
11945
|
+
function slashOnOffSummary(t) {
|
|
11946
|
+
return function (items) {
|
|
11947
|
+
let on = 0
|
|
11948
|
+
for (const item of items) if (item.on === true) on += 1
|
|
11949
|
+
return t('slash.sum.onoff', { on: on, off: items.length - on })
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
11952
|
+
|
|
11953
|
+
/** 条目的身份:`key`(域内唯一)优先,没给就退回 `id`。覆盖表与点选定位都按它走。 */
|
|
11954
|
+
function slashItemKey(item) {
|
|
11955
|
+
return item.key === undefined ? String(item.id) : String(item.key)
|
|
11956
|
+
}
|
|
11957
|
+
|
|
11958
|
+
/**
|
|
11959
|
+
* 一次输入期间的读缓存 + 乐观覆盖,按域放在一起。
|
|
11960
|
+
*
|
|
11961
|
+
* 为什么要覆盖表:点完一条要**立刻**看到勾翻过来,而写请求是异步的、宿主快照也要下一次
|
|
11962
|
+
* 候选轮询才重读。覆盖只在"这个域重新真读过一次"时清掉(读回来的是权威状态,继续压着它
|
|
11963
|
+
* 就会把面板那边的改动盖住),写失败也立刻撤回。
|
|
11964
|
+
*/
|
|
11965
|
+
function slashState() {
|
|
11966
|
+
const entries = new Map()
|
|
11967
|
+
const overrides = new Map()
|
|
11968
|
+
const overrideKey = function (domainKey, itemKey) { return domainKey + '/' + itemKey }
|
|
11969
|
+
const clearOverrides = function (domainKey) {
|
|
11970
|
+
const prefix = domainKey + '/'
|
|
11971
|
+
for (const key of Array.from(overrides.keys())) if (key.indexOf(prefix) === 0) overrides.delete(key)
|
|
11972
|
+
}
|
|
11973
|
+
const read = function (domain, force) {
|
|
11974
|
+
const key = domain.key
|
|
11975
|
+
const hit = entries.get(key)
|
|
11976
|
+
if (!force && hit && hit.promise) return hit.promise
|
|
11977
|
+
if (!force && hit && !hit.promise && Date.now() - hit.at < SLASH_CACHE_TTL_MS) return hit.value
|
|
11978
|
+
const promise = Promise.resolve().then(domain.load).then(function (value) {
|
|
11979
|
+
clearOverrides(key)
|
|
11980
|
+
entries.set(key, { at: Date.now(), value: value })
|
|
11981
|
+
return value
|
|
11982
|
+
}, function (error) { entries.delete(key); throw error })
|
|
11983
|
+
entries.set(key, { promise: promise })
|
|
11984
|
+
return promise
|
|
11985
|
+
}
|
|
11986
|
+
/**
|
|
11987
|
+
* 把覆盖表叠到读回来的条目上(返回新数组,不改缓存里那份)。
|
|
11988
|
+
*
|
|
11989
|
+
* 顺带按**叠完之后**的状态收掉整组:技能目录被(真读或乐观)判成停用时,它里面的技能行不再
|
|
11990
|
+
* 出现。服务端在这种组合下根本不让单条生效,留着那几行就是"看得见、点了没反应";而"目录关了
|
|
11991
|
+
* 勾还在"这种半截状态,用户读到的意思是"里面还有技能",跟面板与下一次真读都对不上。
|
|
11992
|
+
*/
|
|
11993
|
+
const effective = function (domain, items) {
|
|
11994
|
+
const mapped = items.map(function (item) {
|
|
11995
|
+
const o = overrides.get(overrideKey(domain.key, slashItemKey(item)))
|
|
11996
|
+
return o === undefined ? item : Object.assign({}, item, { on: o })
|
|
11997
|
+
})
|
|
11998
|
+
const offGroup = {}
|
|
11999
|
+
for (const item of mapped) if (item.first === true && item.on !== true) offGroup[item.group] = true
|
|
12000
|
+
if (!Object.keys(offGroup).length) return mapped
|
|
12001
|
+
return mapped.filter(function (item) { return item.first === true || !offGroup[item.group] })
|
|
12002
|
+
}
|
|
12003
|
+
/**
|
|
12004
|
+
* 记一次"用户已经把它改成 next"。
|
|
12005
|
+
*
|
|
12006
|
+
* 单选的那一类(场景域的整域 `exclusive`;提示词域里全局那一组的每条 `single`)改一个就是
|
|
12007
|
+
* 把别的关掉,只记一条会让屏幕上同时出现两个绿勾,直到下一次真读才自己对齐 —— 那正是
|
|
12008
|
+
* "界面在骗人"的那几百毫秒。
|
|
12009
|
+
*
|
|
12010
|
+
* **只压同组**:提示词这一域里全局基线是单选、快捷词是多对开关,两类共处一域。不按分组收
|
|
12011
|
+
* 的话,点亮一条快捷词会把当前生效的那份基线的勾一起抹掉(覆盖表说的是假话,而下一次真读
|
|
12012
|
+
* 又把它翻回来 —— 表现就是勾自己跳了一下)。
|
|
12013
|
+
*/
|
|
12014
|
+
const mark = function (domain, items, item, next) {
|
|
12015
|
+
const touched = [overrideKey(domain.key, slashItemKey(item))]
|
|
12016
|
+
overrides.set(touched[0], next)
|
|
12017
|
+
if ((domain.exclusive === true || item.single === true) && next === true) {
|
|
12018
|
+
for (const other of items) {
|
|
12019
|
+
if (slashItemKey(other) === slashItemKey(item) || other.on !== true) continue
|
|
12020
|
+
if ((other.group || '') !== (item.group || '')) continue
|
|
12021
|
+
const otherKey = overrideKey(domain.key, slashItemKey(other))
|
|
12022
|
+
overrides.set(otherKey, false)
|
|
12023
|
+
touched.push(otherKey)
|
|
12024
|
+
}
|
|
12025
|
+
}
|
|
12026
|
+
return touched
|
|
12027
|
+
}
|
|
12028
|
+
const unmark = function (keys) { for (const key of keys) overrides.delete(key) }
|
|
12029
|
+
const flush = function () { entries.clear() }
|
|
12030
|
+
return { read: read, effective: effective, mark: mark, unmark: unmark, flush: flush }
|
|
12031
|
+
}
|
|
12032
|
+
|
|
12033
|
+
/** 名字 / 标题的小写包含匹配(宿主不对 source 的结果二次排序,官方技能源也是自己排的)。 */
|
|
12034
|
+
function slashMatch(needle, item) {
|
|
12035
|
+
if (!needle) return true
|
|
12036
|
+
const q = needle.toLowerCase()
|
|
12037
|
+
return String(item.id).toLowerCase().indexOf(q) >= 0 || String(item.label).toLowerCase().indexOf(q) >= 0
|
|
12038
|
+
}
|
|
12039
|
+
|
|
12040
|
+
/**
|
|
12041
|
+
* 把「工具」段与「快捷提示词」挂进宿主的 `/` 菜单。
|
|
12042
|
+
*
|
|
12043
|
+
* @param ctx 客户端根上下文(apply 的那个)
|
|
12044
|
+
* @param t 取词函数(每次候选都重新取,所以宿主换语言后下一次打开菜单就是新词)
|
|
12045
|
+
* @param settings 兼容页那两颗开关 `{ tools, quickPrompts }`,缺字段一律按开处理。
|
|
12046
|
+
* 两段**各挂各的**:「工具」里那个「提示词」域管两类提示词的启停,
|
|
12047
|
+
* 「快捷提示词」那一段管把正文贴进输入框 —— 两颗开关互不代替,关掉前者不会
|
|
12048
|
+
* 顺带把快捷词收走,反之也一样。
|
|
12049
|
+
* 两颗都关掉时**什么都不做**(不注册源,`/` 菜单回到出厂样子),而不是注册一个
|
|
12050
|
+
* 空段 —— 空段会让人以为开关没生效。
|
|
12051
|
+
*/
|
|
12052
|
+
function mountSlashCommands(ctx, t, settings) {
|
|
12053
|
+
const cfg = settings || {}
|
|
12054
|
+
// 挂载实况按段记:关着的那一段不该在 10s 后被报成"宿主没有这个服务"(那是另一回事),
|
|
12055
|
+
// 也不该一直停在"还没挂上" —— 它就是被关着的,照实说。
|
|
12056
|
+
const wanted = []
|
|
12057
|
+
if (cfg.tools !== false) wanted.push('tools')
|
|
12058
|
+
else setSlashMount('disabled', 'tools')
|
|
12059
|
+
if (cfg.quickPrompts !== false) wanted.push('quickPrompts')
|
|
12060
|
+
else setSlashMount('disabled', 'quickPrompts')
|
|
12061
|
+
if (!wanted.length) return
|
|
12062
|
+
// 「宿主压根没有这个服务」与「还没就绪」得区分开:`ctx.inject` 是等依赖的,服务不出现时
|
|
12063
|
+
// 回调**永远不跑**、也不报错 —— 于是兼容页那两枚标签会一直停在 pending,用户只能猜。
|
|
12064
|
+
// 10s 后还 pending 的段照实说"这台宿主没有 inputTriggers"(真缺服务时这是最终事实;
|
|
12065
|
+
// 万一它只是慢,源随后照样会挂上,状态会被 registered 覆盖)。
|
|
12066
|
+
try {
|
|
12067
|
+
setTimeout(function () {
|
|
12068
|
+
for (const key of wanted) if (slashMountStateOf(key) === 'pending') setSlashMount('missing', key)
|
|
12069
|
+
}, 10000)
|
|
12070
|
+
} catch (e) { /* 报不了状态不影响功能本身 */ }
|
|
12071
|
+
ctx.inject(['inputTriggers'], function (scope) {
|
|
12072
|
+
const triggers = scope.get('inputTriggers')
|
|
12073
|
+
const state = slashState()
|
|
12074
|
+
const sectionTitle = function () { return t('slash.section.tools') }
|
|
12075
|
+
// 每次候选都现取一份域表(域名跟着宿主语言走),但「快捷提示词要不要并进提示词域」这个
|
|
12076
|
+
// 决定来自启动时读到的那颗开关 —— 三处调用点(候选 / 面包屑 / 点选)必须用同一个判断,
|
|
12077
|
+
// 否则面包屑会指到一个候选里根本不存在的域。
|
|
12078
|
+
const domainOpts = { quickPrompts: cfg.quickPrompts !== false }
|
|
12079
|
+
const domainsOf = function () { return slashDomains(t, domainOpts) }
|
|
12080
|
+
|
|
12081
|
+
/** 会话那一份触发器控制器;拿不到就回 undefined(调用方一律静默降级)。 */
|
|
12082
|
+
function controllerOf(sessionId) {
|
|
12083
|
+
if (!sessionId) return undefined
|
|
12084
|
+
const scope = ctx.sessions && typeof ctx.sessions.scope === 'function' ? ctx.sessions.scope(sessionId) : undefined
|
|
12085
|
+
if (!scope) return undefined
|
|
12086
|
+
const service = ctx.get('inputTriggers')
|
|
12087
|
+
return service && typeof service.sessionOf === 'function' ? service.sessionOf(scope) : undefined
|
|
12088
|
+
}
|
|
12089
|
+
|
|
12090
|
+
/**
|
|
12091
|
+
* 写成功之后让菜单重取一次候选(官方口 `controller.refreshOpenMenu()`:只重取,
|
|
12092
|
+
* 不改 hit、不闪可见行)。
|
|
12093
|
+
*
|
|
12094
|
+
* 为什么必须有它:宿主要么按 query 变化、要么按它自己的目录失效才重取候选,而我们的
|
|
12095
|
+
* `state.flush()` 只是把自家缓存清空 —— 菜单没再问,屏幕上就还是**上一次那份快照**:
|
|
12096
|
+
* 条目多寡跟着旧状态走(目录刚关掉、里面十几条还列着;刚开、那几条又不见了),勾翻过来了
|
|
12097
|
+
* 列表却没换。乐观覆盖管的是"状态",管不了"这一屏该有哪些行"。菜单已经关了它就是空操作。
|
|
12098
|
+
*/
|
|
12099
|
+
function refreshAfterWrite(sessionId) {
|
|
12100
|
+
try {
|
|
12101
|
+
const controller = controllerOf(sessionId)
|
|
12102
|
+
if (controller && typeof controller.refreshOpenMenu === 'function') controller.refreshOpenMenu()
|
|
12103
|
+
} catch (e) { /* 刷不动只是这一屏晚一拍,改动本身已经生效 */ }
|
|
12104
|
+
}
|
|
12105
|
+
|
|
12106
|
+
/**
|
|
12107
|
+
* 点完把菜单留在这一层(纪律④)。
|
|
12108
|
+
*
|
|
12109
|
+
* 宿主的 `settle()` 对非下钻选取一律先关菜单,所以这里在微任务里用官方口重开:
|
|
12110
|
+
* `sessions.scope(sessionId)` → `inputTriggers.sessionOf(scope)` → `controller.toggleSource`
|
|
12111
|
+
* (宿主自己那个 `/` 菜单启动按钮走的就是同一条口,见 dsh-client-ui-conversation 的
|
|
12112
|
+
* toggleCommandMenu)。
|
|
12113
|
+
*
|
|
12114
|
+
* 两处必须照实做,否则会以更难看的方式坏:
|
|
12115
|
+
* · `span.draftRev` **现取**(`conversation.input.for(scope).state`)—— 宿主拿它做 CAS,
|
|
12116
|
+
* 差一位下一次点击就被静默拒掉(点了没反应,还查不出原因);
|
|
12117
|
+
* · 取到之后核对草稿里确实是刚写回的那串,对不上就什么都不做。
|
|
12118
|
+
* 任何一步拿不到都只是退回"点完关菜单",不能把功能本身带坏。
|
|
12119
|
+
*/
|
|
12120
|
+
function keepOpen(sessionId, query, spanStart) {
|
|
12121
|
+
if (!sessionId || typeof spanStart !== 'number') return
|
|
12122
|
+
try {
|
|
12123
|
+
const scope = ctx.sessions && typeof ctx.sessions.scope === 'function' ? ctx.sessions.scope(sessionId) : undefined
|
|
12124
|
+
if (!scope) return
|
|
12125
|
+
const service = ctx.get('inputTriggers')
|
|
12126
|
+
const controller = service && typeof service.sessionOf === 'function' ? service.sessionOf(scope) : undefined
|
|
12127
|
+
if (!controller || typeof controller.toggleSource !== 'function') return
|
|
12128
|
+
// 宿主自己已经把它重开了 —— 再 toggle 一次就是关掉,正好相反。
|
|
12129
|
+
if (controller.launcher && typeof controller.launcher.getSnapshot === 'function'
|
|
12130
|
+
&& controller.launcher.getSnapshot() === SLASH_SOURCE_NAME) return
|
|
12131
|
+
const conversation = ctx.get('conversation')
|
|
12132
|
+
const input = conversation && conversation.input && typeof conversation.input.for === 'function'
|
|
12133
|
+
? conversation.input.for(scope) : undefined
|
|
12134
|
+
const st = input && input.state && typeof input.state.getSnapshot === 'function' ? input.state.getSnapshot() : undefined
|
|
12135
|
+
if (!st) return
|
|
12136
|
+
const text = '/' + query
|
|
12137
|
+
const span = { start: spanStart, end: spanStart + text.length, draftRev: st.draftRev }
|
|
12138
|
+
if (String(st.draft || '').slice(span.start, span.end) !== text) return
|
|
12139
|
+
controller.toggleSource(SLASH_SOURCE_NAME, {
|
|
12140
|
+
trigger: '/', query: query, quoted: false, position: 'leading', span: span,
|
|
12141
|
+
})
|
|
12142
|
+
} catch (e) { /* 重开失败只是少一次连点,这次改动本身已经生效 */ }
|
|
12143
|
+
}
|
|
12144
|
+
|
|
12145
|
+
/** 摘要要单独等:域行不能因为一次读失败就不出现(那样整段会凭空消失,看起来像坏了)。 */
|
|
12146
|
+
async function domainRows(domains, query) {
|
|
12147
|
+
const picked = domains.filter(function (d) {
|
|
12148
|
+
if (!query) return true
|
|
12149
|
+
const q = query.toLowerCase()
|
|
12150
|
+
return d.token.toLowerCase().indexOf(q) >= 0 || d.label.toLowerCase().indexOf(q) >= 0
|
|
12151
|
+
})
|
|
12152
|
+
return Promise.all(picked.map(async function (d) {
|
|
12153
|
+
let description = ''
|
|
12154
|
+
try {
|
|
12155
|
+
const data = await state.read(d)
|
|
12156
|
+
description = data.summaryOf(state.effective(d, data.items)) || ''
|
|
12157
|
+
} catch (e) { description = '' }
|
|
12158
|
+
return {
|
|
12159
|
+
name: d.token, label: d.label, icon: SlashOffIcon, drill: true,
|
|
12160
|
+
...description ? { description: description } : {},
|
|
12161
|
+
section: sectionTitle(), value: d.key,
|
|
12162
|
+
}
|
|
12163
|
+
}))
|
|
12164
|
+
}
|
|
12165
|
+
|
|
12166
|
+
/** 叶子行(下钻后的某域条目)。 */
|
|
12167
|
+
async function leafRows(domain, rest) {
|
|
12168
|
+
let data
|
|
12169
|
+
try {
|
|
12170
|
+
data = await state.read(domain)
|
|
12171
|
+
} catch (e) {
|
|
12172
|
+
// 读失败:菜单里不出假列表,直接说清楚。列一行"出错了"再让用户点它,点下去什么
|
|
12173
|
+
// 也不会发生 —— 那是第二条谎。
|
|
12174
|
+
showPluginToast(t('slash.err.load', { domain: domain.label, error: errMsg(e) }), 'err')
|
|
12175
|
+
return []
|
|
12176
|
+
}
|
|
12177
|
+
// 记忆域带了一份跨场景全量:只在打了字的时候用(见 load 里的注释)。
|
|
12178
|
+
const pool = rest && data.searchPool ? state.effective(domain, data.searchPool) : state.effective(domain, data.items)
|
|
12179
|
+
const hit = pool.filter(function (item) { return slashMatch(rest, item) })
|
|
12180
|
+
// 排序三档:先按分组(来源目录 / 场景)**首次出现的顺序**,组内把"目录"那行钉在最上,
|
|
12181
|
+
// 再按"已启用的在前 + 名字"。分组顺序不能打乱 —— 宿主只在 section 与上一行不同时才画一条
|
|
12182
|
+
// 标题,同组被拆开就会冒出一串重复标题。
|
|
12183
|
+
const order = new Map()
|
|
12184
|
+
for (const item of hit) {
|
|
12185
|
+
const g = item.group || domain.label
|
|
12186
|
+
if (!order.has(g)) order.set(g, order.size)
|
|
12187
|
+
}
|
|
12188
|
+
hit.sort(function (a, b) {
|
|
12189
|
+
const ga = order.get(a.group || domain.label)
|
|
12190
|
+
const gb = order.get(b.group || domain.label)
|
|
12191
|
+
if (ga !== gb) return ga - gb
|
|
12192
|
+
// 目录行是这一组的总开关,跟着状态跳到别处就找不着了,所以永远钉在本组第一行。
|
|
12193
|
+
const fa = a.first === true ? 0 : 1
|
|
12194
|
+
const fb = b.first === true ? 0 : 1
|
|
12195
|
+
if (fa !== fb) return fa - fb
|
|
12196
|
+
if (a.on !== b.on) return a.on ? -1 : 1
|
|
12197
|
+
return String(a.id).localeCompare(String(b.id))
|
|
12198
|
+
})
|
|
12199
|
+
// 全量列出,不截断:宿主的候选面板自己带滚动(MenuView 的 viewport + 溢出提示),
|
|
12200
|
+
// 之前那套"每域只列 6 条 + 一行继续输入以筛选"是把滚动条的活拿来让用户打字。
|
|
12201
|
+
return hit.map(function (item) {
|
|
12202
|
+
// 右侧灰字给条目自己的备注(场景说明 / 技能描述 / MCP 备注 / 人设描述 / 预设描述 /
|
|
12203
|
+
// 记忆描述):启用状态图标位那枚勾已经说了,不必再占一格。没有备注的才退回状态词,
|
|
12204
|
+
// 免得那一格空着像坏了。
|
|
12205
|
+
const note = clipText(item.note, LEAF_NOTE_MAX)
|
|
12206
|
+
return {
|
|
12207
|
+
name: domain.token + '/' + item.id, label: item.label,
|
|
12208
|
+
description: note || (item.on ? t('slash.state.on') : t('slash.state.off')),
|
|
12209
|
+
icon: item.on ? SlashOnIcon : SlashOffIcon,
|
|
12210
|
+
section: item.group || domain.label,
|
|
12211
|
+
value: JSON.stringify({ domain: domain.key, key: slashItemKey(item) }),
|
|
12212
|
+
}
|
|
12213
|
+
})
|
|
12214
|
+
}
|
|
12215
|
+
|
|
12216
|
+
const source = {
|
|
12217
|
+
trigger: '/',
|
|
12218
|
+
name: SLASH_SOURCE_NAME,
|
|
12219
|
+
// 段的先后按宿主的源表排序走(`roster.sources(trigger)` 以 `order ?? 0` 升序):
|
|
12220
|
+
// 官方「指令」源没写 `order`(=0)、技能源是 2,所以 1 把「工具」夹在两者中间
|
|
12221
|
+
// (用户裁定:工具在指令下面、技能上面)。
|
|
12222
|
+
order: 1,
|
|
12223
|
+
// 每行都自带 section ⇒ 宿主不再渲染源标题行(MenuView 的判据),段名由我们词典出,
|
|
12224
|
+
// 中英随宿主语言。源名本身(tool-management)因此不会出现在界面上,只当唯一性用。
|
|
12225
|
+
showGroupTitle: false,
|
|
12226
|
+
async candidates(session, req) {
|
|
12227
|
+
// 只在行首提供:`帮我看看 /tool` 这种半路打出来的 `/` 不该弹我们的东西。
|
|
12228
|
+
if (req.position !== 'leading') return []
|
|
12229
|
+
const domains = domainsOf()
|
|
12230
|
+
const query = req.query || ''
|
|
12231
|
+
const at = query.indexOf('/')
|
|
12232
|
+
if (at < 0) return domainRows(domains, query)
|
|
12233
|
+
const token = query.slice(0, at)
|
|
12234
|
+
const domain = domains.filter(function (d) { return d.token === token })[0]
|
|
12235
|
+
if (!domain) return []
|
|
12236
|
+
return leafRows(domain, query.slice(at + 1))
|
|
12237
|
+
},
|
|
12238
|
+
header(session, req) {
|
|
12239
|
+
const query = req.query || ''
|
|
12240
|
+
if (query.indexOf('/') < 0) return undefined
|
|
12241
|
+
const domain = domainsOf().filter(function (d) { return query.indexOf(d.token + '/') === 0 })[0]
|
|
12242
|
+
if (!domain) return undefined
|
|
12243
|
+
// **不看 req.drilled**:宿主在每次"非下钻"的选取后都会把这个标记清掉
|
|
12244
|
+
// (input-trigger client.js 的 `this.drilled = action === 'drill'`),而叶子选取正是
|
|
12245
|
+
// 非下钻 —— 用它当判据的话,勾第一次开关之后面包屑就没了,"返回上一级"跟着消失。
|
|
12246
|
+
// 官方的 @file 源可以只认 drilled(打字打出来的路径草稿里自带上下文),这里不行。
|
|
12247
|
+
return [
|
|
12248
|
+
{ label: sectionTitle(), value: '/tool-' },
|
|
12249
|
+
{ label: domain.label, value: '/' + domain.token + '/', current: true },
|
|
12250
|
+
]
|
|
12251
|
+
},
|
|
12252
|
+
onPick(pick) {
|
|
12253
|
+
const candidate = pick.candidate
|
|
12254
|
+
const value = candidate.value
|
|
12255
|
+
if (typeof value !== 'string' || value === '') return undefined
|
|
12256
|
+
const sessionId = pick.session && pick.session.sessionId
|
|
12257
|
+
const spanStart = pick.span && typeof pick.span.start === 'number' ? pick.span.start : undefined
|
|
12258
|
+
/** 写回 token 并把菜单留在这一层(`inserted` 含前导 `/`,宿主要的是替换整个 token 段)。 */
|
|
12259
|
+
const stayAt = function (inserted) {
|
|
12260
|
+
const query = inserted.slice(1)
|
|
12261
|
+
Promise.resolve().then(function () { keepOpen(sessionId, query, spanStart) })
|
|
12262
|
+
return { text: inserted, continue: true }
|
|
12263
|
+
}
|
|
12264
|
+
// 面包屑:value 就是"回到那一层"要写回的 token(官方语义:回到上层与深入一层是同一个出口)。
|
|
12265
|
+
if (value.charAt(0) === '/') return stayAt(value)
|
|
12266
|
+
// 域行:原地进入该域。宿主只在点行右侧 › 或按 Tab 时发 action:'drill',点整行发的是
|
|
12267
|
+
// 'pick'(MenuView 的行渲染),所以这里不看 action —— 点哪都该进去。
|
|
12268
|
+
if (candidate.drill === true) return stayAt('/' + String(candidate.name) + '/')
|
|
12269
|
+
let target = null
|
|
12270
|
+
try { target = JSON.parse(value) } catch (e) { return undefined }
|
|
12271
|
+
if (!target || !target.domain || target.key === undefined) return undefined
|
|
12272
|
+
const domain = domainsOf().filter(function (d) { return d.key === target.domain })[0]
|
|
12273
|
+
if (!domain) return undefined
|
|
12274
|
+
// 回到本域根 + 菜单不关(纪律④):执行是异步的,所以先叠一份乐观状态再发请求。
|
|
12275
|
+
state.read(domain, true).then(function (data) {
|
|
12276
|
+
const pool = (data.searchPool || data.items)
|
|
12277
|
+
const shown = state.effective(domain, pool)
|
|
12278
|
+
// 按 key 定位,不按 id:技能/来源这类条目会重名(同名技能在两个目录里各有一行),
|
|
12279
|
+
// 按 id 取到的是列表里第一条同名的 —— 用户点了自己那行、改的却是别的目录里的。
|
|
12280
|
+
const item = shown.filter(function (x) { return slashItemKey(x) === String(target.key) })[0]
|
|
12281
|
+
if (!item) { showPluginToast(t('slash.err.gone'), 'err'); return }
|
|
12282
|
+
const next = item.on !== true
|
|
12283
|
+
if (item.single === true && !next) {
|
|
12284
|
+
// 单选又没有"全关"这一态的那一类(AGENTS.md 基线)点"当前这一项":什么都不做,
|
|
12285
|
+
// 只说清楚现在就是它。场景不在这儿 —— 关掉它=退出场景,是一个真实可逆的动作;
|
|
12286
|
+
// 快捷提示词也不在这儿 —— 那颗开关关得掉。
|
|
12287
|
+
showPluginToast(t('slash.did.already', { name: item.label }), 'ok')
|
|
12288
|
+
return
|
|
12289
|
+
}
|
|
12290
|
+
const touched = state.mark(domain, shown, item, next)
|
|
12291
|
+
return Promise.resolve(item.run(next)).then(function (res) {
|
|
12292
|
+
if (!res || res.ok === false) {
|
|
12293
|
+
state.unmark(touched)
|
|
12294
|
+
showPluginToast(res && res.error ? translateError(t, res) : t('slash.err.failed'), 'err')
|
|
12295
|
+
return
|
|
12296
|
+
}
|
|
12297
|
+
// 写成功后清缓存 + 让菜单重取一次:下一次候选读到真值,覆盖表随之让位(宿主的写
|
|
12298
|
+
// 在返回前已经把自己那份快照失效掉了,重读拿到的就是新状态)。
|
|
12299
|
+
state.flush()
|
|
12300
|
+
refreshAfterWrite(sessionId)
|
|
12301
|
+
const warn = joinWarn(stateSyncWarn(t, res) || sceneSyncWarn(t, res), slashModeStaleWarn(t, res))
|
|
12302
|
+
showPluginToast(warn ? joinWarn(item.done(next), warn) : item.done(next), warn ? 'warn' : 'ok')
|
|
12303
|
+
})
|
|
12304
|
+
}).catch(function (e) {
|
|
12305
|
+
showPluginToast(errMsg(e), 'err')
|
|
12306
|
+
})
|
|
12307
|
+
return stayAt('/' + domain.token + '/')
|
|
12308
|
+
},
|
|
12309
|
+
async matchEnter(session, line) {
|
|
12310
|
+
// 只认领"整行就是半个 token"这一种:`/tool-skill 帮我…` 后面还跟着正文时不拦,
|
|
12311
|
+
// 那是用户在写字,不是手滑按了回车。
|
|
12312
|
+
const trimmed = String(line || '').trim()
|
|
12313
|
+
if (trimmed.indexOf('/tool-') !== 0 || /\s/.test(trimmed)) return undefined
|
|
12314
|
+
showPluginToast(t('slash.err.pick'), 'err')
|
|
12315
|
+
return { text: '' }
|
|
12316
|
+
},
|
|
12317
|
+
}
|
|
12318
|
+
|
|
12319
|
+
// ── 快捷提示词单独成段(贴文字那一段,order 3 ⇒ 排在官方「技能」段之后)──────────────
|
|
12320
|
+
//
|
|
12321
|
+
// 与「工具 › 提示词」那一域是**两件事**,所以两处都在(用户 2026-10-01 裁定):域里那一组
|
|
12322
|
+
// 回答"这条要不要用"(与面板那颗药丸同一个动作),这一段回答"把这段话拿出来"——
|
|
12323
|
+
// 点一下把正文原样贴进输入框。同一屏里出现两种行会互相误读,所以贴文字绝不进域里。
|
|
12324
|
+
//
|
|
12325
|
+
// 为什么这一段能排到官方「技能」下面:宿主的段先后由**源**的 `order` 决定(`roster.sources`
|
|
12326
|
+
// 排序后一组一组渲染),同一个源里的行只能跟自己排顺序 —— 分组标题(section)只管一个源内部。
|
|
12327
|
+
//
|
|
12328
|
+
// 点一下 = 把正文原样贴进草稿,**不自动发送**(宿主要用户自己按 Enter):这一段是
|
|
12329
|
+
// 用户自己写的现成话,替他按下发送键就是把"贴上来看看"变成"已经发出去了"。
|
|
12330
|
+
const QUICK_TTL_MS = 15000
|
|
12331
|
+
const quickCache = { at: 0, promise: null, rows: null }
|
|
12332
|
+
function quickPrompts() {
|
|
12333
|
+
if (quickCache.promise) return quickCache.promise
|
|
12334
|
+
if (quickCache.rows && Date.now() - quickCache.at < QUICK_TTL_MS) return Promise.resolve(quickCache.rows)
|
|
12335
|
+
quickCache.promise = apiCall('quickprompt-list', {}).then(function (res) {
|
|
12336
|
+
quickCache.promise = null
|
|
12337
|
+
if (!res || res.ok !== true) throw new Error(translateError(t, res) || t('slash.err.failed'))
|
|
12338
|
+
quickCache.at = Date.now()
|
|
12339
|
+
quickCache.rows = res.prompts || []
|
|
12340
|
+
return quickCache.rows
|
|
12341
|
+
}, function (e) { quickCache.promise = null; throw e })
|
|
12342
|
+
return quickCache.promise
|
|
12343
|
+
}
|
|
12344
|
+
const quickSource = {
|
|
12345
|
+
trigger: '/',
|
|
12346
|
+
name: QUICK_SOURCE_NAME,
|
|
12347
|
+
order: 3,
|
|
12348
|
+
showGroupTitle: false,
|
|
12349
|
+
async candidates(session, req) {
|
|
12350
|
+
if (req.position !== 'leading') return []
|
|
12351
|
+
const query = String(req.query || '')
|
|
12352
|
+
// 只列一层:带 `/` 的草稿(`/tool-skill/…`)不是这一段的 token。
|
|
12353
|
+
if (query.indexOf('/') >= 0) return []
|
|
12354
|
+
let rows
|
|
12355
|
+
try { rows = await quickPrompts() } catch (e) {
|
|
12356
|
+
// 读不到就不出这一段。每次按键都弹一条 toast 只会变成噪声 —— 为什么没有这段,
|
|
12357
|
+
// 兼容页那行挂载状态说得出"挂上了",具体哪次读失败在面板里看得境。
|
|
12358
|
+
return []
|
|
12359
|
+
}
|
|
12360
|
+
const q = query.toLowerCase()
|
|
12361
|
+
return rows.filter(function (p) {
|
|
12362
|
+
// 面板上那颗开关关掉的不出现(与「提示词」域里那一组同一条判据,见 slashQuickRows)。
|
|
12363
|
+
if (p.enabled === false) return false
|
|
12364
|
+
if (!q) return true
|
|
12365
|
+
return String(p.id).toLowerCase().indexOf(q) >= 0 || String(p.description || '').toLowerCase().indexOf(q) >= 0
|
|
12366
|
+
}).map(function (p) {
|
|
12367
|
+
return {
|
|
12368
|
+
name: 'qp-' + p.id, label: p.id, value: p.id,
|
|
12369
|
+
// 右侧灰字给备注(与提示词页那一行同一个字段):正文本身点下去就整段进输入框,
|
|
12370
|
+
// 在这里再截一行只是把同一句话占成两处。
|
|
12371
|
+
description: String(p.description || ''),
|
|
12372
|
+
section: t('slash.section.quick'),
|
|
12373
|
+
// 正文随候选带着:宿主的 `settle()` 拿到 onPick 的返回值后**同步**执行插入,
|
|
12374
|
+
// 现场发请求读正文来不及(`quickprompt-list` 连正文一起回就是这个原因)。
|
|
12375
|
+
body: String(p.content || ''),
|
|
12376
|
+
}
|
|
12377
|
+
})
|
|
12378
|
+
},
|
|
12379
|
+
onPick(pick) {
|
|
12380
|
+
const body = String((pick.candidate && pick.candidate.body) || '')
|
|
12381
|
+
if (!body.trim()) { showPluginToast(t('slash.quick.empty'), 'err'); return undefined }
|
|
12382
|
+
// 不带 `continue`:菜单关掉、正文留在草稿里(宿主替换的是整个 `/qp-xxx` token)。
|
|
12383
|
+
return { text: body }
|
|
12384
|
+
},
|
|
12385
|
+
async matchEnter(session, line) {
|
|
12386
|
+
const trimmed = String(line || '').trim()
|
|
12387
|
+
if (trimmed.indexOf('/qp-') !== 0 || /\s/.test(trimmed)) return undefined
|
|
12388
|
+
showPluginToast(t('slash.err.pick'), 'err')
|
|
12389
|
+
return { text: '' }
|
|
12390
|
+
},
|
|
12391
|
+
}
|
|
12392
|
+
// 重名会当场抛(`registerSource` 判据是同 trigger 下源名唯一)—— 那是别的插件占了
|
|
12393
|
+
// `tool-management` 这个名字,不是本功能的故障,但必须说清"为什么没有这一段",
|
|
12394
|
+
// 否则表现就是"开关开着、菜单里却什么都没有"。
|
|
12395
|
+
const unregisters = []
|
|
12396
|
+
try {
|
|
12397
|
+
// 两段各按自己那颗开关注册:「工具」管六个域,「快捷提示词」管贴文字那一段 ——
|
|
12398
|
+
// 域里的快捷词是启停,不是贴文字,所以两处互不代替(见上面两段各自的注释)。
|
|
12399
|
+
if (cfg.tools !== false) unregisters.push(triggers.registerSource(source))
|
|
12400
|
+
if (cfg.quickPrompts !== false) unregisters.push(triggers.registerSource(quickSource))
|
|
12401
|
+
} catch (e) {
|
|
12402
|
+
// 后一个源抛了也要把前一个撤掉:只挂一半比一个都不挂更难解释。
|
|
12403
|
+
unregisters.forEach(function (off) { try { off() } catch (e2) { /* 已经报不了状态了 */ } })
|
|
12404
|
+
setSlashMount('conflict')
|
|
12405
|
+
return
|
|
12406
|
+
}
|
|
12407
|
+
for (const key of wanted) setSlashMount('registered', key)
|
|
12408
|
+
scope.effect(function () {
|
|
12409
|
+
return function () {
|
|
12410
|
+
unregisters.forEach(function (off) { try { off() } catch (e) { /* 撤不掉只是多留一个源,不影响界面 */ } })
|
|
12411
|
+
setSlashMount('unregistered')
|
|
12412
|
+
}
|
|
12413
|
+
})
|
|
12414
|
+
})
|
|
12415
|
+
}
|
|
12416
|
+
|
|
10916
12417
|
|
|
10917
12418
|
// 页面组件填进 factory 作用域那个占位对象(导出对象求值时已指向它)。
|
|
10918
12419
|
// 不要改回 `module.exports.X = ...`:apply 可能在本函数开头提前返回。
|